Liskov 替换原则和接口

Liskov substitution principle and Interface

数组的ICollection<T>.Add()实现是否违反里氏代换原则?该方法导致 NotSupportedException,它确实破坏了 LSP,恕我直言。

string[] data = new string[] {"a"};
ICollection<string> dataCollection = data;
dataCollection.Add("b");

这导致

Unhandled exception: System.NotSupportedException: Collection was of a fixed size.

我发现了一个关于 Stream-实现的非常相似的问题。我打开一个单独的问题,因为这种情况非常不同:Liskov substitution principle and Streams。 这里的区别在于 ICollection 不提供 CanAdd-属性 或类似的东西,而 Stream-class 提供。

不,因为它不是 class - 接口和实现之间的关系 class 不同于 super 和 subclass 之间的关系。

LSP 特别适用于暗示实现的代码行为 - 接口没有实现,因此 LSP 不适用。

然而,这违反了接口隔离原则,该原则指出您应该编写接口以避免未实现的方法。

我明白你为什么这么想了。有一个函数需要一个集合,并且它希望它是可修改的。传递一个数组会使它失败,所以很明显你不能用这个特定的实现来代替接口,对吧?

有问题吗?可能是。这取决于您期望理想能够实现的频率。您是否会不小心使用数组而不是集合,然后在十年后对它崩溃感到惊讶?并不真地。 .NET 应用程序使用的类型系统并不完美 - 它没有告诉您这种特殊的 ICollection<T> 用法要求集合是可修改的。

如果数组不假装实现 ICollection<T>(或 IEnumerable<T>,它们也不 "really" 实现),.NET 会更好吗?我不这么认为。有没有办法保持数组 "being" ICollection<T> 的便利性,同时避免同样的 LSP 违规?没有。底层数组仍将是固定长度的 - 充其量,您会违反更多有用的原则(例如引用类型不应具有引用透明性这一事实)。

但是等等!让我们看看ICollection<T>.Add的实际合约。它允许抛出 NotSupportedException 吗?哦是的 - 引用 MSDN:

[NotSupportedException is thrown if ...] The ICollection is read-only.

当您查询 IsReadOnly 时,数组 return 为真。合同成立。

如果您认为 Stream 不会因为 CanWrite 而破坏 LSP,您 必须 将数组视为有效集合,因为它们有 IsReadOnly,结果是 true。如果一个函数接受一个只读集合并尝试向它添加内容,则它是函数中的一个错误。没有办法在 C#/.NET 中明确指定这一点,因此您必须依赖合同的其他部分而不仅仅是类型 - 例如该函数的文档应指定为只读集合抛出 NotSupportedException(或 ArgumentException 或其他)。一个好的实现会在函数的开头进行此测试。

需要注意的一件重要事情是,类型在 C# 中不像在定义 LSP 的类型理论中那样受到限制。例如,您可以在 C# 中编写如下函数:

bool IsFrob(object bobicator)
{
  return ((Bob)bobicator).IsFrob;
}

bobicator 可以替换为 object 的任何超类型吗?显然不是。但这显然不是 Frobinate 类型的问题——这是 IsFrob 函数中的一个错误。实际上,C#(和大多数其他语言)中的许多代码仅适用于比方法签名中的类型所指示的对象更受约束的对象。

一个对象只有在违反其超类型的约定时才会违反 LSP。它不能对 other code violationg LSP 负责。通常,您会发现编写不能完美 支持 LSP 的代码是非常务实的——工程是,而且一直是,关于权衡。仔细权衡成本。