如何创建ImmutableDictionary的新实例?

时间:2014-06-04 09:22:34

标签: c# .net-4.5 immutability

我想写这样的东西:

var d = new ImmutableDictionary<string, int> { { "a", 1 }, { "b", 2 } };

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

  

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

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

8 个答案:

答案 0 :(得分:36)

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

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)

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

你自己的回答和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();

答案 1 :(得分:28)

首先创建“普通”词典并拨打ToImmutableDictionary(根据您自己的回答),或使用ImmutableDictionary<,>.Builder

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

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

答案 2 :(得分: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();
    }
}

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

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

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

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

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

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

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

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

ToImmutableDictionary将迭代遍历字典的内容(通过Dictionary<TKey, TValue>内部工作方式呈现 less 效率的过程 - 执行此操作需要更多工作比起一个简单的列表所做的那样,完全没有从构建字典的工作中获益,然后必须做同样的工作,如果你直接使用了构建器。

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

我同意Jon的沮丧,即不可变的集合无法提供开箱即用的方法。

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

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

答案 3 :(得分:9)

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

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

答案 4 :(得分:7)

或者这个

ImmutableDictionary<string, int>.Empty
   .Add("a", 1)
   .Add("b", 2);

还有AddRange方法可用。

答案 5 :(得分:0)

您可以编写一个带有隐式运算符的自定义 ImmutableDictionaryLiteral 类,使语法非常接近您想要的。

public class ImmutableDictionaryLiteral<TKey, TValue> : Dictionary<TKey, TValue>
{
    public static implicit operator ImmutableDictionary<TKey, TValue>(ImmutableDictionaryLiteral<TKey, TValue> source)
    {
        return source.ToImmutableDictionary();
    }
}

然后您通过声明一个 ImmutableDictionary<TKey, TValue> 变量并使用 ImmutableDictionaryLiteral<TKey, TValue> 值进行初始化来调用它。

ImmutableDictionary<string, string> dict = new ImmutableDictionaryLiteral<string, string>()
{
    { "key", "value" },
    { "key2", "value2" }
};

这也适用于对象初始值设定项语法

ImmutableDictionary<string, string> dict = new ImmutableDictionaryLiteral<string, string>()
{
    ["key"] = "value",
    ["key2"] = "value2"
};

答案 6 :(得分:0)

我更喜欢这种语法:

var dict = ImmutableDictionaryEx.Create<int, string>(
                (1, "one"),
                (2, "two"),
                (3, "three"));

使用此方法可以轻松实现:

public static class ImmutableDictionaryEx {

    /// <summary>
    /// Creates a new <see cref="ImmutableDictionary"/> with the given key/value pairs.
    /// </summary>
    public static ImmutableDictionary<K, V> Create<K, V>(params (K key, V value)[] items) where K : notnull {
        var builder = ImmutableDictionary.CreateBuilder<K, V>();
        foreach (var (key, value) in items)
            builder.Add(key, value);
        return builder.ToImmutable();
    }

}

答案 7 :(得分:0)

我在这里没有看到另一个答案(也许是一种新方法?):

只需使用 CreateRange 方法通过 IEnumerable<KVP> 创建一个新的 ImmutableDictionary。

// The MyObject record example
record MyObject(string Name, string OtherStuff);


// The init logic for the ImmutableDictionary
var myObjects = new List<MyObject>();
var dictionary = ImmutableDictionary
   .CreateRange(myObjects.Select(obj => new KeyValuePair<string, MyObject>(obj.Name, obj)));