泛型中的<out T>与<T>有什么区别?

265

<out T><T> 有什么区别?例如:

public interface IExample<out T>
{
    ...
}

对比。

public interface IExample<T>
{
    ...
}

2
一个很好的例子是在mscorlib中定义的system ns中的IObservable<T>和IObserver<T>。public interface IObservable<out T>,以及public interface IObserver<in T>。同样地,IEnumerator<out T>,IEnumerable<out T>。 - VivekDev
5
我遇到的最佳解释:https://agirlamonggeeks.com/2019/05/29/vs-in-generic-interfaces-contravariance-vs-covariance-the-easier-part-1/。(<in T> < - 意味着 T 只能作为参数传递给方法;<out T> < - 意味着 T 只能作为方法结果返回) - Uladzimir Sharyi
8个回答

273
在泛型中,out关键字用于表示接口中的类型T是协变的。有关详细信息,请参见协变和逆变性
经典示例是IEnumerable<out T>。由于IEnumerable<out T>是协变的,您可以执行以下操作:
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings;

上面的第二行如果不是协变的话会失败,尽管从逻辑上讲它应该是可以工作的,因为字符串是从对象派生出来的。在C#和VB.NET中(在.NET 4和VS 2010中)添加了泛型接口的变异性之前,这是一个编译时错误。
在.NET 4之后,IEnumerable<T>被标记为协变,并变成了IEnumerable<out T>。由于IEnumerable<out T>只使用其中的元素,而不会添加/更改它们,所以它可以将一个字符串的可枚举集合视为一个对象的可枚举集合,这意味着它是协变的。
这对于像IList<T>这样的类型是行不通的,因为IList<T>有一个Add方法。假设这是允许的:
IList<string> strings = new List<string>();
IList<object> objects = strings;  // NOTE: Fails at compile time

你可以随后打电话:
objects.Add(7); // This should work, since IList<object> should let us add **any** object

这当然会失败 - 所以 IList<T> 不能被标记为协变。
顺便说一下,还有一个 in 的选项 - 这个选项被用于比较接口之类的东西。例如,IComparer<in T> 的工作方式相反。如果 BarFoo 的子类,那么你可以直接将具体的 IComparer<Foo> 用作 IComparer<Bar>,因为 IComparer<in T> 接口是逆变的。

5
因为Image是一个抽象类,你可以使用new List<object>() { Image.FromFile("test.jpg") };或者new List<object>() { new Bitmap("test.jpg") };来创建一个对象列表,但是使用new Image()是不被允许的。需要注意的是,即使使用var img = new Image();也是不被允许的。 - Reed Copsey
4
一个通用的 IList<object> 是一个奇怪的例子,如果你只是想要获取 object 类型的元素,就不需要使用泛型。 - Jodrell
7
@ReedCopsey,在你的评论中,你是否与自己的答案相矛盾了? - MarioDS

82
为了更容易记住inout关键字(以及协变性和逆变性),我们可以将继承想象成包装:
String : Object
Bar : Foo

in/out


