不可变性/只读语义(特别是C#中的IReadOnlyCollection<T>)

12

我对 System.Collection.Generic.IReadOnlyCollection<T> 语义的理解和如何使用只读和不可变概念进行设计感到怀疑。让我使用 documentation 描述我怀疑的两种性质:

表示一个强类型的、只读的元素集合。

根据我在脑海中或者大声朗读时强调“表示”或“只读”的方式,我觉得句子的含义会发生变化:

  1. 当我强调“只读”时,文档中定义了我的观察性不可变性(Eric Lippert's article中使用的一个术语),这意味着接口的实现可以随心所欲地进行,只要没有公开的突变就行。
  2. 当我强调“代表”时,文档定义了一个不可变外观(再次在Eric Lippert的文章中描述),这是一个较弱的形式,其中突变可能是可能的,但用户无法进行。例如,类型为IReadOnlyCollection<T>的属性清楚地告诉用户(即针对声明类型编码的人)他不能修改此集合。但是,它在声明类型本身是否可以修改集合方面存在歧义。
  3. 为了完整起见:该接口除了成员签名所承载的语义之外,不承载任何语义。在这种情况下,观察性或外观不可变性取决于实现(不仅仅是接口实现,而是实例化相关的实现)。

第一种选择实际上是我更喜欢的解释,尽管这个约定很容易被打破,例如通过从T数组构造一个ReadOnlyCollection<T>,然后将一个值设置到包装器数组中。

BCL具有出色的外观不可变性接口,例如IReadOnlyCollection<T>IReadOnlyList<T>甚至IEnumerable<T>等。然而,我发现观察不可变性也很有用,据我所知,BCL中没有任何承载此含义的接口(如果我错了,请指出来)。这些不存在是有道理的,因为这种形式的不可变性不能通过接口声明强制执行,只能由实现者执行(尽管接口可以携带语义,如下所示)。另外:我希望在未来的C#版本中拥有这种能力!


示例:(可跳过)我经常需要实现一个方法,该方法的参数是一个集合,另一个线程也在使用该集合,但该方法要求在其执行期间不修改集合,因此我将参数声明为IReadOnlyCollection<T>类型,并为自己鼓掌,认为我已经满足了要求。错了...对于调用者来说,这个签名看起来就像是该方法承诺不改变集合,仅此而已,如果调用者采用文档(外观)的第二种解释,他可能会认为允许突变,并且所讨论的方法对此具有抵抗力。虽然对于这个示例还有其他更传统的解决方案,但我希望您能看到这个问题可能是一个实际问题,特别是当其他人使用您的代码(或未来的您)时。


现在来谈谈我的实际问题(这引发了我对现有接口语义的怀疑):

我想使用观察性不可变性和外观不可变性,并区分它们。我想到的两个选项是:

  1. 使用BCL接口,并在每次文档中注明其是否为观察性或仅为外观不变性。缺点:使用此类代码的用户只有在发现错误时才会查阅文档,这时已经太晚了。我想引导他们走向成功之路;文档无法做到这一点。此外,我认为这种语义足够重要,应该在类型系统中可见,而不仅仅是在文档中。
  2. 定义显式携带观察性不变性语义的接口,例如 IImmutableCollection<T> : IReadOnlyCollection<T> { }IImmutableList<T> : IReadOnlyList<T> { }。请注意,这些接口除了继承的成员之外没有任何成员。这些接口的目的是仅仅表达“即使声明类型也不会改变我!”‡ 我特别在这里说“不会”而不是“不能”。这里存在一个缺点:邪恶(或错误的)实现者并没有被编译器或其他任何东西阻止打破这个契约。然而,优点是选择实现此接口而不是直接继承它的程序员很可能知道此接口发送的额外消息,因为程序员知道此接口的存在,因此很可能相应地实现它。

我正在考虑采用第二个选项,但担心它存在与委托类型相似的设计问题(委托类型是为了在其无语义对应物FuncAction之间传递语义信息而发明的),但某种程度上失败了,例如请参见这里

我想知道您是否也遇到/讨论过这个问题,或者我是否只是太过于纠结于语义,应该接受现有的接口,或者我是否不知道BCL中已经存在的解决方案。任何像上面提到的那样的设计问题都将是有帮助的。但我特别感兴趣的是您可能(已经)提出的其他解决方案,以区分声明和使用中的观察和外观不变性。
提前感谢您。


†我忽略了集合元素字段等的变异。
‡这适用于我之前给出的例子,但这个语句实际上更为广泛。例如,任何声明方法都不会改变它,或者这样一种类型的参数表明该方法可以期望在执行期间集合不会发生变化(这与说该方法不能更改集合是不同的,在现有接口中只能做出这种语句),可能还有许多其他情况。


1
也许这个链接会有帮助:http://blogs.msdn.com/b/bclteam/archive/2012/12/18/preview-of-immutable-collections-released-on-nuget.aspx - Chris Sinclair
6
我支持在观察上不可变的集合类名称中使用“Immutable”一词,以及在可变集合的不可变门面类名称中使用“Readonly”一词。我还喜欢将非集合类的名称中加入“Immutable”,以使其成为“深度不可变”的类,即所有字段均为私有只读值类型,或者是私有只读引用类型,而这些引用类型本身也是不可变的。(其中,“readonly”是C#关键字)(这似乎也是微软采用的惯例,所以太好了。) - Matthew Watson
我不是很确定你在问什么,但是IImmutablexxx<T>的意思是它不会改变(它可以改变,但改变会生成一个新的集合),而IReadOnlyxxx<T>只是防止接收者更改集合,提供者可以更改它。 - Pedro.The.Kid
1个回答

1

接口无法确保不可变性。名称中的一个单词并不能防止可变性,那只是另一个提示,就像文档一样。

如果你想要一个不可变的对象,请要求一个不可变的具体类型。在C#中,不可变性取决于实现,而不是在接口中可见。

正如Chris所说,您可以找到不可变集合的现有实现


除了一些“标记”接口之外,任何接口都只能保证实现它的类将遵守接口的契约或被视为“损坏”的实现。如果ImmutableFloatMatrix的契约规定每次调用GetValue(X,Y)必须始终为给定的(X,Y)对返回相同的结果,则消费者不仅有权假设ImmutableFloatMatrix的实现是不可变的,而且也有权假设传递“Fred”List<String>.Add不会附加“Barney”。 - supercat

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