为什么泛型类型参数中的隐式子类型转换不可行?

6

我一直在学习C#中的协变性和逆变性,但有一个问题我不太理解:

class Program
{
    static void Main(string[] args)
    {
        PersonCollection<Person> personCollection = new PersonCollection<Person>();
        IMyCollection<Teacher> myCollection = personCollection; // Here's the error:
        // Cannot implicitly convert type 'PersonCollection<Teacher>' to 'IMyCollection<Person>'
    }
}
class Person { }
class Teacher : Person { }
interface IMyCollection<T> { }
class PersonCollection<T> : IMyCollection<T> { }

众所周知,我们可以将派生类的实例隐式转换为基类。因此,在上面的代码中,虽然'Teacher'类派生自'Person'类,但IMyCollection<Teacher>无法转换为IMyCollection<Person>,为什么?!
注意:我想知道原因,而不是解决方案。

2
继续阅读协变性和逆变性。这个问题就是这样的。 - Camilo Terevinto
这就是泛型中协变和逆变的含义。如果你在IMyCollection中声明T为协变,那么你可以将一个带有类型参数的类赋值给一个具有基类类型参数声明的类。 - Venson
1
请查看 https://learn.microsoft.com/en-us/dotnet/standard/generics/covariance-and-contravariance 中的列表。它非常清晰明了。 - Venson
@JPVenson 我知道这一点。我只是想知道为什么我们不能正常地做它的原因。 - aradalvand
因为如果您想要获取myCollection中的第一个元素,您期望它是一个Teacher,但实际上它也可能只是一个普通人。 - Caramiriel
你在问题中提到了三个不同的转换:一个在代码中,一个在错误信息中,还有一个在你的问题文本中。你应该真正决定你想要做什么... - poke
2个回答

7
注意:我想知道原因,而不是解决方案。
尽管这就是逆变和协变存在的原因,但让我快速给你展示一个以你的例子为例的解释,突显为什么这行不通:
所以让我们假设以下设置代码:
PersonCollection<Person> personCollection = new PersonCollection<Person>();

personCollection.Add(new Teacher("Teacher A"));
personCollection.Add(new Teacher("Teacher B"));
personCollection.Add(new Student("Student A"));
personCollection.Add(new Student("Student B"));
personCollection.Add(new Student("Student C"));
personCollection.Add(new Student("Student D"));

现在,我有一个包含两个Teacher和四个Student对象的PersonCollection<Person>(这里,Student也继承自Person)。这是完全有效的,因为任何TeacherStudent也都是Person。所以我可以将元素添加到集合中。
现在,想象一下如果允许以下情况:
IMyCollection<Teacher> myCollection = personCollection;

现在,我有一个名为myCollection的集合,显然包含了Teacher对象。但是由于这只是一个引用赋值,myCollection仍然是与personCollection完全相同的集合。
所以,myCollection将包含四个Student对象,尽管它的约定规定它只包含Teacher元素。根据接口的约定,以下操作应该是完全允许的:
Teacher teacher = personCollection[4];

但是personCollection[4]是学生C,所以显然这是行不通的。
由于编译器无法在此项赋值期间进行验证,并且我们希望类型安全而不是运行时验证,编译器唯一合理的方式是不允许将集合转换为IMyCollection<Teacher>
您可以通过将其声明为IMyCollection<in T>来使您的IMyCollection<T>逆变,这将修复您的情况并允许您进行该赋值,但同时它将阻止您从中检索出一个Teacher对象,因为它不是协变的。
通常,从集合中设置和检索泛型值的唯一方法是使其不变(这是默认值),这也是为什么BCL中的所有泛型集合都是不变的,只有一些接口是逆变或协变的(例如IEnumerable<T>是协变的,因为它只涉及检索值)。
由于您将问题中的错误更改为“无法隐式转换类型 'PersonCollection' 为 'IMyCollection'”,让我也解释一下这个情况(将这个答案变成一个完整的逆变与协变的答案*叹气*…)。
所以对于这个问题,代码应该如下所示:
PersonCollection<Teacher> personCollection = new PersonCollection<Teacher>();
IMyCollection<Person> myCollection = personCollection;

再次假设这是有效的并且能工作。现在,我们有一个可以使用的 IMyCollection<Person> 集合!那么让我们在这里添加一些人:

myCollection.Add(new Teacher("Teacher A"));
myCollection.Add(new Teacher("Teacher B"));
myCollection.Add(new Student("Student A"));

哎呀!实际的集合仍然是一个 PersonCollection<Teacher>,它只能接受 Teacher 对象。但是 IMyCollection<Person> 类型允许我们添加也是人的 Student 对象!因此,这将在运行时失败,并且考虑到我们想要在编译时进行类型安全检查,编译器必须禁止在此处进行赋值。

这种赋值只对协变的 IMyCollection<out T> 有效,但这也会禁止我们向其中添加类型为 T 的元素(出于与上述相同的原因)。


我不明白,这个例子包含了一个集合机制,如果没有'Add(new Object())'方法或类似的东西怎么办?或者(就像我的例子一样)如果我想将'IMyCollection<Teacher>'转换为'IMyCollection<Person>'怎么办? - aradalvand
@AmirHosseinAhmadi 对不起,我在你的类型被称为“IMyCollection”而你没有提供任何更多信息时,只是假定了集合语义。无论如何,正如我在我的答案中解释的那样,您必须决定您想要的类型是contra, co还是invariant。根据您的所作所为,这对所包括的操作有所影响。将“IMyCollection<Teacher>”转换为“IMyCollection<Person>”与从“PersonCollection<Teacher>”转换相同,因此相同的事情适用于那里。 - poke

1

从您的源代码作为真相出发,实际上您误读了错误信息。

所以,在上面的代码中,尽管类'Teacher'是从类'Person'派生出来的, 但IMyCollection<Teacher>无法转换为 IMyCollection<Person>,为什么?

实际上的错误是

无法隐式地将类型PersonCollection<Person>转换为 IMyCollection<Teacher>

至少从面向对象的角度来看,这是预期的行为。重要的是要理解默认的T是不变的。这就是为什么一开始就遇到了这个问题。也就是说,如果T是Teacher,那么T只能是Teacher而不是Person。同样地,如果T是Person,那么T只能是Person而不是Teacher

这是因为协变和逆变是互斥的。虽然有方法支持两者,但你必须将它们分成支持不变性、协变性和逆变性的接口。在你的情况下,你需要添加对逆变性的支持。例如:
接口 IMyCollection {}
换句话说,进入(参见in generic modifier keyword)接口的内容可以是类型T,而不是“返回”或从接口返回的类型(那将是协变)。

请注意,OP的错误信息也与代码不匹配,因此实际上有三种不同的示例情况。 - poke
修改了问题,以指出我认为的真相来源。 - P.Brian.Mackey

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