协变和逆变中的类型安全问题

9
我正在阅读Jon Skeet的《深入理解C#》一书。虽然我已经理解了协变和逆变的概念,但我无法理解下面这句话:
“当SomeType仅描述返回类型参数的操作时,协变是安全的,而当SomeType仅描述接受类型参数的操作时,逆变是安全的。”
请有人举个例子来解释为什么在一个方向上都是类型安全的,而在另一个方向上则不是。
更新问题:
我仍然没有从给出的答案中理解。 我将尝试使用《C# In Depth》中相同的示例来解释我的疑虑。
书中使用以下类层次结构进行说明:
COVARIANCE是:尝试从IEnumerable

你能具体说明一下你对提供的答案不理解的地方吗?这些答案非常详细,所以很难立即确定你的困惑点在哪里。 - Asad Saeeduddin
可能是仍然困惑于协变和逆变以及in/out的问题的重复。 - Haspemulator
这两个问题在试图解释相同的概念方面是相关的。但它们只是相关,不是完全重复的。 - teenup
3个回答

10

协变性

当SomeType仅描述返回类型参数的操作时,协变性是安全的。

IEnumerable<out T>接口可能是协变性最常见的例子。它是安全的,因为它只返回类型为T的值(确切地说是IEnumerator<out T>),但不接受任何T对象作为参数。

public interface IEnumerable<out T> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

这是可行的,因为IEnumerator<T>也是协变的,并且仅返回T
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
    T Current { get; }
}

如果您拥有一个名为Base的基类和一个名为Derived的派生类,则可以执行以下操作:
IEnumerable<Derived> derivedItems = Something();
IEnumerable<Base> baseItems = derivedItems;

这种方法可行是因为 derivedItems 中的每个项目都是 Base 的实例,所以我们刚才这样赋值是完全可以接受的。然而,我们不能反过来赋值:
IEnumerable<Base> baseItems = Something();
IEnumerable<Derived> derivedItems = baseItems; // No good!

这并不安全,因为不能保证每个Base的实例也是Derived的实例。

逆变性

当SomeType只描述接受类型参数的操作时,逆变性是安全的。

Action<in T>委托是逆变性的一个很好的例子。

public delegate void Action<in T>(T obj);

这是安全的,因为它只接受T作为参数,但不会返回T

逆变性让您可以执行以下操作:

Action<Base> baseAction = b => b.DoSomething()
Action<Derived> derivedAction = baseAction;

Derived d = new Derived();
// These 2 lines do the same thing:
baseAction(d);
derivedAction(d);

这能够工作是因为将 Derived 的实例传递给 baseAction 是完全可以接受的。然而,反过来就行不通了:
Action<Derived> derivedAction = d => d.DoSomething()
Action<Base> baseAction = derivedAction; // No good!

Base b = new Base();
baseAction(b);    // This is OK.
derivedAction(b); // This does not work because b may not be an instance of Derived!

这并不安全,因为无法保证 Base 的实例也是 Derived 的实例。


6

想象一下,您创建了一个ILogger<in T>接口,该接口知道如何记录T的详细信息。假设您有一个Request类和一个ExpeditedRequest子类。毫无疑问,ILogger<Request>应该可以转换为ILogger<ExpeditedRequest>。毕竟,它可以记录任何请求。

Interface ILogger<in T> where T: Request {
     void Log(T arg);
}

现在想象一下另一个接口IRequestProducer<out T>,它从某个队列中获取下一个请求。在您的系统中有不同的请求来源,当然,其中一些仍然可以有不同的子类。在这种情况下,我们不能依赖将IRequestProducer<Request>转换为IRequestProducer<ExpeditedRequest>,因为它可能会产生非加急请求。但是反向转换是可行的。

Interface IRequestProducer<T> where T: Request {
    T GetNextRequest();
}

1

我在阅读MSDN的以下两个页面后理解了:

https://msdn.microsoft.com/en-us/library/dd469484.aspx
https://msdn.microsoft.com/en-us/library/dd469487.aspx

实际上,我认为当我阅读本书的C# 4部分并解释了GenericsType Parameters时,书中的内容也会变得更加清晰明了。现在,我正在阅读书中C# 1部分的Limitations of Generics in C#
我想要理解的声明如下:
“当SomeType仅描述返回类型参数的操作时,协变是安全的,并且当SomeType仅描述接受类型参数的操作时,逆变是安全的。”
除了安全之外,编译器还会抱怨无法以其他方向编写接口方法,正如msdn上述两页中所写:
“如果一个类型仅用作方法参数类型而不用作方法返回类型,则可以在泛型接口或委托中声明该类型为逆变类型。”
“在泛型接口中,如果满足以下条件,则可以声明类型参数为协变类型:
类型参数仅用作接口方法的返回类型,而不用作方法参数类型。”
现在有两个点,让我对这个声明感到困惑:
首先,我误解了声明本身——我认为协变仅在从某个方法返回Inteface<T>实例而不是将其作为输入传递给某个方法时才安全。然而,它涉及类型参数T和接口方法。逆变亦然。
第二,在我理解了这个声明的含义之后——它涉及将泛型类型参数T传递/返回到接口方法中。我想知道为什么在协变中只有从接口方法返回T时才是类型安全的,而在逆变中只有将T作为输入传递到接口方法中时才是类型安全的。
为什么只有从接口方法中返回T时才是类型安全的协变:
interface IBase<out T>
{
    T Return_T(); // Valid and Type Safe
    void Accept_T(T input) // Invalid and Type UnSafe
}
class Sample<T> : IBase<T> { }
class BaseClass {}
class DerivedClass : BaseClass
{}
IBase<BaseClass> ibase = new Sample<BaseClass>();
IBase<DerivedClass> iderived = new Sample<DerivedClass>();

ibase = iderived; // Can be assinged because `T` is Covariant

BaseClass b = new BaseClass();
DerivedClass d = new DerivedClass();

ibase.Return_T(); //  At runtime, this will return `DerivedClass` which can be assinged to variable of both base and derived class and is type safe
ibase.Accept_T(b); // The compiler will accept this statement, because at compile time, it accepts an instance of `BaseClass`, but at runtime, it actually needs an instance of `DerivedClass`. So, we are eventually assigning an instance of `BaseClass` to `DerivedClass` which is type unsafe.

为什么只有在将T作为输入参数传递时,协变性才是类型安全的:
interface IBase<in T>
{
    T Return_T(); // Invalid and Type UnSafe
    void Accept_T(T input) // Valid and Type Safe
}
class Sample<T> : IBase<T> { }
class BaseClass {}
class DerivedClass : BaseClass
{}
IBase<BaseClass> ibase = new Sample<BaseClass>();
IBase<DerivedClass> iderived = new Sample<DerivedClass>();

iderived = ibase; // Can be assinged because `T` is Contravariant

BaseClass b = new BaseClass();
DerivedClass d = new DerivedClass();

iderived.Accept_T(d); // This is Type Safe, because both at compile time and runtime, either instance of `DerivedClass` can be assinged to `BaseClass` or instance of `BaseClass` can be assinged to `BaseClass`

DerivedClass d2 = iderived.Return_T(); // This is type unsafe, because this statement is valid at compile time, but at runtime, this will return an instance of `BaseClass` which is getting assinged to `DerivedClass`

这就是我理解的方式,所以我选择了我的答案。为什么要踩我呢? - teenup
我看不出任何理由来踩这个回答;在我看来,它是一个很好的回答。 - AJ Richardson

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