【问题标题】:Liskov substitution principle and InterfaceLiskov替换原理和接口
【发布时间】:2017-10-18 09:02:12
【问题描述】:

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

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

这会导致

未处理的异常:System.NotSupportedException: Collection was of 固定大小。

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

【问题讨论】:

  • 类型不能单独违反 LSP,只有子类型不能用它们的超类型替代。 ICollection&lt;T&gt;.Add 很丑,但它确实记录了如果 IsReadOnly 为假,NotSupportedException 将被抛出。

标签: c# interface liskov-substitution-principle


【解决方案1】:

不,因为它不是类——接口和实现类之间的关系与父类和子类之间的关系不同。

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

然而,这违反了接口隔离原则,该原则说您应该组合接口以避免未实现的方法。

【讨论】:

  • LSP确实是在子类型的上下文中定义的;但是,我不太确定这是否是一个有效的论点。当子类型不能替换超类型时它应该是坏的,但是当接口的实现不能替换接口时可以吗?没错,这违反了 ISP,但在 C#/.NET 中无法解决该问题 - 您不能将现有接口拆分为多个接口。
  • @DarrenYoung - 你不会'按定义',因为ICollection&lt;T&gt;.Add 仅在IsReadOnly 返回false 而数组返回true 时才有效。
  • @Luaan:接口(方法契约)不定义行为,只有实现定义,实现只能由类提供。因此 LSP 不能适用,因为该原则专门适用于行为。
  • 是的,在严格的类型理论意义上。但是编程不仅仅是由类型系统定义的——接口在理论上没有行为,但它们的契约(例如文档)可能描述接口的有效和无效行为。接口契约不仅仅是方法的名称和类型。这就是为什么我没有对您的答案投反对票或投赞成票的原因-老实说,当问题显然与编码有关时,我真的无法决定是否值得考虑该理论,而所有这些事情都比理论中的清晰得多。
  • 相反,ICollection&lt;T&gt; 接口是来自 Microsoft 的设计糟糕的代码,因为它没有正确应用这些原则 - 之所以存在争论,只是因为这个特定的接口是框架的一部分。在其他任何地方,它都会被称为糟糕的实现并指出需要更改。我不反对实用主义,但如果您可以更改代码,您不会妥协的基本原则。
【解决方案2】:

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

有问题吗?可能是。这取决于您期望理想保持多久。您是否会意外地使用数组而不是集合,然后在十年后惊讶地发现它崩溃了?并不真地。 .NET 应用程序使用的类型系统并不完美 - 它没有告诉您这种特定的 ICollection&lt;T&gt; 用法要求集合是可修改的。

如果数组不假装实现ICollection&lt;T&gt;(或IEnumerable&lt;T&gt;,它们也没有“真正”实现),.NET 会更好吗?我不这么认为。有没有办法保持数组“存在”ICollection&lt;T&gt; 的便利性,这也可以避免同样的 LSP 违规?没有。底层数组仍然是固定长度的 - 充其量,您会违反更有用的原则(例如引用类型不应具有引用透明性这一事实)。

但是等等!我们来看看ICollection&lt;T&gt;.Add的实际合约。是否允许抛出NotSupportedException?哦,是的 - 引用 MSDN:

[NotSupportedException is throwed if ...] ICollection 是只读的。

当您查询 IsReadOnly 时,数组确实返回 true。合同有效。

如果你认为 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。它不对其他 LSP 代码违规负责。通常你会发现在 LSP 下编写不能完全支持的代码是非常务实的——工程是,而且一直都是,关于权衡。仔细权衡成本。

【讨论】:

    猜你喜欢
    • 2019-10-29
    • 2013-12-15
    • 1970-01-01
    • 2013-10-15
    • 1970-01-01
    • 1970-01-01
    • 2013-08-23
    • 2019-06-26
    • 1970-01-01
    相关资源
    最近更新 更多