我在我正在阅读的 C# 教科书中遇到了这些,但我很难理解它们,可能是由于缺乏上下文。
对它们是什么以及它们有什么用处有一个很好的简明解释吗?
编辑澄清:
协变接口:
interface IBibble<out T>
.
.
逆变接口:
interface IBibble<in T>
.
.
我在我正在阅读的 C# 教科书中遇到了这些,但我很难理解它们,可能是由于缺乏上下文。
对它们是什么以及它们有什么用处有一个很好的简明解释吗?
编辑澄清:
协变接口:
interface IBibble<out T>
.
.
逆变接口:
interface IBibble<in T>
.
.
使用<out T>
,您可以将接口引用视为层次结构中的一个向上。
使用<in T>
,您可以将接口引用视为层次结构中的一个向下。
让我试着用更多的英语来解释它。
假设您正在从动物园中检索动物列表,并且您打算处理它们。所有动物(在您的动物园中)都有一个名称和一个唯一的 ID。有些动物是哺乳动物,有些是爬行动物,有些是两栖动物,有些是鱼,等等,但它们都是动物。
因此,通过您的动物列表(其中包含不同类型的动物),您可以说所有动物都有一个名称,因此显然获得所有动物的名称是安全的。
然而,如果你只有一个鱼的清单,但需要像对待动物一样对待它们,那行得通吗?直观地说,它应该可以工作,但是在 C# 3.0 及之前的版本中,这段代码将无法编译:
IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>
这样做的原因是编译器不“知道”你想要或可以在你检索到动物集合后对它做什么。据它所知,可能有一种方法IEnumerable<T>
可以将对象放回列表中,这可能允许您将不是鱼的动物放入应该只包含鱼的集合中。
换句话说,编译器不能保证这是不允许的:
animals.Add(new Mammal("Zebra"));
所以编译器直接拒绝编译你的代码。这是协方差。
让我们看看逆变换。
既然我们的动物园可以处理所有的动物,它当然可以处理鱼,所以让我们尝试在我们的动物园中添加一些鱼。
在 C# 3.0 及之前版本中,这不会编译:
List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
fishes.Add(new Fish("Guppy"));
在这里,编译器可以允许这段代码,即使该方法返回List<Animal>
只是因为所有的鱼都是动物,所以如果我们只是将类型更改为:
List<Animal> fishes = GetAccessToFishes();
fishes.Add(new Fish("Guppy"));
然后它会起作用,但编译器无法确定您没有尝试这样做:
List<Fish> fishes = GetAccessToFishes(); // for some reason, returns List<Animal>
Fish firstFist = fishes[0];
由于该列表实际上是动物列表,因此这是不允许的。
因此,逆方差和协方差是您处理对象引用的方式以及您可以对它们做什么。
C# 4.0 中的in
andout
关键字专门将接口标记为一个或另一个。使用in
,您可以将泛型类型(通常是 T)放在输入位置,这意味着方法参数和只写属性。
使用out
,您可以将泛型类型放在输出位置,即方法返回值、只读属性和输出方法参数。
这将允许您对代码执行预期的操作:
IEnumerable<Animal> animals = GetFishes(); // returns IEnumerable<Fish>
// since we can only get animals *out* of the collection, every fish is an animal
// so this is safe
List<T>
在 T 上同时具有进出方向,因此它既不是协变也不是逆变,而是一个允许您添加对象的接口,如下所示:
interface IWriteOnlyList<in T>
{
void Add(T value);
}
将允许您这样做:
IWriteOnlyList<Fish> fishes = GetWriteAccessToAnimals(); // still returns
IWriteOnlyList<Animal>
fishes.Add(new Fish("Guppy")); <-- this is now safe
以下是一些展示这些概念的视频:
这是一个例子:
namespace SO2719954
{
class Base { }
class Descendant : Base { }
interface IBibbleOut<out T> { }
interface IBibbleIn<in T> { }
class Program
{
static void Main(string[] args)
{
// We can do this since every Descendant is also a Base
// and there is no chance we can put Base objects into
// the returned object, since T is "out"
// We can not, however, put Base objects into b, since all
// Base objects might not be Descendant.
IBibbleOut<Base> b = GetOutDescendant();
// We can do this since every Descendant is also a Base
// and we can now put Descendant objects into Base
// We can not, however, retrieve Descendant objects out
// of d, since all Base objects might not be Descendant
IBibbleIn<Descendant> d = GetInBase();
}
static IBibbleOut<Descendant> GetOutDescendant()
{
return null;
}
static IBibbleIn<Base> GetInBase()
{
return null;
}
}
}
没有这些标记,以下可以编译:
public List<Descendant> GetDescendants() ...
List<Base> bases = GetDescendants();
bases.Add(new Base()); <-- uh-oh, we try to add a Base to a Descendant
或这个:
public List<Base> GetBases() ...
List<Descendant> descendants = GetBases(); <-- uh-oh, we try to treat all Bases
as Descendants
这篇文章是我读过的关于这个主题的最好的
简而言之,协变/逆变/不变性处理自动类型转换(从基到派生,反之亦然)。仅当在对转换对象执行的读/写操作方面遵守某些保证时,这些类型转换才是可能的。阅读帖子了解更多详情。