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