为什么对象初始化和集合初始化的组合使用Add方法?

时间:2018-10-20 14:49:08

标签: c# collections initialization

以下对象和集合初始值设定项的组合不会产生编译错误,但是从根本上是错误的(https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers#examples),因为在初始化过程中将使用Add方法:

public class Foo
{
    public List<string> Bar { get; set; }
}

static void Main()
{
    var foo = new Foo
    {
        Bar =
        {
            "one",
            "two"
        }
    };
}

因此您将获得NullReferenceException。在开发语言语法时做出如此不安全决定的原因是什么?为什么不使用例如新集合的初始化?

4 个答案:

答案 0 :(得分:4)

首先,它不仅适用于对象初始化和集合初始化。您在此处指的是嵌套集合初始化器,并且相同的规则(或您认为是问题)适用于嵌套对象初始化器。因此,如果您具有以下课程:

public class Foo
{
    public Bar Bar { get; set; }
}

public class Bar
{
    public string Baz { get; set; }
}

并且您使用以下代码

var foo = new Foo
{
    Bar = { Baz = "one" }
};

在运行时,您将获得相同的NRE,因为将不会创建new Bar,而是尝试设置Baz的{​​{1}}属性。

通常,对象/集合初始化程序的语法为

Foo.Bar

其中target = source 可以是表达式,对象初始化程序或集合初始化程序。请注意,source不是集合初始化器-它是一个对象创建表达式(毕竟,所有东西都是一个对象,包括集合)组合与集合初始化程序。区别就在这里-并不是{em> 省略new List<Bar> { … },而是给您一个选择,可以使用创建表达式+对象/集合初始化程序,或者仅使用 初始化程序。

不幸的是,C#文档没有解释这个概念,但是C#规范在Object Initializers部分中做了说明:

  

在等号后指定对象初始化器的成员初始化器是嵌套对象初始化器,即嵌入式对象的初始化。而不是为字段或属性分配新值,而是将嵌套对象初始化器中的分配视为对字段或属性成员的分配。嵌套对象初始化程序不能应用于具有值类型的属性,也不能应用于具有值类型的只读字段。

  

在等号后指定集合初始化程序的成员初始化程序是嵌入式集合的初始化。不必向目标字段,属性或索引器分配新的集合,而是将初始化器中给定的元素添加到目标引用的集合中。


那为什么呢?首先,因为它显然完全按照您的要求执行。如果您需要new,请使用new,否则它可以作为分配(或为集合添加)。

其他原因是-无法设置目标属性(已在其他答案中提及)。但它也可能是不可创建的类型(例如,接口,抽象类),即使是具体类(除了它是结构),它还是将如何决定应使用new(或{{1} },而不是new List<Bar>(如果我们有

new Bar

new MyBarList(如果有)

class MyBarList : List<Bar> { }

如您所见,编译器无法做出这样的假设,因此IMO语言功能旨在以非常清晰和合乎逻辑的方式工作。唯一令人困惑的部分可能是使用new MyBar运算符进行其他操作,但是我想这是一个折衷的决定-使用相同的运算符class MyBar : Bar { } 并在需要时添加=

答案 1 :(得分:2)

看看下面的代码及其输出,这是由于Debug.WriteLine():

public class Foo
{
    public ObservableCollection<string> _bar = new ObservableCollection<string>();

    public ObservableCollection<string> Bar
    {
        get
        {
            Debug.WriteLine("Bar property getter called");
            return _bar;
        }

        set
        {
            Debug.WriteLine("Bar allocated");
            _bar = value;
        }
    }

    public Foo()
    {
        _bar.CollectionChanged += _bar_CollectionChanged;
    }

    private void _bar_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        Debug.WriteLine("Item added");
    }
}

public MainWindow()
{
    Debug.WriteLine("Starting..");

    var foo = new Foo
    {
        Bar =
        {
            "one",
            "two"
        }
    };

    Debug.WriteLine("Ending..");
}

输出为:

Starting..
Bar property getter called
Item added
Bar property getter called
Item added
Ending..

您遇到的问题: 在开发语言语法时做出如此不安全决定的原因是什么?为什么不使用例如新集合的初始化?

答案: 如您所见,该功能设计者的意图不是重新分配集合,而是考虑到您自己管理集合分配,从而帮助您更轻松地向其中添加项目。

希望这一切都清楚了;)

答案 2 :(得分:0)

考虑以下代码:

class Program
{
    static void Main()
    {
        var foo = new Foo
        {
            Bar =
            {
                "one",
                "two"
            }
        };
    }
}

public class Foo
{
    public List<string> Bar { get; set; } = new List<string>();
}

编译器不知道您是否已经在类构造函数(或其他方法)中创建了新的列表实例。

回想一下集合初始化器是对现有集合上Add方法的一系列调用!

另请参阅: Custom Collection Initializers

还要注意,此初始化程序适用于作为属性公开的集合。因此,可以将集合初始化程序作为外部对象初始化程序(在您的示例中为Foo对象)的一部分。

但是,如果它是一个简单变量,编译器将不允许您以这种方式初始化集合。这是一个示例:

List<string> list = 
{
    "one",
    "two"
};

这将引发编译错误。

作为最后一个示例,以下代码的输出将为:“ 1、2、3、4”。我认为现在您明白了。 注意列表静态实例,以及Bar属性的“ set”中的private修饰符,这无关紧要,因为初始化程序仅调用Add方法,即使Bar“ set”为private时也可以访问该方法。

class Program
{
    static void Main()
    {
        var foo1 = new Foo
        {
            Bar =
            {
                "one",
                "two"
            }
        };

        var foo2 = new Foo
        {
            Bar =
            {
                "three",
                "four"
            }
        };

        PrintList(foo1.Bar);
    }

    public static void PrintList(List<string> list)
    {
        foreach (var item in list)
        {
            Console.Write(item + ", ");
        }
        Console.WriteLine();
    }

}

public class Foo
{
    private static readonly List<string> _bar = new List<string>();
    public List<string> Bar { get; private set; } = _bar;
}

答案 3 :(得分:0)

我相信这里要理解的关键是,有两种语法糖在起作用(或者至少应该有):

  1. 对象初始化
  2. 集合初始化

暂时拿掉List并将其视为对象:

public class Foo
{
    public object Bar { get; set; }
}

使用对象初始化时,您分配一个对象(或为空):

var foo = new Foo()
{
    Bar = new object(); //or Bar = null
}

现在,让我们回到您的原始示例,并在此示例上加上 Collection Initialization 。这次,编译器意识到此属性实现了IEnumerable,并且您提供的数组具有正确的类型,因此它尝试调用接口的Add方法。它必须首先查找对象,在您的情况下为null,因为您尚未在内部对其进行初始化。如果您对此进行调试,则会发现该getter被调用并返回null,因此会出错。

然后混合使用这两种功能的正确方法是让您分配一个使用值初始化的 new 对象:

var foo = new Foo()
{
    Bar = new List<string>(){ "one", "two" }
};

如果调试此版本,则会发现使用初始化的新实例代替了setter。

或者,您可以在内部初始化属性:

public List<string> Bar { get; set; } = new List<string>();

如果您调试此版本,则会发现该属性首先用一个值初始化,然后您的代码版本将正确执行(首先调用getter):

var foo = new Foo()
{    
    Bar = {"one", "two"}
};

为说明语法糖方面,集合初始化仅在构造函数调用语句的范围内起作用:

List<string> bar = {"one", "two" }; //ERROR: Can only use array initializer expressions to assign to array types. Try using a new expression instead.

List<string> bar = new[] { "one", "two" }; //ERROR: Cannot implicitly convert type 'string[]' to 'System.Collections.Generic.List<string>'

List<string> bar = new List<string>() { "one", "two" }; //This works!

如果您希望像原始示例中那样允许初始化,那么可以期望在调用Add方法之前将变量设置为实例。无论是否使用语法糖,都是如此。通过执行此操作,我同样会遇到相同的错误:

var foo = new Foo();
foo.Bar.Add("one");

因此,您可能需要初始化变量以覆盖所有基数,除非null值在您的应用程序中当然具有语义。