【问题标题】:Struct/Class to Interface substitution contract broken?结构/类到接口替换合同损坏?
【发布时间】:2012-01-06 14:38:06
【问题描述】:
public interface IVector<TScalar> {
    void Add(ref IVector<TScalar> addend);
}

public struct Vector3f : IVector<float> {
    public void Add(ref Vector3f addend);
}

编译器回答:

Vector3f没有实现接口成员IVector&lt;float&gt;.Add(ref IVector&lt;float&gt;)

【问题讨论】:

  • 合约是它应该接受 any IVector&lt;float&gt;,而不仅仅是你给定的实现。 C# 不支持将参数变化作为覆盖方法或实现接口的一种方式。
  • 是的,我自己也意识到了,无论如何,TY。

标签: c# class interface substitution contract


【解决方案1】:

但你可以这样做:

public interface IVector<T, TScalar>
    where T : IVector<T, TScalar>
{
    void Add(ref T addend);
}

public struct Vector3f : IVector<Vector3f, float>
{
    public void Add(ref Vector3f addend)
    {
    }
} 

然而,这意味着你有可变结构,这是你不应该的。要拥有不可变的接口,您需要重新定义接口:

public interface IVector<T, TScalar>
    where T : IVector<T, TScalar>
{
    T Add(T addend);
}

public struct Vector3f : IVector<Vector3f, float>
{
    public Vector3f Add(Vector3f addend)
    {
    }
} 

编辑:

正如 Anthony Pegram 所指出的,这种模式存在漏洞。尽管如此,它被广泛使用。例如:

struct Int32 : IComparable<Int32> ...

有关更多信息,这里是 Eric Lippert 关于此模式的文章Curiouser and curiouser 的链接。

【讨论】:

  • 但是,鉴于此,您还可以定义EvilVector3f : IVector&lt;Vector3f, float&gt;,因为仍然没有*方法可以清楚地表达接口或抽象方法以一种只能工作的方式实现的想法与实现它的类的类型。 (*据我所知,我承认我是个白痴。)
  • 同意,显然这是错误的方法,不是我想要的,因为它没有将接口与实现分开。
  • @AnthonyPegram 好点。这在 C++ 中被称为“奇怪地重复出现的模板模式”,在 C# 中或许应该被称为“奇怪地重复出现的通用模式”,但谷歌只给出了 1390 次该短语的点击率。我添加了指向 Eric Lippert 关于该主题的文章的链接。
  • @Haroogan 以何种方式无法将接口与实现分开?
  • @supercat,你被结构问题所吸引,就像飞蛾扑火一样! 1)我已经说过我只是普通的白痴,我既没有经验也没有足够的创造力来想出险恶的场景,但更重要的是,2)我的想法是类型系统根本不可能习惯于以这种方式限制实现。以您想要的方式使用它成为信任的问题。按照这些思路,请注意您正在对如何使用实现做出假设,只要您是定义合同和实现的人,这是安全的。
【解决方案2】:

其他人注意到您的界面存在一个问题,即没有任何方法可以清晰地识别可以与自己类的其他项相互操作的类;这种困难在一定程度上源于这样的类违反了里氏替换原则。如果一个类接受两个 baseQ 类型的对象并期望一个对象相互操作,那么 LSP 将规定一个类应该能够用派生Q 替换其中一个 baseQ 对象。这反过来意味着 baseQ 应该在 derivedQ 上运行,并且 derivedQ 应该在 baseQ 上运行。更广泛地说,baseQ 的任何导数都应该对 baseQ 的任何其他导数进行操作。因此接口不是协变的,也不是逆变的,也不是不变的,而是非泛型的。

如果一个人希望使用泛型的原因是允许一个人的接口在没有装箱的情况下作用于结构,那么 phoog 的答案中给出的模式是一个很好的模式。通常不应该担心对类型参数施加自反约束,因为接口的目的不是用作约束,而是用作变量或参数类型,并且可以通过例程使用约束(例如VectorList&lt;T,U&gt; where T:IVector&lt;T,U&gt;)。

顺便提一下,用作约束的接口类型的行为与接口类型的变量和参数的行为非常不同。对于每个结构类型,都有另一种派生自 ValueType 的类型;后一种类型将表现出引用语义而不是值语义。如果将值类型的变量或参数传递给例程或存储在需要类类型的变量中,系统会将内容复制到从 ValueType 派生的新类对象中。如果所讨论的结构是不可变的,则任何和所有此类副本将始终与原始内容和彼此保持相同的内容,因此可以认为在语义上通常与原始内容等效。但是,如果所讨论的结构是可变的,则此类复制操作可能会产生与预期非常不同的语义。虽然有时让接口方法改变结构会很有用,但必须非常小心地使用此类接口。

例如,考虑实现IEnumerator&lt;T&gt;List&lt;T&gt;.Enumerator 的行为。将List&lt;T&gt;.Enumerator 类型的一个变量复制到另一个相同类型的变量将获取列表位置的“快照”;在一个变量上调用 MoveNext 不会影响另一个。将这样的变量复制到ObjectIEnumerator&lt;T&gt; 类型之一或从IEnumerator&lt;T&gt; 派生的接口也将进行快照,并且如上所述在原始变量或新变量上调用 MoveNext 将使另一个不受影响。另一方面,将ObjectIEnumerator&lt;T&gt; 类型的一个变量或从IEnumerator&lt;T&gt; 派生的接口复制到也是这些类型之一(相同或不同)的另一个变量,不会拍摄快照,而只是复制对先前创建的快照的引用。

有时让变量的所有副本在语义上是等价的会很有用。在其他时候,将它们在语义上分离可能很有用。不幸的是,如果一个人不小心,最终可能会出现一种奇怪的语义混杂,只能被描述为“语义混乱”。

【讨论】:

    猜你喜欢
    • 2011-06-13
    • 2018-09-17
    • 1970-01-01
    • 2010-09-10
    • 1970-01-01
    • 2020-12-14
    • 2018-04-14
    • 2022-01-14
    相关资源
    最近更新 更多