我最近正在研究一些代码,这些代码已经从使用十进制变为使用具有十进制数的复杂类型和表示分数的类型。我不得不更新一些测试,在输入时我忘了添加 new 关键字。代码已编译但测试仍然失败,抛出NullReferenceException。在那里,我意识到缺少新的,并且该属性未初始化。有谁知道为什么会这样?我在C#lang规范中找不到任何可以解释这一点的内容。
以下是代码示例:
public class Fraction
{
public int Numerator { get; set; }
public int Denominator { get; set; }
}
public class MyDecimal
{
public decimal? Decimal { get; set; }
public Fraction Fractional { get; set; }
}
public class ClassA
{
public MyDecimal Value { get; set; }
}
//...
var instance = new ClassA
{
Value = // new MyDecimal is missing here
{
Decimal = 2.0m,
Fractional = new Fraction
{
Numerator = 3,
Denominator = 4
}
}
}
请注意我使用的是C#6和VS 2015,但我在LINQPad中也得到了相同的结果。
如果有人可以解释一下(我正朝着你的方向寻找Jon Skeet :))我会很高兴。
答案 0 :(得分:6)
C#Specification 5.0将对象初始化器定义为( 7.6.10.2对象初始化器):
对象初始值设定项指定零个或多个字段或对象属性的值。
object-initializer: { member-initializer-listopt } { member-initializer-list , }
在详细解释之后,给出了一个与您的代码非常相似的示例:
如果Rectangle的构造函数分配了两个嵌入的Point实例
public class Rectangle { Point p1 = new Point(); Point p2 = new Point(); public Point P1 { get { return p1; } } public Point P2 { get { return p2; } } }
以下构造可用于初始化嵌入点 实例而不是分配新实例:
Rectangle r = new Rectangle { P1 = { X = 0, Y = 1 }, P2 = { X = 2, Y = 3 } };
与
具有相同的效果Rectangle __r = new Rectangle(); __r.P1.X = 0; __r.P1.Y = 1; __r.P2.X = 2; __r.P2.Y = 3; Rectangle r = __r;
但只有一个区别,这里的Point
个实例是在Rectangle
类中初始化的,它发生在Rectangle
的构造函数中。
因此规范的语法是有效的,但是在使用对象初始化程序初始化其属性以避免NRE之前,需要确保初始化Value
。
答案 1 :(得分:3)
对象初始化程序并不真正实例化您的成员。
请参阅以下代码:
var myInstance = new MyInstance { MyMember = new MyMember { Value = 3 }; }
这编译为:
var myMember= new MyMember();
myMember.MyMember = 3;
var myInstance = new MyInstance();
myInstance.MyMember = myMember;
在您的情况下,您忘记实例化 MyMember
,因此对象初始化程序会尝试访问该属性并为其分配更多值。这是因为对象初始化器总是在相应的构造函数之后运行,这在你的情况下没有调用。所以在你的情况下,它编译为:
var myInstance = new MyInstance();
myMymber.MyMember = 3;
导致NullReferenceException
myMember
从未实例化。
为什么这甚至编译?好吧,我假设编译器假定您在MyMember
的构造函数中实例化MyInstance
。它不知道你实际上是这么做的。
class Instance
{
MyMember MyMember = new MyMember();
}
离开会员null
当然是绝对有效的。
答案 2 :(得分:3)
对象初始值设定项语法允许您初始化对象而不首先创建它。如果您想保留对象标识,这一点非常重要。
例如,您可以使ClassA.Value
成为只读属性,并在对象构造函数中初始化它:
public class ClassA
{
public ClassA()
{
Value = new MyDecimal();
}
public MyDecimal Value { get; private set; }
}
当然,C#规范(摘录自版本5)中明确概述了这种行为:
7.6.10.2对象初始值设定项
成员初始值设定项指定等号后的表达式,其处理方式与对字段或属性的赋值(第7.17.1节)相同。
在等号后面指定对象初始值设定项的成员初始值设定项是嵌套对象初始值设定项,即嵌入对象的初始化。而不是为字段或属性分配新值,嵌套对象初始值设定项中的赋值被视为对字段或属性成员的赋值。嵌套对象初始值设定项不能应用于具有值类型的属性,也不能应用于具有值类型的只读字段。
由于您的Value
初始值设定项是嵌套初始值设定项,因此只要Value
成员已经初始化,就可以在不初始化的情况下分配Value
成员。编译器无法验证Value
是否为null
,因此无法给您错误。