初始化不带“new List”的列表属性会导致 NullReferenceException

Initializing list property without "new List" causes NullReferenceException

提问人:Ajai 提问时间:9/2/2015 最后编辑:Alexei LevenkovAjai 更新时间:2/17/2020 访问量:5104

问:

using System;
using System.Collections.Generic;

class Parent
{
   public Child Child { get; set; }
}

class Child
{
   public List<string> Strings { get; set; }
}

static class Program
{
   static void Main() {
      // bad object initialization
      var parent = new Parent() {
         Child = {
            Strings = { "hello", "world" }
         }
      };
   }
}

上面的程序编译良好,但在运行时崩溃,对象引用未设置为对象的实例

如果您在上面的代码片段中注意到,我在初始化子属性时省略了 new

显然,正确的初始化方式是:

      var parent = new Parent() {
         Child = new Child() {
            Strings = new List<string> { "hello", "world" }
         }
      };

我的问题是,为什么 C# 编译器在看到第一个构造时不抱怨?

为什么中断的初始化是有效的语法?

      var parent = new Parent() {
         Child = {
            Strings = { "hello", "world" }
         }
      };
C# NullReferenceException

评论


答:

18赞 Jeroen Vannevel 9/2/2015 #1

这不是语法中断,而是您在根本没有实例化的属性上使用对象初始值设定项。你写的内容可以扩展到

var parent = new Parent();
parent.Child.Strings = new List<string> { "hello", "world" };

这抛出 : 您正在尝试分配该属性所包含的属性,而 仍然是 。 首先使用构造函数进行实例化,可以解决这个问题。NullReferenceExceptionStringsChildChildnullChild

4赞 Colin Grealy 9/2/2015 #2

第二种语法对只读属性有效。如果更改代码以在各自的构造函数中初始化 Child 和 Strings 属性,则语法有效。

class Parent
{
    public Parent()
    {
        Child = new Child();
    }

    public Child Child { get; private set; }
}

class Child
{
    public Child()
    {
        Strings = new List<string>();
    }
    public List<string> Strings { get; private set; }
}

static class Program
{
    static void Main()
    {
        // works fine now
        var parent = new Parent
        {
            Child =
            {
                Strings = { "hello", "world" }
            }
        };

    }
}

评论

5赞 GSerg 9/2/2015
集合初始值设定项与只读属性无关。此代码之所以有效,是因为您输入了构造函数,而不是因为该属性具有 .Strings = new List<string>();private set
0赞 Colin Grealy 9/2/2015
再次读取代码。Child 属性不是集合,而是只读属性。
1赞 GSerg 9/3/2015
@Ajai,科林的回答具有欺骗性,因此是错误的。它指出只读属性与集合初始值设定项的行为之间存在联系。事实并非如此。集合初始值设定项和只读属性不必相互执行任何操作,它们是完全不相关的概念。请看我链接到的这个答案
1赞 GSerg 9/3/2015
矩形初始值设定项是一个 .OP 所谈论的初始值设定项是一个 .它们是不同的。集合初始值设定项由编译器转换为一系列或对方法的调用,而不是转换为任何其他内容。此行为与只读属性或 s 无关。object-initializercollection-initializerAdd()object-initializer
2赞 GSerg 9/5/2015
您的下一句话,如果您更改代码以在各自的构造函数中初始化 Child 和 Strings 属性,则语法是正确的,如果该语句后面跟着“因为对象初始值设定项不创建对象,所以它们只设置属性或将项添加到集合”而不是“它用于只读属性”,我会支持您的答案。
12赞 Guffa 9/2/2015 #3

初始化没有错,但它试图初始化不存在的对象。

如果类具有创建对象的构造函数,则初始化有效:

class Parent {
  public Child Child { get; set; }
  public Parent() {
    Child = new Child();
  }
}

class Child {
  public List<string> Strings { get; set; }
  public Child() {
    Strings = new List<string>();
  }
}
0赞 Joe Smith 9/2/2015 #4

在编译时不能始终检查引用 null。尽管编译器有时会警告在赋值之前使用变量。编译器工作正常。这是一个运行时错误。

7赞 GSerg 9/2/2015 #5

您似乎误解了集合初始值设定项的作用。

它只是一个语法糖,它将大括号中的列表转换为Add() 方法的一系列调用,这些调用必须在正在初始化的集合对象上定义。
因此,你的效果与
= { "hello", "world" }

.Add("hello");
.Add("world");

显然,如果未创建集合,这将失败并出现 NullReferenceException。

0赞 EventHorizon 2/17/2020 #6

请注意,此语法可能会导致一些意外结果和难以发现的错误:

class Test
{
    public List<int> Ids { get; set; } = new List<int> { 1, 2 };
}

var test = new Test { Ids = { 1, 3 } };

foreach (var n in test)
{
    Console.WriteLine(n);
}

您可能希望输出为 1,3,但实际情况是:

1
2
1
3

评论

0赞 Petru Lutenco 8/4/2023
那么这是否意味着 Ids = { 1, 3 } 调用 AddRange 而不是 new List<int> { 1, 3} ?文档中某处提到过吗?
1赞 EventHorizon 8/8/2023
编译器为每个值发出单独的 Add 语句。Test 的构造函数包含两个值为 1 和 2 的 Add 语句,主体包含两个用于 1 和 3 的附加 Add 语句。如果在 Test 中删除初始值设定项,则会在“var test =”代码行上收到 null 引用异常。我相信某处的一篇文章中有更好的解释,但我忘记了在哪里。官方文档中提到了它:learn.microsoft.com/en-us/dotnet/csharp/programming-guide/......
0赞 Petru Lutenco 8/9/2023
很好的解释,谢谢!