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>)
”
答案 0 :(得分:2)
但你可以这样做:
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。
答案 1 :(得分:1)
其他人已经注意到你的界面有一个困难,那就是没有办法清楚地识别可以与他们自己班级的其他项目相互操作的类;这种困难在某种程度上源于此类违反Liskov替代原则的事实;如果一个类接受两个类型为baseQ的对象并希望有一个对象相互操作,则LSP将指示一个应该能够用derivedQ替换其中一个baseQ对象。这反过来意味着baseQ应该在derivedQ上运行,而derivedQ应该在baseQ上运行。更广泛地说,baseQ的任何衍生物都应该对baseQ的任何其他衍生物进行操作。因此,界面不是协变的,也不是逆变的,也不是不变的,而是非一般的。
如果一个人希望使用泛型的原因是允许一个人的界面在没有拳击的情况下对结构进行操作,那么在phoog的答案中给出的模式是一个很好的模式。人们通常不应该担心对类型参数施加反身约束,因为接口的目的不是用作约束而不是变量或参数类型,并且例程可以使用约束来强加必要条件(例如, VectorList<T,U> where T:IVector<T,U>
)。
顺便提一下,我应该提到用作约束的接口类型的行为与接口类型的变量和参数的行为非常不同。对于每个结构类型,还有另一种派生自ValueType的类型;后一种类型将展示引用语义而不是值语义。如果将值类型的变量或参数传递给例程或存储在需要类类型的变量中,则系统会将内容复制到从ValueType派生的新类对象。如果所讨论的结构是不可变的,则任何和所有这样的副本将始终保持与原始和彼此相同的内容,因此可以被视为通常在语义上等同于原始。但是,如果有问题的结构是可变的,那么这种复制操作可能会产生与预期不同的语义。虽然有时候让接口方法改变结构是有用的,但是这些接口必须非常谨慎地使用。
例如,考虑实现List<T>.Enumerator
的{{1}}的行为。将类型IEnumerator<T>
的一个变量复制到同一类型的另一个变量将获取列表位置的“快照”;在一个变量上调用MoveNext不会影响另一个变量。将此类变量复制到List<T>.Enumerator
,Object
类型或从IEnumerator<T>
派生的接口之一,也会进行shapshot,并且如上所述在原始变量或新变量上调用MoveNext会让对方不受影响。另一方面,将IEnumerator<T>
,Object
类型的一个变量或从IEnumerator<T>
派生的接口复制到另一个也是其中一种类型(相同或不同)的变量,将不会快照,但只是复制对先前创建的快照的引用。
有时,让变量的所有副本在语义上等效是有用的。还有一些时候,对它们进行语义分离是有用的。不幸的是,如果一个人不小心,最终可能会出现一种奇怪的语义错误,只能被描述为“语义混乱”。