public interface IVector<TScalar> {
void Add(ref IVector<TScalar> addend);
}
public struct Vector3f : IVector<float> {
public void Add(ref Vector3f addend);
}
编译器回答:
“Vector3f
不实现接口成员IVector<float>.Add(ref IVector<float>)
”
public interface IVector<TScalar> {
void Add(ref IVector<TScalar> addend);
}
public struct Vector3f : IVector<float> {
public void Add(ref Vector3f addend);
}
编译器回答:
“Vector3f
不实现接口成员IVector<float>.Add(ref IVector<float>)
”
但是你可以这样做:
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 wonderer about this pattern 的链接。
其他人注意到您的界面存在一个困难,即没有任何方法可以清晰地识别可以与自己类的其他项目相互操作的类;这种困难在一定程度上源于这样的类违反了里氏替换原则。如果一个类接受两个 baseQ 类型的对象并期望一个对象相互操作,那么 LSP 将规定一个类应该能够用派生Q 替换其中一个 baseQ 对象。这反过来意味着 baseQ 应该在 derivedQ 上运行,并且 derivedQ 应该在 baseQ 上运行。更广泛地说,baseQ 的任何导数都应该对 baseQ 的任何其他导数进行操作。因此,接口不是协变的,也不是逆变的,也不是不变的,而是非泛型的。
如果一个人希望使用泛型的原因是允许一个人的接口在没有装箱的情况下作用于结构,那么 phoog 的答案中给出的模式是一个很好的模式。通常不必担心对类型参数施加反身约束,因为接口的目的不是用作约束,而是用作变量或参数类型,并且可以通过例程使用约束来施加必要的条件(例如VectorList<T,U> where T:IVector<T,U>
)。
顺便提一下,用作约束的接口类型的行为与接口类型的变量和参数的行为非常不同。对于每个结构类型,都有另一种派生自 ValueType 的类型;后一种类型将表现出引用语义而不是值语义。如果将值类型的变量或参数传递给例程或存储在需要类类型的变量中,系统会将内容复制到从 ValueType 派生的新类对象中。如果所讨论的结构是不可变的,则任何和所有此类副本将始终与原始内容和彼此保持相同的内容,因此可以认为在语义上通常与原始内容等效。但是,如果所讨论的结构是可变的,这种复制操作可能会产生与预期非常不同的语义。虽然有时让接口方法改变结构可能很有用,但必须非常小心地使用此类接口。
例如,考虑实现 的List<T>.Enumerator
行为IEnumerator<T>
。将一个类型的变量复制List<T>.Enumerator
到另一个相同类型的变量将获取列表位置的“快照”;在一个变量上调用 MoveNext 不会影响另一个。将这样的变量复制到类型Object
,IEnumerator<T>
或派生自 的接口中的一个IEnumerator<T>
,也将进行快照,并且如上所述在原始变量或新变量上调用 MoveNext 将使另一个不受影响。另一方面,将一个 , 类型的变量或派生自另一个的接口复制Object
到IEnumerator<T>
另一个IEnumerator<T>
也是这些类型之一(相同或不同)的变量,不会拍摄快照,而只是复制对先前创建的快照的引用。
有时让变量的所有副本在语义上是等价的会很有用。在其他时候,在语义上分离它们可能很有用。不幸的是,如果一个人不小心,最终可能会出现一种奇怪的语义混杂,只能被描述为“语义混乱”。