18
这是不是颠倒了?逆变(Contravariance)= 输入 = 允许使用派生程度更低的类型来代替派生程度更高的类型。/协变(Covariance)= 输出 = 允许使用派生程度更高的类型来代替派生程度更低的类型。 就个人看来,根据您所提供的图表,我认为它和上述描述相反。 - Sam Shiles
协变量 (: 对我来说 - Soner from The Ottoman Empire
我们可以同时使用它们吗? - Luka Samkharadze
@SamShiles 从技术上讲,派生类型(string)应该比其基类型(object)“更大”,因为它应该有更多的成员。因此,如果您只交换类型,则图像有效。 - Luke Vo

65

请考虑,

class Fruit {}

class Banana : Fruit {}

interface ICovariantSkinned<out T> {}

interface ISkinned<T> {}

以及这些功能,

void Peel(ISkinned<Fruit> skinned) { }

void Peel(ICovariantSkinned<Fruit> skinned) { }

接受ICovariantSkinned<Fruit>的函数将能够接受ICovariantSkinned<Fruit>ICovariantSkinned<Banana>,因为ICovariantSkinned<T>是一个协变接口,而BananaFruit的一种类型。

接受ISkinned<Fruit>的函数只能接受ISkinned<Fruit>


58
"out T" 表示类型 T 是“协变”的。这限制了 T 只能出现在泛型类、接口或方法的返回值(出站)中。这意味着您可以将类型/接口/方法转换为具有 T 超类型的等效项。
例如,ICovariant<out Dog> 可以被转换为 ICovariant<Animal>

29
直到我读了这篇回答,我才意识到 out 约束了只能返回类型为 T 的内容。现在整个概念更加清晰易懂了! - MarioDS

8

根据您发布的链接,对于泛型类型参数,out关键字指定类型参数是协变的

编辑: 同样来自您发布的链接

有关更多信息,请参阅协变性和逆变性 (C# 和 Visual Basic) 。 http://msdn.microsoft.com/en-us/library/ee207183.aspx


4

我认为这张来自VS2022的截图非常描述性 - 它说明了这对泛型施加了什么样的限制:

generics variance constraing


2
截图展示文本是一种糟糕的方式。 - DavidW
2
我不同意,因为它提供了真正的VS IDE反馈。请随意编辑我的帖子@DavidW。 - lissajous

2

我找到的最简单的解释在这篇文章中提到:

接口 ISomeName<in T> - 意味着T只能作为方法的参数传递(它进入接口的方法,所以它进入内部,也许这就是我们在这里使用“in”关键字的原因...嗯,不,那只是巧合吧?)

接口 ISomeName<out T> - 意味着T只能作为方法结果返回(它是我们从方法中接收到的,所以它从中出来了 - 哇,听起来又很正常!)


1
协变输出
//I need a fruit farm(base class) so i can produce fruits(base type)
//But what I have is an apple farm(derived class) that produce apples (derive type)
IProducer<Apple> appleFarm = new AppleProducer(); 
//thats ok, I'll just mark it as covariant(out) since apple is also a fruit
IProducer<Fruit> fruitFarm = appleFarm; 

Apple apple = appleFarm.Produce();
//I can produce fruits since apple is a fruit
Fruit fruit = apple; 

逆变在
//I need an apple bakery(derive class) so i can use my apples(derive type)
//But what I have is a fruit bakery(base class) that uses fruits(base type)
IConsumer<Fruit> fruitBakery = new FruitConsumer();
//thats ok, I'll just mark it as contravariant(in) since a fruit bakery can also consume apples
IConsumer<Apple> appleBakery = fruitBakery;

//can consume apples and any fruits
appleBakery.Consume(new Apple());
fruitBakery.Consume(new Fruit());
fruitBakery.Consume(new Apple());

所以基本上:
- 如果你将一个接口的泛型标记为协变,它必须能够输出泛型类型,并且允许你将泛型类型的派生类型转换为其基类型。
IProducer<Apple> appleFarm = new AppleProducer(); 
IProducer<Fruit> fruitFarm = appleFarm; 

Apple apple = appleFarm.Produce();
Fruit fruit = apple; 

如果你将一个接口的泛型标记为逆变,它必须能够输入泛型类型,并且允许你将泛型类型的派生类型转换为其基类型。
IConsumer<Fruit> fruitBakery = new FruitConsumer();
IConsumer<Apple> appleBakery = fruitBakery;

fruitBakery.Consume(new Apple());
fruitBakery.Consume(new Fruit());
appleBakery.Consume(new Apple());

要理解为什么,可以查阅Liskov替换原则。
完整代码:
public interface IProducer<out T>
{
    Produce();
}
 
public class Fruit { }
 
public class Apple : Fruit { }
 
public class FruitProducer : IProducer<Fruit>
{
    public Fruit Produce() => new Fruit();
}
 
public class AppleProducer : IProducer<Apple>
{
    public Apple Produce() => new Apple();
}
 
public interface IConsumer<in T>
{
    void Consume(T item);
}
 
public class FruitConsumer : IConsumer<Fruit>
{
    public void Consume(Fruit item)
    {
        // Consume the fruit
    }
}
 
public class AppleConsumer : IConsumer<Apple>
{
    public void Consume(Apple item)
    {
        // Consume the apple
    }
}

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