我对 System.Collection.Generic.IReadOnlyCollection<T>
语义的理解和如何使用只读和不可变概念进行设计感到怀疑。让我使用 documentation 描述我怀疑的两种性质:
表示一个强类型的、只读的元素集合。
根据我在脑海中或者大声朗读时强调“表示”或“只读”的方式,我觉得句子的含义会发生变化:
- 当我强调“只读”时,文档中定义了我的观察性不可变性(Eric Lippert's article中使用的一个术语),这意味着接口的实现可以随心所欲地进行,只要没有公开的突变就行。
- 当我强调“代表”时,文档定义了一个不可变外观(再次在Eric Lippert的文章中描述),这是一个较弱的形式,其中突变可能是可能的,但用户无法进行。例如,类型为
IReadOnlyCollection<T>
的属性清楚地告诉用户(即针对声明类型编码的人)他不能修改此集合。但是,它在声明类型本身是否可以修改集合方面存在歧义。 - 为了完整起见:该接口除了成员签名所承载的语义之外,不承载任何语义。在这种情况下,观察性或外观不可变性取决于实现(不仅仅是接口实现,而是实例化相关的实现)。
第一种选择实际上是我更喜欢的解释,尽管这个约定很容易被打破,例如通过从T
数组构造一个ReadOnlyCollection<T>
,然后将一个值设置到包装器数组中。
BCL具有出色的外观不可变性接口,例如IReadOnlyCollection<T>
、IReadOnlyList<T>
甚至IEnumerable<T>
等。然而,我发现观察不可变性也很有用,据我所知,BCL中没有任何承载此含义的接口(如果我错了,请指出来)。这些不存在是有道理的,因为这种形式的不可变性不能通过接口声明强制执行,只能由实现者执行(尽管接口可以携带语义,如下所示)。另外:我希望在未来的C#版本中拥有这种能力!
示例:(可跳过)我经常需要实现一个方法,该方法的参数是一个集合,另一个线程也在使用该集合,但该方法要求在其执行期间不修改集合,因此我将参数声明为IReadOnlyCollection<T>
类型,并为自己鼓掌,认为我已经满足了要求。错了...对于调用者来说,这个签名看起来就像是该方法承诺不改变集合,仅此而已,如果调用者采用文档(外观)的第二种解释,他可能会认为允许突变,并且所讨论的方法对此具有抵抗力。虽然对于这个示例还有其他更传统的解决方案,但我希望您能看到这个问题可能是一个实际问题,特别是当其他人使用您的代码(或未来的您)时。
现在来谈谈我的实际问题(这引发了我对现有接口语义的怀疑):
我想使用观察性不可变性和外观不可变性,并区分它们。我想到的两个选项是:
- 使用BCL接口,并在每次文档中注明其是否为观察性或仅为外观不变性。缺点:使用此类代码的用户只有在发现错误时才会查阅文档,这时已经太晚了。我想引导他们走向成功之路;文档无法做到这一点。此外,我认为这种语义足够重要,应该在类型系统中可见,而不仅仅是在文档中。
- 定义显式携带观察性不变性语义的接口,例如
IImmutableCollection<T> : IReadOnlyCollection<T> { }
和IImmutableList<T> : IReadOnlyList<T> { }
。请注意,这些接口除了继承的成员之外没有任何成员。这些接口的目的是仅仅表达“即使声明类型也不会改变我!”‡ 我特别在这里说“不会”而不是“不能”。这里存在一个缺点:邪恶(或错误的)实现者并没有被编译器或其他任何东西阻止打破这个契约。然而,优点是选择实现此接口而不是直接继承它的程序员很可能知道此接口发送的额外消息,因为程序员知道此接口的存在,因此很可能相应地实现它。
我正在考虑采用第二个选项,但担心它存在与委托类型相似的设计问题(委托类型是为了在其无语义对应物Func
和Action
之间传递语义信息而发明的),但某种程度上失败了,例如请参见这里。
我想知道您是否也遇到/讨论过这个问题,或者我是否只是太过于纠结于语义,应该接受现有的接口,或者我是否不知道BCL中已经存在的解决方案。任何像上面提到的那样的设计问题都将是有帮助的。但我特别感兴趣的是您可能(已经)提出的其他解决方案,以区分声明和使用中的观察和外观不变性。
提前感谢您。
†我忽略了集合元素字段等的变异。
‡这适用于我之前给出的例子,但这个语句实际上更为广泛。例如,任何声明方法都不会改变它,或者这样一种类型的参数表明该方法可以期望在执行期间集合不会发生变化(这与说该方法不能更改集合是不同的,在现有接口中只能做出这种语句),可能还有许多其他情况。