协变的泛型参数

43

我正在尝试理解这个问题,但是在搜索中没有找到合适的结果。

在C# 4中,我可以这样做:

    public interface IFoo<out T>
    {

    }

这个有什么不同之处?

    public interface IFoo<T>
    {

    }

我知道的是out将泛型参数协变(??)。

有人能用示例解释一下<out T>的用法吗?还有为什么它只适用于接口和委托,而不适用于类?

如果这是重复的问题,请原谅并将其关闭。


7
阅读:http://blogs.msdn.com/b/csharpfaq/archive/2010/02/16/covariance-and-contravariance-faq.aspx这篇文章讨论了 C# 中的“协变性”和“逆变性”,并回答了有关这些主题的常见问题。协变性指的是可以在派生类型中使用基础类型的对象,而逆变性则意味着可以在基础类型中使用派生类型的对象。这些概念对于理解一些常见的编程场景非常重要,例如将一个委托作为参数传递给方法,或者实现泛型接口。如果你是 C# 开发人员,并且希望更好地理解这些主题,那么这篇文章值得一读。 - Daniel Hilgarth
2
以上评论的存档链接:https://web.archive.org/web/20140709063137/http://blogs.msdn.com/b/csharpfaq/archive/2010/02/16/covariance-and-contravariance-faq.aspx - Stephen Klancher
3个回答

53
有人能举个例子解释一下 out T 的用法吗?
当然可以。IEnumerable 是协变的。这意味着你可以这样做:
static void FeedAll(IEnumerable<Animal> animals) 
{
    foreach(Animal animal in animals) animal.Feed();
}

...

 IEnumerable<Giraffe> giraffes = GetABunchOfGiraffes();
 FeedAll(giraffes);

“协变”是指在泛型类型中保留了类型参数的赋值兼容关系。 `Giraffe` 可以赋值给 `Animal`,因此该关系在构建的类型中得到保留:`IEnumerable<Giraffe>` 可以赋值给 `IEnumerable<Animal>`。
“为什么这只适用于接口和委托而不适用于类?”
类的问题是它们往往有可变的字段。让我们举个例子。假设我们允许这样做:
class C<out T>
{
    private T t;

在继续之前,请认真思考这个问题:C<T>除了构造函数以外,是否还有其他方法可以将字段t设置为默认值之外的其他值?

由于需要类型安全,C<T>现在不能有任何以T作为参数的方法;只能返回T。那么是谁设置了t,他们从哪里获取了设置它的值呢?

如果类是不可变的,协变类类型确实只适用于该类。但是我们没有很好的方法来创建C#中的不可变类。

我希望我们可以拥有更好的支持不可变类和协变类的CLR类型系统。如果您对此功能感兴趣,请考虑阅读我的长篇系列文章,了解我们如何设计和实现该功能。从底部开始:

https://blogs.msdn.microsoft.com/ericlippert/tag/covariance-and-contravariance/


5
谢谢Eric!你给了我一个大致的想法,我会阅读你的博客文章。附言:当我看到你的回答时,我本来希望看到一些酒店房间和丢失的钥匙,但我也可以接受长颈鹿。 - TheOtherGuy
如果C<T>有多个字段的类型涉及到T,它可以有基于另一个字段中的信息设置t的方法。此外,即使字段只能在构造函数中设置,如果像Tuple<Cat, ToyotaPrius>这样的东西可以传递给期望Tuple<Animal, Car>的代码,那将非常有用。我认为协变类的一个更根本的问题是,从Foo<T>产生的每个不同类都有自己的一组静态变量。如果Foo<T>.Q()是返回静态字段的实例属性,并且可以有Foo<Animal> it = new Foo<Cat>();... - supercat
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Eric Lippert
1
@ErickBrown:是的,如果编译器和运行时能够可靠地知道“这个类中的所有内容都只设置一次,然后只读取”,那么我们可以推断出这种不可变类型可以安全地协变。逻辑是正确的;因此,剩下的问题就是设计一个类型系统来跟踪不可变性以及其他所有内容的工程问题!这才是真正需要做的工作。最近C# 9的“记录类型”原型是朝着这个方向迈出的一步,所以我期待了解更多相关信息。 - Eric Lippert
1
@BorisB:关于你的第二个问题:如果类型系统能够推理出对于字段的“内部”和“外部”修改,那么类型系统可以使某些现在非法的协变合法。但即使是诸如字段的“只读性”这样的事实,在进行类型分析时也不会被考虑。理论上,它们可以被考虑。 - Eric Lippert
显示剩余5条评论

8

如果我们谈论一般性的差异:

协变是关于从操作返回值到调用方的所有内容。

逆变则相反,它是关于由调用者传递值的内容:

据我所知,如果一个类型参数仅用于输出,您可以使用out。然而,如果该类型仅用于输入,则可以使用in。这很方便,因为编译器无法确定您是否记得哪种形式被称为协变,哪种形式被称为逆变。如果不显式声明它们,一旦已声明了类型,相关的转换类型将隐式可用。

类中没有任何协变(包括协变或逆变),因为即使您有一个仅对类型参数进行输入(或仅对其进行输出)的类,也不能指定in或out修饰符。只有接口和委托可以具有变量类型参数。首先CLR不允许这样做。从概念上来说,接口代表了从特定角度观察对象的方式,而类则更多地代表了实际实现类型。


7
这意味着如果您有以下内容:
class Parent { } 
class Child : Parent { }

那么,IFoo<Child>的实例也是IFoo<Parent>的实例。


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