使用C#和接口的协变性

5

我不太清楚C#中关于接口协变的概念。基于下面的例子,这是一个协变的例子吗?请解释为什么或者为什么不是。

class Program
{
    static void Main()
    {
        ICarInterface car = new Car();
    }
}

interface ICarInterface
{
    void Start();
}

class Car : ICarInterface
{
    public void Start()
    {

    }
}

不,这与泛型有关。SO上已经有很多相关的问题了。 - nawfal
但是我不明白,为什么它严格地与泛型有关呢?在我的示例中,Car 继承自 ICarInterface。所以以下不就是一个隐式上转换,这意味着它是一个协变的例子吗?我错过了什么吗? ICarInterface car = new Car(); - Exocomp
这不是重复内容。Exocomp正在询问他展示的特定代码是否是协变的示例(以及为什么/为什么不是),而不是要求列出“协变的现实世界示例”的大列表。它可能是解释什么是协变的问题的重复,但并非被选中的那个问题。重新开放... - Cody Gray
3个回答

3
协变性与子类型和泛型的相互作用有关。由于您的程序不涉及泛型,因此它不是协变性的示例。
如果U是V的一个子类型(例如U = Pear且V = Fruit),则泛型类型G称为在T中具有协变性,如果G是G的一个子类型。例如,IEnumerable是IEnumerable的一个子类型:可以从中取出梨子的东西也可以用来取出水果。
如果G扭转了子类型关系(G是G的子类型),则称其在T中具有逆变性。例如,Action是Action的一个子类型:可以放水果的东西也可以用来放梨子。
在您的示例中,既没有ICarInterface也没有Car具有可在其中具有协变性的类型参数。
特别地,在C#中,如果T标记为out,则泛型类型在类型参数T上具有协变性。如果T标记为in,则在T上具有对偶性。

"协变性与子类型和泛型的相互作用有关。在C# 1中,数组协变性怎么样?当时还没有泛型,所以你的说法并不完全正确。总之,您的回答是说为了具有协变性,需要具有泛型集合/列表吗?因此,因为我的示例没有泛型集合/列表,所以它没有协变性?" - Exocomp
1
C# 当时还没有泛型,所以数组的特定情况被硬编码了。本质上,数组 一种通用类型:您可以将 T[] 看作是 Array<T>。(实际的 Array 类不是泛型的;这只是历史遗留问题,就像非泛型的 IListArrayList 一样。)在这里,数组协变意味着 Pear[]Fruit[] 的子类型。这类似于 IEnumerable<T> 的情况。(请注意,数组协变是有风险的,因为它意味着您可以将 Orange 放入实际上是 Pear[]Fruit[] 中。这可通过运行时检查来避免。) - Ruud
此外,协变性和逆变性不仅限于泛型集合。例如,Func<T, TResult>TResult 上是协变的,在 T 上是逆变的。协变性和逆变性是类型参数的属性。由于您的示例中没有类型参数,因此没有任何东西可以是协变的。 - Ruud

1
不,这并不是真正的协变性。这只是接口实现的例子。具体来说,这是一个“赋值兼容性”的示例。因为“Car”类实现了“ICarInterface”接口,所以“Car”对象可以分配给类型为“ICarInterface”的变量。更具体类型(“Car”)的对象被分配到了更不具体类型(“ICarInterface”)的存储区域中,这是有效的,因为两种类型在分配目的上是兼容的。
协变性是一种略微不同的东西。如果保留类型的顺序(从更具体到更通用),则类型关系是协变的。例如,“IEnumerable”相对于类型“T”是协变的,因此它保留类型“IEnumerable”(更通用)和“IEnumerable”(更具体)的顺序。(在这个例子中,我们当然假设“Car”是“Vehicle”的子类)。

Eric Lippert写了一篇很好的文章,区分了协变和赋值兼容性。它有点技术性和理论性,但你绝对应该阅读它。我在这里试图进一步概括它并不足以体现它的价值。


更易于理解的协变示例(至少在我看来)是返回类型协变。这是派生类重写基类方法并返回更具体类型的情况。例如:

abstract class Habitat
{
    public abstract Animal ApexPredator();
}

class Savanna : Habitat
{
    public override Lion ApexPredator()
    { ... }
}

class Ocean : Habitat
{
    public override Shark ApexPredator()
    { ... }
}

在这个例子中,抽象类Habitat有两个具体的子类:SavannaOcean。所有栖息地都有一个ApexPredator,它的类型是Animal。但是在Savanna对象中,顶级掠食者是Lion,而在Ocean对象中,顶级掠食者是Shark。这是合法且安全的,因为LionShark都是Animal类型。
不幸的是,C#不支持返回类型协变。然而,它被C++(包括C++/CLI)、Java和许多其他面向对象的语言支持。
以下是一些协变的具体例子

嗨,Cody,你关于C#不支持返回类型协变的例子让我思考了一会儿,我明白你特别指的是那个例子。我想你可以使用泛型来解决这个问题,使用一个返回类型的占位符,但是不要太偏离主题了。我的问题实际上是为什么我的例子不是协变,你说那是“赋值兼容性”的一个例子,但这并不能解释为什么它不是协变?难道“赋值兼容性”就是协变,但并不是所有的协变都是“赋值兼容性”吗? - Exocomp
@exo 嗯,你不会真的想使用泛型来替代返回类型协变。泛型意味着类型之间没有关系,而关系正是返回类型协变的重点。在C#中模拟返回类型协变的标准方法是方法隐藏。将覆盖方法设置为“protected”,然后添加一个隐藏(“new”)覆盖方法的“public”方法。如果您对此有更多问题,请提出新问题。 - Cody Gray
@exo 至于你的其他问题... "我的问题实际上是为什么我的例子不是协变性,你说那是“赋值兼容性”的例子,但这并没有解释为什么它不是协变性?" 你有阅读链接的博客文章吗?它非常好地解释了两者之间的区别。你不理解哪一部分? "“赋值兼容性”可能是协变性,但不是所有协变性都是“赋值兼容性”吗?" 不,它们是不同的概念。你可以说在协变关系中保留了赋值兼容性。 - Cody Gray
你给了我一些值得思考的事情,让我先回顾一下,稍后会跟进。 - Exocomp
1
@Exocomp:参数多态协变与赋值兼容性有关,因为赋值兼容性是在通用类型映射中保留的关系。但它们并不完全相同。一个是规定何时赋值合法的规则,另一个是生成新的赋值合法规则的模式。制作更多裙子的模式和一条裙子是两回事。 - Eric Lippert

0

这不是协变性的示例,只是多态性的简单示例。在这里你可以找到协变性和多态性之间的区别。


我访问了你给的链接,它有使用.NET 4的示例,但老实说并没有回答为什么我的示例不是协变的示例。 - Exocomp

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接