似乎对象初始化器和构造函数+属性赋值不同,为什么?

4

考虑下面的代码:

class Data
{
    public string Name;
    public string NameWithSufix;
}

class Behaviour
{
    private Data data;
    public string Name { get { return data.Name; } private set { } }

    public Behaviour()
    {
        data = new Data()
        {
            Name = "My Name",
            NameWithSufix = Name + " Sufix",
        };
        //data = new Data();
        //data.Name = "My Name";
        //data.NameWithSufix = Name + " Sufix";
    }
}

class Program
{
    static void Main(string[] args)
    {
        Behaviour behaviour = new Behaviour();
    }
}

如果您运行这个程序,它将在Name属性处失败,抛出NullReferenceException异常。这篇文章这篇文章以及Visual Studio试图说服我对象初始化和对象构造函数跟随属性赋值是相同的,但事实并非如此。如果我将构造函数主体与注释代码交换,它可以正常工作。看起来好像初始化器在尝试分配属性之前并没有真正运行构造函数。为什么呢?

这是因为属性在构造函数运行之前被初始化,而你正在 Name 中访问 data 的成员。 - Crowcoder
2个回答

4

NameWithSufix = Name 中的 Name 指向 data.Name,此时 data 为空。更好的表示对象初始化器的方式是:

Data d = new Data();
d.Name = "My Name";
d.NameWithSufix = this.data.Name /*Name*/ + " Sufix"; // <-- see the problem here

this.data = d;

注意,this.data 直到对象初始化程序完成后才被设置。

这得到了C# 语言规范的支持,正如 PetSerAl 所指出的那样。


请问您能否指向支持您最后一句话的文档?Visual Studio建议我将构造函数+赋值缩短为初始化器,因此我认为它们是相同的,但显然它们不是。我希望了解为什么不是这样。 - Dread Boy
不确定是否有记录,但您的代码清楚地显示了这一实现。 - Patrick Hofman
@DreadBoy 你可以查看C#规范,其中描述了对象初始化器的处理方式。 - user4003407
@PetSerAl,谢谢您。它说初始化程序会创建一个临时变量来保存对象,在初始化后将引用值复制到实际变量中。您能否将其发布为答案,以便我可以接受它? - Dread Boy
@dre 不确定那是否有帮助,因为那与我的答案相同。 - Patrick Hofman
我看到你在回答中添加了规格链接,我已经接受了。谢谢! - Dread Boy

3

首先运行Behavior构造函数。问题在于Data类的初始化尚未完成,因此以下引用会抛出异常。

NameWithSufix = Name + " Sufix",

因为它调用了 get { return data.Name; },但此时的 data 仍然是 null


更新:

Patrick Hofman 在他的答案中表述得更好、更准确——问题不在于 Data 类的初始化不完整,而是新实例尚未分配给 data 变量。

此外,他要求官方文档做出澄清——如果您同意,请点赞。


它没有先运行构造函数,可能需要重新措辞。 - Crowcoder
我不相信这个。data 不应该为空,因为构造函数应该已经被调用了。这是我链接的两个 SO 问题告诉我的。虽然我在官方文档中没有找到任何关于操作顺序的说明。 - Dread Boy
1
@Crowcoder,你能帮我找到它吗?通过阅读这篇文章(https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers),我只发现了“编译器首先访问默认实例构造函数,然后处理成员初始化程序”这句话。这听起来像是构造函数先运行。当构造函数完成时,对象应该被创建,对吧? - Dread Boy
1
我认为文档应该写成“编译器通过首先访问默认实例构造函数,然后处理成员初始化,最后将实例分配给变量来处理对象初始化程序。” - Patrick Hofman
建议的文档更改:https://github.com/dotnet/docs/pull/3733 - Patrick Hofman
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接