C# 嵌套初始化的奇怪行为

4
在这些初始化语句编译的前提下。
List<int> l = new List<int> { 1, 2, 3 };
Dictionary<int, int> d = new Dictionary<int, int> { [1] = 11, [2] = 22 };
Foo f = new Foo { Bar = new List<int>() };

而这将不会

List<int> l = { 1, 2, 3 };
Dictionary<int, int> d = { [1] = 11, [2] = 22 };
Foo f = { Bar = new List<int>() };

我有一个关于嵌套初始化的问题。给定以下类:

public class Foo {
    public List<int> Bar { get; set; } = new List<int>();
    public Dictionary<int, Foo> Baz { get; set; } = new Dictionary<int, Foo>();
}

我意外地发现,您实际上可以这样做:

Foo f = new Foo {
    Bar = { 1, 2, 3 },
    Baz = {
        [1] = {
            Bar = { 4, 5, 6 }
        }
    }
};

虽然它编译通过,但会抛出一个KeyNotFoundException异常。因此,我将属性更改为

public List<int> Bar { get; set; } = new List<int> { 4, 5, 6 };
public Dictionary<int, Foo> Baz { get; set; }
    = new Dictionary<int, Foo> { [1] = new Foo { Bar = new List<int>() { 1, 2, 3 } } };

假设这是替换现有成员的一些不寻常的符号表示法。现在,初始化会抛出一个 StackOverflowException 异常。
所以我的问题是,为什么这个表达式甚至能够通过编译?它应该做什么?我觉得我一定漏掉了什么非常明显的东西。

堆栈溢出的原因很简单,因为创建一个 Foo 将会创建一个 Dictionary 并创建一个要添加到其中的 Foo,这反过来又会创建一个带有 FooDictionary,以此类推。 - juharr
请注意,可以将其更改为Baz = {{1,new Foo { Bar = {4,5,6}}}};以使其正常工作,因为该符号使用字典的Add(Tkey,TValue),在这种情况下,如果没有new Foo,它将无法编译。 - juharr
1个回答

7
所以我的问题是,为什么这个表达式甚至能够编译通过?
它是一个带有集合初始化程序值的对象初始化程序。根据C#规范第7.6.10.2节:
指定等号后面的集合初始化程序的成员初始化程序是嵌入式集合的初始化。与将新集合分配给字段或属性不同,给定初始化程序中的元素将添加到由字段或属性引用的集合中。
因此,您的代码大致相当于:
Foo tmp = new Foo();
tmp.Bar.Add(1);
tmp.Bar.Add(2);
tmp.Bar.Add(3);
tmp.Baz[1].Bar.Add(4); // This will throw KeyNotFoundException if Baz is empty
tmp.Baz[1].Bar.Add(5);
tmp.Baz[1].Bar.Add(6);
Foo f = tmp;

您的初始化版本会抛出一个StackOverflowException,因为Foo的初始化器需要创建一个新的Foo实例,该实例又需要创建一个新的Foo实例,以此类推。


StackOverflowException 是一个愚蠢的疏忽。虽然我不知道规范的这一部分。谢谢。 - Marcus S.
那不应该是 temp.Baz[1].Bar.Add(4) 吗? - juharr
1
@Sung 如果你指的是字典中的 Foo,那么它并没有被创建,它假设已经有一个 Foo 在索引1处,并且只是在向其 Bar 添加值。 - juharr
我想知道他们在“无类型”字典初始化中考虑了哪些用例。如果它执行(tmp.Baz.ContainsKey(1) ? tmp.Baz[1] : tmp.Baz[1] = new ...).Bar.Add(4),那么这种表示法将非常好和简洁。 - Marcus S.
遗憾的是,在MSDN上似乎没有关于嵌套对象初始化器的文档(http://msdn.microsoft.com/en-us/library/bb384062.aspx)。它们在C#规范中有说明(C# 5.0规范的7.6.10.2):“在等号后指定对象初始化器的成员初始化器是一个嵌套对象初始化器,即嵌入式对象的初始化。[...]嵌套对象初始化器中的赋值被视为对字段或属性成员的赋值。” - Brian
显示剩余4条评论

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