如何创建ImmutableDictionary的新实例?

Luk*_*ský 41 c# immutability .net-4.5

我想写这样的东西:

var d = new ImmutableDictionary<string, int> { { "a", 1 }, { "b", 2 } };
Run Code Online (Sandbox Code Playgroud)

(使用ImmutableDictionary来自System.Collections.Immutable).这似乎是一个简单的用法,因为我预先声明了所有的价值 - 那里没有变异.但这给了我错误:

类型' System.Collections.Immutable.ImmutableDictionary<TKey,TValue>'没有定义构造函数

我应该如何用静态内容创建一个新的不可变字典?

Dir*_*irk 43

您无法使用集合初始值设定项创建不可变集合,因为编译器会将它们转换为对该Add方法的一系列调用.例如,如果您查看IL代码,var d = new Dictionary<string, int> { { "a", 1 }, { "b", 2 } };您将获得

IL_0000: newobj instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::.ctor()
IL_0005: dup
IL_0006: ldstr "a"
IL_000b: ldc.i4.1
IL_000c: callvirt instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::Add(!0, !1)
IL_0011: dup
IL_0012: ldstr "b"
IL_0017: ldc.i4.2
IL_0018: callvirt instance void class [mscorlib]System.Collections.Generic.Dictionary`2<string, int32>::Add(!0, !1)
Run Code Online (Sandbox Code Playgroud)

显然,这违反了不可变集合的概念.

你自己的答案和Jon Skeet都是解决这个问题的方法.

// lukasLansky's solution
var d = new Dictionary<string, int> { { "a", 1 }, { "b", 2 } }.ToImmutableDictionary();

// Jon Skeet's solution
var builder = ImmutableDictionary.CreateBuilder<string, int>();
builder.Add("a", 1);
builder.Add("b", 2);   
var result = builder.ToImmutable();
Run Code Online (Sandbox Code Playgroud)


Jon*_*eet 31

首先创建一个"普通"字典并调用ToImmutableDictionary(根据您自己的答案),或使用ImmutableDictionary<,>.Builder:

var builder = ImmutableDictionary.CreateBuilder<string, int>();
builder.Add("a", 1);
builder.Add("b", 2);
var result = builder.ToImmutable();
Run Code Online (Sandbox Code Playgroud)

令人遗憾的是,构建器没有我可以告诉的公共构造函数,因为它阻止你使用集合初始化器语法,除非我错过了某些东西......该Add方法返回void意味着你可以'甚至链调用它,使它更烦人 - 据我所知,你基本上不能使用构建器在单个表达式中创建不可变字典,这是非常令人沮丧的:(

  • 荒谬的是,Add 方法不流畅。谁写了那个构建器类?它应该是 `ImmutableDictionary.Builder.Add("aKey","aValue").Add("anotherKey","anotherValue").Build()` 就像曾经编写的所有其他构建器一样。 (3认同)
  • @AdamHouldsworth:我会对缺乏针对建造者的建设者背后的原因感兴趣。对于不可变的集合本身,这很好-但对于构建器而言则不行。 (2认同)
  • @AdamHouldsworth:事实上,由于“Add”的返回类型为空,我之前的帖子不会起作用。恶心!看看可怕的编辑... (2认同)

Ian*_*ths 11

您可以使用这样的帮助:

public struct MyDictionaryBuilder<TKey, TValue> : IEnumerable
{
    private ImmutableDictionary<TKey, TValue>.Builder _builder;

    public MyDictionaryBuilder(int dummy)
    {
        _builder = ImmutableDictionary.CreateBuilder<TKey, TValue>();
    }

    public void Add(TKey key, TValue value) => _builder.Add(key, value);

    public TValue this[TKey key]
    {
        set { _builder[key] = value; }
    }

    public ImmutableDictionary<TKey, TValue> ToImmutable() => _builder.ToImmutable();

    public IEnumerator GetEnumerator()
    {
        // Only implementing IEnumerable because collection initializer
        // syntax is unavailable if you don't.
        throw new NotImplementedException();
    }
}
Run Code Online (Sandbox Code Playgroud)

(我正在使用新的C#6表达式成员,所以如果你想在旧版本上编译它,你需要将它们扩展为正式成员.)

使用该类型,您可以使用集合初始化程序语法,如下所示:

var d = new MyDictionaryBuilder<int, string>(0)
{
    { 1, "One" },
    { 2, "Two" },
    { 3, "Three" }
}.ToImmutable();
Run Code Online (Sandbox Code Playgroud)

或者如果你使用的是C#6,你可以使用对象初始化器语法,以及对索引器的新支持(这就是为什么我在我的类型中包含了一个只写索引器):

var d2 = new MyDictionaryBuilder<int, string>(0)
{
    [1] = "One",
    [2] = "Two",
    [3] = "Three"
}.ToImmutable();
Run Code Online (Sandbox Code Playgroud)

这结合了两个优点的好处:

  • 避免建立一个完整的 Dictionary<TKey, TValue>
  • 允许您使用初始化程序

构建一个完整的问题是构建Dictionary<TKey, TValue>它有一大堆开销; 传递基本上是键/值对的列表是一种不必要的昂贵方式,因为它会仔细设置一个哈希表结构,以实现您永远不会实际使用的高效查找.(您将要执行查找的对象是您最终得到的不可变字典,而不是您在初始化期间使用的可变字典.)

ToImmutableDictionary只是要通过字典的内容进行迭代(渲染的过程中的方式有效的Dictionary<TKey, TValue>内部工作-它需要更多的工作要做,这会比用一个简单的列表),从走进建设工作取得绝对没有好处在字典中,然后必须执行与直接使用构建器时相同的工作.

Jon的代码避免了这种情况,仅使用构建器,这应该更有效.但他的方法不允许你使用初始化器.

我分享Jon的沮丧,即不可变的集合没有提供开箱即用的方法.

编辑2017/08/10:我必须将零参数构造函数更改为接受它忽略的参数,并在您使用它的任何地方传递虚拟值.@ gareth-latty在评论中指出a struct不能有零参数构造函数.当我最初编写这个不成立的例子时:有一段时间,C#6的预览允许你提供这样的构造函数.在C#6发布之前已经删除了这个功能(显然是在我写完原始答案之后),可能是因为它令人困惑 - 有些情况下构造函数无法运行.在这种特殊情况下使用它是安全的,但遗憾的是语言功能已不复存在.Gareth的建议是把它改成一个类,但是然后使用它的任何代码都必须分配一个对象,导致不必要的GC压力 - 我使用a的全部原因struct是使得使用这种语法成为可能而没有额外的运行时开销.

我尝试修改它来执行延迟初始化,_builder但事实证明,JIT代码生成器不够智能,无法优化这些,所以即使在发布版本中,它也会检查_builder您添加的每个项目.(并且它内联检查和相应的调用CreateBuilder,结果产生了大量具有大量条件分支的代码).最好进行一次性初始化,如果您希望能够使用此初始化程序语法,则必须在构造函数中进行初始化.因此,使用此语法而无需额外成本的唯一方法是_builder在其构造函数中初始化一个结构,这意味着我们现在需要这个丑陋的伪参数.


Luk*_*ský 10

到目前为止,我最喜欢这个:

var d = new Dictionary<string, int> { { "a", 1 }, { "b", 2 } }.ToImmutableDictionary();
Run Code Online (Sandbox Code Playgroud)

  • 请注意,这将在运行时导致不必要的额外工作 - 请参阅我的答案http://stackoverflow.com/a/28448092/497397 (2认同)

Mih*_*kic 7

或这个

ImmutableDictionary<string, int>.Empty
   .Add("a", 1)
   .Add("b", 2);
Run Code Online (Sandbox Code Playgroud)

还有AddRange方法可用.

  • 作为记录,根据[文档](https://docs.microsoft.com/en-us/dotnet/api/system.collections.immutable.immutabledictionary-2.add?view=netcore-3.1#System_Collections_Immutable_ImmutableDictionary_2_Add__0__1_),每次调用 Add 时,您都会在此处创建 2 个不可变字典。 (2认同)