【问题标题】:Truly Immutable Dictionary in .NET.NET 中真正不可变的字典
【发布时间】:2011-03-25 06:57:42
【问题描述】:

早上好,下午好,晚上好,

仍然基于我关于 .NET 中 immutable dictionaries 的问题,我提出了以下问题:如果 TKeyTValue 是值类型,那么您可以使字典真正不可变,因为两者都不是它的内部结构和值本身都不能改变,如果这些参数是引用类型键和值可以很容易地改变,从而改变字典本身。我说的对吗?

非常感谢。

【问题讨论】:

  • 如果您正在寻找 ReadOnlyDictionary 实现,请查看此处:bit.ly/ecgwhZ
  • 是的,你是对的。如果你用 C 语言编程,你就有一个 int * const ptr。你想要一个 const int * const ptr。你不能在 C# 中使用它,除非你的对象契约指定对象是不可变的。
  • 引用类型也可以是不可变的,不可变不是值类型所特有的。
  • @MattDavey:是的,引用类型可以是不可变的,但它不是必需的。 Miguel 描述的问题不是不存在不可变引用类型,而是存在可变引用类型。
  • @Miguel 顺便说一句:您现在可能已经完成了不可变字典的实现。但是您可能对我自己的不可变字典 Map<Key,Value> 感兴趣,它具有一些独特的功能和算法属性,以及一个有趣的可变兄弟类型 MMap<Key,Value>

标签: c# dictionary immutability


【解决方案1】:

如果 TKey 和 TValue 是值类型,则可以使字典真正不可变,因为它的内部结构和值本身都不能改变,如果这些参数是引用类型,键和值可以很容易地改变,从而改变字典本身。我说的对吗?

不完全是。您已经确定了一个真正的问题,但您对它的描述没有得到充分考虑。

首先,我注意到,是的,卡在不可变字典中的值类型是不可变的,即使它是可变值类型。为什么?因为值类型只能通过改变包含它们的变量来改变。如果您的字典没有公开它用于存储值类型的变量,那么就无法更改这些变量。

然而,即使值类型本身是不可变的不可变值类型也可以包含对可变引用类型的引用,现在我们遇到了同样的问题。将字典限制为值类型只会将问题推到一个层次,它并不能解决它!您真正需要保证“深度”不变性的是 blittable 值类型的字典。 “blittable”是指没有引用类型字段的值类型。 (之所以这么称呼,是因为您可以通过将这些位直接“blitting”到磁盘来将其中一个序列化到存储中。)

不幸的是,在泛型类型系统中没有约束到 blittable 值类型。

现在让我们考虑不可变字典中可变引用类型的一般问题,无论这些引用类型是直接存在还是通过值类型的字段存在。如果引用类型在不可变字典中被带外变异,会出现什么问题?

嗯,首先想到的是,一个不可变的字典应该两次对同一个问题给出相同的答案两次,现在情况不再如此。如果您说“customers[name].Address”,您希望两次得到相同的答案,但如果客户是可以变异的引用类型,您将得到一个可能不同的答案。这可能是可取的,也可能是不可取的。 (请注意,字典两次给出了相同的答案:它对客户对象给出了相同的引用。实际上是客户对象没有给出两次相同的答案。)

假设您不尝试记住可能变化的问题的答案,这通常不是什么大问题。

更大的问题是,当哈希表中作为键的对象发生变异时,会改变其哈希值并“丢失”表中的对象。

如果发生这种情况,则说明有人没有遵守准则。准则是 (1) 引用类型应尽可能不将其哈希码(和相等性)基于可以变异的数据,以及 (2) 用作哈希表中的键的对象不应发生变异。

【讨论】:

  • .NET 中有 blittable 类型的概念吗?我想您可以制作一个,但对于运行时而言,它与非 blittable 类型没有什么不同? .NET 中还有许多可变的值类型,例如 Point、Size 等。你知道你们是否根据指南做出了这个决定,还是因为在早期版本的 .NET 中没有考虑不变性?只是好奇,因为它甚至可能是因为性能原因,所以我理解。
  • @Joan:运行时确实很在意,因为不安全代码中的指针类型必须是指向 blittable 类型的指针。我不知道为什么 Point 等是可变的。
  • 谢谢埃里克,不知道。
  • 如果这些引用的目的只是封装它们的不可变方面(最显着的是它们的标识),那么不可变对象保存对可变对象的引用是非常好的。例如,如果一家维修店保留了其在某一天维修的汽车的 VIN 的不可变列表,则该列表可用于确定该维修店当天维修的第五辆汽车的品牌和型号(因为品牌并且汽车的型号一旦离开工厂就永远不会改变)。商店也可以使用它来确定它是否再次看到同一辆车。它不能...
  • ...用于判断上次访问时第五辆车是红色还是蓝色。即使可以根据 VIN 找到第五辆汽车并看到它现在是红色的,即使工厂可以报告这辆车在制造时是红色的,也无法确定这辆车是否已经喷漆最后一次服务访问之前为蓝色,之后涂为红色。尽管如此,汽车的许多方面都是可变的这一事实并不意味着不可变的汽车列表不应被视为“真正不可变的”。
【解决方案2】:

结构是值类型,不一定是不可变的——所以答案是否定的。您可以设计不可变类型(将所有字段和属性设为只读)。但是,没有可用的类型约束(如where TKey : class)可以让您强制执行它。

更新:一个例子:

class Bar { public int I; }
struct Foo { public Bar B; }

