如何使内联数组初始化像字典初始化一样工作?

21

为什么可以像这样初始化一个 Dictionary<T1,T2>

var dict = new Dictionary<string,int>() { 
    { "key1", 1 },
    { "key2", 2 }
};

...但不能用完全相同的方式初始化 KeyValuePair<T1,T2> 对象的数组:

var kvps = new KeyValuePair<string,int>[] {
    { "key1", 1 },
    { "key2", 2 }
};
// compiler error: "Array initializers can only be used in a variable 
// or field initializer.  Try using a new expression instead."

我意识到我可以通过仅编写new KeyValuePair<string,int>() { "key1", 1 }为每个项目使第二个示例起作用。 但我想知道是否可能使用与第一个示例中可能的简洁语法相同的类型。

如果不可能,那么是什么使Dictionary类型如此特殊?


我喜欢这个问题的方向,但它似乎还不够成熟。我喜欢把重点放在ListDictionary上(以便排除数组),尽管错误消息可能会有所不同?(思考中:如何知道如何从{a,b}创建KVP?;-) - user166390
(我认为标题不太正确。) - user166390
@pst 是的,我认为你越来越接近我所问的核心了。我不太擅长写问题... - McGarnagle
6个回答

33
集合初始化程序语法将被转换为调用带有适当数量参数的Add方法:
var dict = new Dictionary<string,int>();
dict.Add("key1", 1);
dict.Add("key2", 2);

这种特殊的初始化语法也适用于其他具有 Add 方法并实现了 IEnumerable 接口的类。让我们创建一个完全疯狂的类来证明 Dictionary 并没有什么特别之处,而且这种语法可以应用于任何合适的类:

// Don't do this in production code!
class CrazyAdd : IEnumerable
{
    public void Add(int x, int y, int z)
    {
        Console.WriteLine(x + y + z); // Well it *does* add...
    }

    public IEnumerator GetEnumerator() { throw new NotImplementedException(); }
}

现在你可以编写以下内容:
var crazyAdd = new CrazyAdd
{
    {1, 2, 3},
    {4, 5, 6}
};

输出:

6
15

在线查看它的工作效果:ideone

至于你提到的其他类型:

  • 对于数组来说,它没有Add方法,所以无法使用。
  • List<T>Add方法,但只接受一个参数。

1
好的...但是List确实有一个add方法,尝试使用它会产生相同的错误。 - McGarnagle
1
那是一个非常恶毒的Add()。我喜欢它。 - lesderid

11

由于Dictionary类有一个接受两个参数的Add重载方法,所以它可以使用。数组甚至没有Add方法,更别提带两个参数的方法了。

Dictionary类专门设计用于内部与KeyValuePair<,>一起使用,这是您不需要手动调用构造函数的唯一原因,而是在幕后调用具有两个参数的Add并构建KeyValuePair。

其他任何IEnumerable<KeyValuePair<,>>都没有这种特殊实现,因此必须通过以下方式初始化:

var array = new KeyValuePair<int, int>[] {
    new KeyValuePair<int, int>(1, 2),
    new KeyValuePair<int, int>(3, 4)
};

您可以使用自己的类来创建相同的行为,比如说您实现了这个:

class ThreeTupleList<T1, T2, T3> : List<Tuple<T1, T2, T3>>
{
    public void Add(T1 a, T2 b, T3 c)
    {
        this.Add(new Tuple<T1, T2, T3>(a, b, c));
    }

    // You can even implement a two-argument Add and mix initializers
    public void Add(T1 a, T2 b)
    {
        this.Add(new Tuple<T1, T2, T3>(a, b, default(T3)));
    }
}

您可以按照此方式初始化它,甚至可以混合使用三个、两个和一个参数的初始化程序:

var mylist = new ThreeTupleList<int, string, double>()
{
    { 1, "foo", 2.3 },
    { 4, "bar", 5.6 },
    { 7, "no double here" },
    null
};

3

你的问题源于它是一个数组,而不是集合。

var kvps = new KeyValuePair<string,int>[] {
    { "key1", 1 },
    { "key2", 2 }
};

应该是这样的:

var kvps = new KeyValuePair<string, int>[] {
    new KeyValuePair<string, int>("key1", 1),
    new KeyValuePair<string, int>("key2", 2)
};

赠品是括号。[]表示数组。{}表示集合。

4
也许我的问题表达不够清晰,请仔细阅读。我在回答中提到了你的例子。我知道如何编写初始化,但是我想知道更简单的语法为什么不可行的根本原因。 - McGarnagle

2

感谢多位回答者指出,Add 方法是使初始化语法生效的秘密武器。因此,我可以通过继承相关类(KeyValuePair)来实现我的目标:

public class InitializableKVPs<T1,T2> : IEnumerable<KeyValuePair<T1,T2>>
{
    public void Add(T1 key, T2 value) 
    {
        throw new NotImplementedException();
    }

    public IEnumerator<KeyValuePair<string,string>>  GetEnumerator()
    {
        throw new NotImplementedException();
    }

    IEnumerator  IEnumerable.GetEnumerator()
    {
        throw new NotImplementedException();
    }
}

现在编译器已经接受了这个代码:

var kvps = new InitializableKVPs<string,int> {
    { "key1", 1 },
    { "key2", 2 }
};

编辑: Philip Daubmeier 的回答提供了一个实际而简洁的实现。


2
没错。如果你只是继承一个已经实现了ICollection<>的类,比如List<>,那么请看我的更新答案中对此的非常简单的实现。 - Philip Daubmeier
刚发现您甚至可以混合使用具有不同参数数量的初始化器元素 :) 我再次更新了我的答案... - Philip Daubmeier

1

为C# 6.0语法覆盖IDictionary

如果有人像我一样来到这里,希望为新的C# 6.0字典初始化器语法节省一些按键次数,那么可以做到,但需要派生自IDictionary。您只需要实现this[] set方法即可使其起作用,这将留下大量未实现的方法。

类的实现如下:

// Seriously!  Don't do this in production code! Ever!!!
public class CrazyAdd2 : IDictionary<string, int>
{
    public int this[string key]
    {
        get { throw new NotImplementedException(); }
        set {Console.WriteLine($"([{key}]={value})"); }
    }

#region NotImplemented
// lots of empty methods go here
#endregion
}

然后使用它:
var crazyAdd2 = new CrazyAdd2
{
    ["one"] = 1,
    ["two"] = 2,
};

和输出:

([one]=1)
([two]=2)

这里有一个演示整个过程的fiddle:

https://dotnetfiddle.net/Lovy7m


0

你可能看不到,但语法是相同的。唯一的区别在于数组以元素作为输入,而字典以二维数组作为输入。

int[] a = new int[]{5,6};
int[,] a = new int[]{{5,6},{3,6}};
Dictionary<int,int> a = new Dictionary<int,int>{{5,6},{3,6}};

3
这不完全是允许这种语法糖的原因。实际上,这是由通用字典的实现造成的,该字典提供了一个两个参数的“Add”方法,在幕后为您构建“KeyValuePair”。 - Philip Daubmeier

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