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

Posted

技术标签:

【中文标题】不变性/只读语义(特别是 C# IReadOnlyCollection<T>)【英文标题】:Immutability/Read-only semantics (particular C# IReadOnlyCollection<T>) 【发布时间】:2013-07-24 12:19:34 【问题描述】:

我怀疑我对System.Collection.Generic.IReadOnlyCollection&lt;T&gt; 语义的理解,并怀疑如何使用只读和不可变等概念进行设计。让我通过使用 documentation 来描述我怀疑的两种性质,其中指出

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

取决于我是否强调“代表”或“只读”这两个词(在我脑海中发音时,或者如果那是你的风格,则大声朗读)我觉得这句话改变了含义:

    当我强调“只读”时,我认为文档定义了观察不变性(Eric Lippert's article 中使用的术语),这意味着只要没有可见的突变,接口的实现就可以随心所欲地进行 公开†。 当我强调“代表”时,文档定义(在我看来,再次)一个不可变的外观(在 Eric Lippert 的文章中再次描述),这是一种较弱的形式,其中可能发生突变,但不能由用户。例如,IReadOnlyCollection&lt;T&gt; 类型的属性向用户(即针对声明类型编码的人)明确表示他不能修改此集合。但是,声明类型本身是否可以修改集合是模棱两可的。 为了完整起见:接口不携带除其成员签名所携带的语义之外的任何语义。在这种情况下,观察或外观不变性是依赖于实现的(不仅依赖于接口的实现,还依赖于实例)。

第一个选项实际上是我的首选解释,虽然这个合同很容易被打破,例如通过从T 的数组构造ReadOnlyCollection&lt;T&gt;,然后在包装数组中设置一个值。

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


示例:(可以跳过)我经常必须实现一个方法,该方法将另一个线程也使用的集合作为参数,但该方法要求在运行期间不要修改集合它的执行,因此我将参数声明为IReadOnlyCollection&lt;T&gt; 类型并给自己拍拍背,认为我已经满足了要求。错误......对于调用者来说,签名看起来好像方法承诺不会更改集合,没有别的,如果调用者对文档(外观)进行第二种解释,他可能只是认为允许突变并且方法在问题是抗拒的。尽管此示例还有其他更常规的解决方案,但我希望您看到这个问题可能是一个实际问题,特别是当其他人使用您的代码时(或未来的您)。


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

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

    每次使用 BCL 接口并记录它是观察性的还是只是外观不变性。缺点:使用此类代码的用户只会在为时已晚(即发现错误时)查阅文档。我要带领他们进入成功的深渊;文档不能这样做)。此外,我发现这种语义非常重要,可以在类型系统中看到,而不仅仅是在文档中。 明确定义带有观察不变性语义的接口,例如IImmutableCollection&lt;T&gt; : IReadOnlyCollection&lt;T&gt; IImmutableList&lt;T&gt; : IReadOnlyList&lt;T&gt; 。请注意,除了继承的接口之外,接口没有任何成员。这些接口的目的只是说“即使声明类型也不会改变我!”‡我在这里特别说“不会”而不是“不能”。这里有一个缺点:一个邪恶的(或错误的,为了保持礼貌)实现者不会被编译器或其他任何东西阻止破坏这个合同。然而,优点是选择实现这个接口而不是它直接继承的接口的程序员很可能知道这个接口发送的额外消息,因为程序员知道这个接口的存在,从而可能会相应地实施。

我正在考虑使用第二个选项,但我担心它的设计问题与委托类型的设计问题相当(发明这些委托类型是为了在它们的无语义对应物 FuncAction 上携带语义信息)并且不知何故失败了,参见例如here。

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


† 我忽略了集合元素上字段等的突变。 ‡ 这对于我之前给出的示例是有效的,但该陈述实际上更广泛。例如,任何声明方法都不会改变它,或者这种类型的参数表明该方法可以期望集合在其执行期间不会改变(这与说该方法不能改变集合不同,这是唯一的一个可以用现有接口做的声明),可能还有很多其他的。

【问题讨论】:

也许这是相关的:blogs.msdn.com/b/bclteam/archive/2012/12/18/… 我赞成使用术语“不可变”作为观察不可变集合的类名的一部分,而将“只读”一词用作可变集合的不可变外观的类名的一部分。我还喜欢名称中带有“不可变”一词的非集合类是“深度不可变”的,即字段都是私有只读值类型,或者是本身不可变的私有只读引用类型。 (readonly 是 C# 关键字)(这似乎也是微软采用的约定,好开心。) 我并没有真正支持你的要求,但 IImmutablexxx 意味着它不会改变(它可以改变但改变会生成一个新的集合) IReadOnlyxxx 只会阻止收件人通过更改集合,提供者可以更改它。 【参考方案1】:

接口不能确保不变性。名称中的一个词不会阻止可变性,这只是另一个提示,如文档。

如果您想要一个不可变对象,则需要一个不可变的具体类型。在 c# 中,不变性依赖于实现,并且在接口中不可见。

正如克里斯所说,你可以找到existing implementations of immutable collections。

【讨论】:

除了一些“标记”接口之外,any 接口唯一可以保证的是,任何实现它的类要么遵守接口的约定,要么被视为“破碎”的实施。如果ImmutableFloatMatrix 的合同规定每次对GetValue(X,Y) 的调用必须始终为任何给定的(X,Y) 对返回相同的结果,那么消费者将同样有权假设ImmutableFloatMatrix 的实现是不可变的,而不是假设传递“Fred”List&lt;String&gt;.Add 不会附加“Barney”。

以上是关于不变性/只读语义(特别是 C# IReadOnlyCollection<T>)的主要内容,如果未能解决你的问题,请参考以下文章

并发编程设计模式--Immutability模式

c#中关于协变性和逆变性(又叫抗变)详解

C#中string类型的不可变性

进入快速通道的委托(深入理解c#)

C# 类和只读成员

编写高质量代码改善C#程序的157个建议——建议42:使用泛型参数兼容泛型接口的不可变性