var b = new Bar();
var f = new Foo { B = b; }

dict[key] = f;
b.I++;

我承认有点构建。

【讨论】:

  • 您可以添加一个 where TKey : ValueType 常量,然后在构造函数中通过反射查询它们为 ImmutableAttribute 的 TKey 类型。
  • 除非编译器强制执行,否则您不会真正获得那么多。我想你可以编写一个作为构建后步骤运行的工具,并检查标有不可变属性的类型是否真的不可变,但它很快就会变得棘手。将 TKey 限制为值类型不会改变这一点。
  • 如果有人用 ImmutableAttribute 标记了一个类/结构就足够了,如果他们的 Immutable 对象实际上不是 Immutable,那是他们的错。作为字典的作者,您不能对它可能包含的所有类型负责。
  • ChrisWue,那么如果值类型是可变的呢? 在不可变字典中如何变异?一个值类型只能通过改变包含它的 variable 来改变,如果不可变字典没有向它的调用者公开它的任何内部变量,那么他们就没有能力改变值类型。
  • Mh,但是你可以打破值类型的不变性。诚然,只有在您关心深度不​​变性的情况下才会这样做。 `
【解决方案3】:

看看新的 BCL 不可变集合: http://blogs.msdn.com/b/bclteam/archive/2012/12/18/preview-of-immutable-collections-released-on-nuget.aspx

这仍然是一个预览版本,但它包含以下类型:

  • ImmutableStack<T>
  • ImmutableQueue<T>
  • ImmutableList<T>
  • ImmutableHashSet<T>
  • ImmutableSortedSet<T>
  • ImmutableDictionary<K, V>
  • ImmutableSortedDictionary<K, V>

我希望下一个版本的 C# 将包含用于在类级别指定不变性约束的关键字(类似于建议的 here),以及更容易克隆对象的方法,例如带有关键字 @987654331 的 what can be done in F# @:

var o1 = new CustomObject { Field1 = 0, Field2 = 3 }
var o2 = o1 with { Field1 = 1} 

【讨论】:

    【解决方案4】:

    这是一个非常快速的设计,可能不会直接编译,但希望能给你一些想法......

    [Immutable]
    class ImmutableDictionary<TKey, TValue> : Dictionary<TKey, TValue>
    {
        public ImmutableDictionary(IEnumerable<KeyValuePair<TKey, TValue>> keysValues)
        {
            // Ensure TKey is immutable...
            if (typeof(TKey).GetCustomAttribute(typeof(ImmutableAttribute), false).Length == 0)
                throw new InvalidOperationException(String.Format("Type '{0}' must be immutable.", typeof(TKey).AssemblyQualifiedName);
    
            // Ensure TValue is immutable...
            if (typeof(TValue).GetCustomAttribute(typeof(ImmutableAttribute), false).Length == 0)
                throw new InvalidOperationException(String.Format("Type '{0}' must be immutable.", typeof(TValue).AssemblyQualifiedName);
    
            foreach(var keyValue in keysValues)
                base.Add(keyValue.Key, keyValue.Value);
        }
    
        public new void Add(TKey key, TValue value)
        {
            throw new InvalidOperationException("Cannot modify contents of immutable dictionary.");
        }
    
        public new void Clear()
        {
            throw new InvalidOperationException("Cannot modify contents of immutable dictionary.");
        }
    
        public new void Remove(TKey key)
        {
            throw new InvalidOperationException("Cannot modify contents of immutable dictionary.");
        }
    
        public TValue this[TKey key]
        {
            get { return base[key]; }
            set
            {
                throw new InvalidOperationException("Cannot modify contents of immutable dictionary.");
            }
        }
    }
    

    【讨论】:

      【解决方案5】:

      要了解发生了什么,请下拉到比字典更简单的类型:List&lt;T&gt;。如果创建一个List&lt;T&gt;,在其中抛出一些项目,然后创建将其传递给ReadOnlyCollection&lt;T&gt; 的构造函数并销毁除ReadOnlyCollection&lt;T&gt; 中包含的引用之外的对该列表的所有引用,那么该列表将是不可变的。它的状态永远不会改变,无论T是可变的还是不可变的,类还是结构。如果T 是一个可变类类型失败,则任何将此类列表视为可变的人都会错误地将列表视为包含对象。没有。

      如果T 是类类型,则List&lt;T&gt;拥有对象--它识别它们。如果@ 987654329@ 如上所述是不可变的,在向其添加了五个int 数组引用之后,只要它存在,它将始终保存对这五个数组的引用。数组的内容可能会改变,但List&lt;T&gt; 引用的对象的属性不构成列表状态的一部分

      Dictionary&lt;TKey,TValue&gt; 的状态比List&lt;T&gt; 的状态更难定义,特别是如果考虑到由不稳定的GetHashCode() 实现引起的混乱状态的可能性,但同样的原则也适用。如果TKey 是类类型,则构成字典状态一部分的TKey 实例的唯一方面是GetHashCode 返回的值,以及其Equals 方法所隐含的等价类;这两件事都应该是不可变的。 TKeyTValue 类对象的合法可变方面不应被视为 TDictionary&lt;TKey,TValue&gt; 状态的一部分。

      【讨论】:

        【解决方案6】:

        看看这个: http://ayende.com/blog/164739/immutable-collections-performance 顺便说一句,使用 ImmutableDictionary 时要小心

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2013-05-12
          • 1970-01-01
          • 1970-01-01
          • 2012-04-24
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多