仍然对协变和逆变感到困惑输入/输出

发布于 2024-09-13 19:10:18 字数 1203 浏览 2 评论 0原文

好吧,我在 stackoverflow 上读了一些关于这个主题的内容,观看了 this & ,但对协方差/反方差仍然有点困惑。

来自此处

协方差允许“更大”(更少 特定)类型被替换为 仅原始类型的 API 用于“输出”位置(例如 返回值)。逆变允许 “更小”(更具体)的类型 替换为 API,其中 原始类型仅用于 “输入”位置。

我知道这与类型安全有关。

关于in/out的事情。我可以说当我需要写入时我使用in,而当我需要写入时我使用outin 表示逆变,out 表示协方差。但从上面的解释......

这里

例如,List 不能 被视为 List 因为 list.Add(new Apple()) 适用于 列出但不适用于 List

所以不应该是这样,如果我要使用 in/ am 要写入对象,它必须更大更通用。

我知道这个问题已经被问过,但仍然很困惑。

ok i read a bit on this topic on stackoverflow, watched this & this, but still a bit confused about co/contra-variance.

from here

Covariance allows a "bigger" (less
specific) type to be substituted in an
API where the original type is only
used in an "output" position (e.g. as
a return value). Contravariance allows
a "smaller" (more specific) type to be
substituted in an API where the
original type is only used in an
"input" position.

i know it has to do with type safety.

about the in/out thing. can i say i use in when i need to write to it, and out when its read only. and in means contra-variance, out co-variance. but from the explanation above...

and here

For example, a List<Banana> can't be
treated as a List<Fruit> because
list.Add(new Apple()) is valid for
List but not for List<Banana>.

so shouldn't it be, if i were to use in/ am going to write to the object, it must be bigger more generic.

i know this question has been asked but still very confused.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(7

不奢求什么 2024-09-20 19:10:19

协方差很容易理解。这是很自然的。逆变更令人困惑。

仔细查看来自 MSDN 的示例。了解 SortedList 如何期望 IComparer,但它们传入的是 ShapeAreaComparer :IComparer。 Shape 是“较大”类型(它位于被调用者的签名中,而不是调用者),但逆变允许“较小”类型(Circle)替换 ShapeAreaComparer 中通常采用 Shape 的任何位置。

希望有帮助。

Covariance is pretty easy to understand. It's natural. Contravariance is more confusing.

Take a close look at this example from MSDN. See how SortedList expects an IComparer, but they are passing in a ShapeAreaComparer : IComparer. The Shape is the "bigger" type (it's in the signature of the callee, not the caller), but contravariance allows the "smaller" type - the Circle - to be substituted for everywhere in the ShapeAreaComparer that would normally take a Shape.

Hope that helps.

梦里兽 2024-09-20 19:10:19

在进入主题之前,让我们快速回顾一下:

基类引用可以保存派生类对象,但反之则不然。

协方差
协方差允许您在需要基类型对象的地方传递派生类型对象
协方差可以应用于委托、泛型、数组、接口等。

逆变:
逆变应用于参数。它允许将带有基类参数的方法分配给需要派生类参数的委托,

请看下面的简单示例:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CovarianceContravarianceDemo
{
    //base class
    class A
    {

    }

    //derived class
    class B : A
    {

    }
    class Program
    {
        static A Method1(A a)
        {
            Console.WriteLine("Method1");
            return new A();
        }

        static A Method2(B b)
        {
            Console.WriteLine("Method2");
            return new A();
        }

        static B Method3(B b)
        {
            Console.WriteLine("Method3");
            return new B();
        }

        public delegate A MyDelegate(B b);
        static void Main(string[] args)
        {
            MyDelegate myDel = null;
            myDel = Method2;// normal assignment as per parameter and return type

            //Covariance,  delegate expects a return type of base class
            //but we can still assign Method3 that returns derived type and 
            //Thus, covariance allows you to assign a method to the delegate that has a less derived return type.
            myDel = Method3;
            A a = myDel(new B());//this will return a more derived type object which can be assigned to base class reference

            //Contravariane is applied to parameters. 
            //Contravariance allows a method with the parameter of a base class to be assigned to a delegate that expects the parameter of a derived class.
            myDel = Method1;
            myDel(new B()); //Contravariance, 

        }
    }
}

Before coming to topic, lets have a quick refresher:

Base class reference can hold a derived class object BUT not vice-versa.

Covariance:
Covariance lets you to pass a derived type object where a base type object is expected
Covariance can be applied on delegate, generic, array, interface, etc.

Contravariance:
Contravariance is applied to parameters. It allows a method with the parameter of a base class to be assigned to a delegate that expects the parameter of a derived class

Have a look at simple example below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CovarianceContravarianceDemo
{
    //base class
    class A
    {

    }

    //derived class
    class B : A
    {

    }
    class Program
    {
        static A Method1(A a)
        {
            Console.WriteLine("Method1");
            return new A();
        }

        static A Method2(B b)
        {
            Console.WriteLine("Method2");
            return new A();
        }

        static B Method3(B b)
        {
            Console.WriteLine("Method3");
            return new B();
        }

        public delegate A MyDelegate(B b);
        static void Main(string[] args)
        {
            MyDelegate myDel = null;
            myDel = Method2;// normal assignment as per parameter and return type

            //Covariance,  delegate expects a return type of base class
            //but we can still assign Method3 that returns derived type and 
            //Thus, covariance allows you to assign a method to the delegate that has a less derived return type.
            myDel = Method3;
            A a = myDel(new B());//this will return a more derived type object which can be assigned to base class reference

            //Contravariane is applied to parameters. 
            //Contravariance allows a method with the parameter of a base class to be assigned to a delegate that expects the parameter of a derived class.
            myDel = Method1;
            myDel(new B()); //Contravariance, 

        }
    }
}
十雾 2024-09-20 19:10:19

用乔恩的话来说:

协方差允许在 API 中替换“更大”(不太具体)的类型,其中原始类型仅用于“输出”位置(例如作为返回值)。逆变允许在 API 中替换“较小”(更具体)的类型,其中原始类型仅用于“输入”位置。

我一开始发现他的解释令人困惑 - 但一旦强调了被替换,再结合 C# 编程指南中的示例,我就明白了:

// Covariance.   
IEnumerable<string> strings = new List<string>();  
// An object that is instantiated with a more derived type argument   
// is assigned to an object instantiated with a less derived type argument.   

// Assignment compatibility is preserved.   
IEnumerable<object> objects = strings;

// Contravariance.             
// Assume that the following method is in the class:   
// static void SetObject(object o) { }   
Action<object> actObject = SetObject;  
// An object that is instantiated with a less derived type argument   
// is assigned to an object instantiated with a more derived type argument.   

// Assignment compatibility is reversed.   
Action<string> actString = actObject;    

转换器委托帮助我理解它:

delegate TOutput Converter<in TInput, out TOutput>(TInput input);

TOutput 代表 <协方差,其中方法返回更具体的类型

TInput 表示逆变,其中向方法传递不太具体的类型

public class Dog { public string Name { get; set; } }
public class Poodle : Dog { public void DoBackflip(){ System.Console.WriteLine("2nd smartest breed - woof!"); } }

public static Poodle ConvertDogToPoodle(Dog dog)
{
    return new Poodle() { Name = dog.Name };
}

List<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } };
List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle));
poodles[0].DoBackflip();

In Jons words:

Covariance allows a "bigger" (less specific) type to be substituted in an API where the original type is only used in an "output" position (e.g. as a return value). Contravariance allows a "smaller" (more specific) type to be substituted in an API where the original type is only used in an "input" position.

I found his explanation confusing at first - but it made sense to me once to be substitued is emphasised, combined with the example from the C# programming guide:

// Covariance.   
IEnumerable<string> strings = new List<string>();  
// An object that is instantiated with a more derived type argument   
// is assigned to an object instantiated with a less derived type argument.   

// Assignment compatibility is preserved.   
IEnumerable<object> objects = strings;

// Contravariance.             
// Assume that the following method is in the class:   
// static void SetObject(object o) { }   
Action<object> actObject = SetObject;  
// An object that is instantiated with a less derived type argument   
// is assigned to an object instantiated with a more derived type argument.   

// Assignment compatibility is reversed.   
Action<string> actString = actObject;    

The converter delegate helps me to understand it:

delegate TOutput Converter<in TInput, out TOutput>(TInput input);

TOutput represents covariance where a method returns a more specific type.

TInput represents contravariance where a method is passed a less specific type.

public class Dog { public string Name { get; set; } }
public class Poodle : Dog { public void DoBackflip(){ System.Console.WriteLine("2nd smartest breed - woof!"); } }

public static Poodle ConvertDogToPoodle(Dog dog)
{
    return new Poodle() { Name = dog.Name };
}

List<Dog> dogs = new List<Dog>() { new Dog { Name = "Truffles" }, new Dog { Name = "Fuzzball" } };
List<Poodle> poodles = dogs.ConvertAll(new Converter<Dog, Poodle>(ConvertDogToPoodle));
poodles[0].DoBackflip();
温暖的光 2024-09-20 19:10:19

让我们从协变和逆变示例中使用的类层次结构开始:

public class Weapon { }
public class Sword : Weapon { }
public class TwoHandedSword : Sword { }

协变意味着您可以返回(输出)子类型的实例作为其超类型。这是一个
示例:

[Fact]
public void Covariance_tests()

 Assert.IsType<Sword>(Covariance());
 Assert.Throws<InvalidCastException>(() => BreakCovariance());
}
// We can return a Sword into a Weapon
private Weapon Covariance()
 => new Sword();
// We cannot return a Sword into a TwoHandedSword
private TwoHandedSword BreakCovariance()
 => (TwoHandedSword)new Sword();

如前面的示例所示,打破协方差的一种方法是将超类型作为子类型返回。
另一方面,逆变意味着您可以输入子类型的实例作为其超类型。
它基本上是相同的,但对于输入来说,如下所示:

[Fact]
public void Contravariance_tests()
{
 // We can pass a Sword as a Weapon
 Contravariance(new Sword());
 // We cannot pass a Weapon as a Sword
 BreakContravariance(new Weapon()); // Compilation error 
}
private void Contravariance(Weapon weapon) { }
private void BreakContravariance(Sword weapon) { }

正如我们从前面的代码中看到的那样,适用相同的多态规则。我们可以使用子类型作为
一个超类型。

Let’s start with the class hierarchy we are using in the covariance and contravariance examples:

public class Weapon { }
public class Sword : Weapon { }
public class TwoHandedSword : Sword { }

Covariance means you can return (output) the instance of a subtype as its supertype. Here is an
example:

[Fact]
public void Covariance_tests()

 Assert.IsType<Sword>(Covariance());
 Assert.Throws<InvalidCastException>(() => BreakCovariance());
}
// We can return a Sword into a Weapon
private Weapon Covariance()
 => new Sword();
// We cannot return a Sword into a TwoHandedSword
private TwoHandedSword BreakCovariance()
 => (TwoHandedSword)new Sword();

As shown in the preceding example, one way to break covariance is to return a supertype as a subtype.
On the other hand, contravariance means you can input the instance of a subtype as its supertype.
It is basically the same thing but for inputs, like this:

[Fact]
public void Contravariance_tests()
{
 // We can pass a Sword as a Weapon
 Contravariance(new Sword());
 // We cannot pass a Weapon as a Sword
 BreakContravariance(new Weapon()); // Compilation error 
}
private void Contravariance(Weapon weapon) { }
private void BreakContravariance(Sword weapon) { }

The same polymorphic rule applies, as we can see from the preceding code. We can use a subtype as
a supertype.

焚却相思 2024-09-20 19:10:18

我不得不认真思考如何很好地解释这一点。解释似乎和理解它一样困难。

假设您有一个基类 Fruit。你有两个子类 Apple 和 Banana。

     Fruit
      / \
Banana   Apple

您创建两个对象:

Apple a = new Apple();
Banana b = new Banana();

对于这两个对象,您可以将它们类型转换为 Fruit 对象。

Fruit f = (Fruit)a;
Fruit g = (Fruit)b;

您可以将派生类视为它们的基类。

但是,您不能将基类视为派生类,

a = (Apple)f; //This is incorrect

让我们将其应用于列表示例。

假设您创建了两个列表:

List<Fruit> fruitList = new List<Fruit>();
List<Banana> bananaList = new List<Banana>();

您可以执行类似的操作...

fruitList.Add(new Apple());

因为

fruitList.Add(new Banana());

当您将它们添加到列表中时,它本质上是对它们进行类型转换。您可以这样想……

fruitList.Add((Fruit)new Apple());
fruitList.Add((Fruit)new Banana());

但是,将相同的逻辑应用于相反的情况会引发一些危险信号。

bananaList.Add(new Fruit());

bannanaList.Add((Banana)new Fruit());

因为您不能像派生类一样对待基类,所以会产生错误。

以防万一您的问题是为什么这会导致错误,我也会对此进行解释。

这是 Fruit 类

public class Fruit
{
    public Fruit()
    {
        a = 0;
    }
    public int A { get { return a; } set { a = value } }
    private int a;
}

,这是 Banana 类

public class Banana: Fruit
{
   public Banana(): Fruit() // This calls the Fruit constructor
   {
       // By calling ^^^ Fruit() the inherited variable a is also = 0; 
       b = 0;
   }
   public int B { get { return b; } set { b = value; } }
   private int b;
}

因此,假设您再次创建了两个对象,

Fruit f = new Fruit();
Banana ba = new Banana();

请记住 Banana 有两个变量“a”和“b”,而 Fruit 只有一个变量“a”。
所以当你这样做时......

f = (Fruit)b;
f.A = 5;

你创建了一个完整的 Fruit 对象。
但如果你要这样做......

ba = (Banana)f;
ba.A = 5;
ba.B = 3; //Error!!!: Was "b" ever initialized? Does it exist?

问题是你没有创建一个完整的 Banana 类。并非所有数据成员都被声明/初始化。

现在我洗完澡回来,给自己买了点零食,事情变得有点复杂。

事后看来,当进入复杂的东西时,我应该放弃这个隐喻

让我们创建两个新类:

public class Base
public class Derived : Base

它们可以做任何你喜欢的事情

现在让我们定义两个函数

public Base DoSomething(int variable)
{
    return (Base)DoSomethingElse(variable);
}  
public Derived DoSomethingElse(int variable)
{
    // Do stuff 
}

这有点像“out”的工作方式你应该总是能够使用派生的类就好像它是基类一样,让我们​​将其应用到接口

interface MyInterface<T>
{
    T MyFunction(int variable);
}

out/in 之间的主要区别在于泛型用作返回类型或方法参数时,这是前一种情况。

让我们定义一个实现此接口的类:

public class Thing<T>: MyInterface<T> { }

然后我们创建两个对象:

MyInterface<Base> base = new Thing<Base>;
MyInterface<Derived> derived = new Thing<Derived>;

如果您这样做:

base = derived;

您会收到类似“无法隐式转换自...”的错误

,您有两种选择,1)显式转换它们,或者,2)告诉编译器隐式转换它们。

base = (MyInterface<Base>)derived; // #1

或者

interface MyInterface<out T>  // #2
{
    T MyFunction(int variable);
}

如果您的界面如下所示,则第二种情况会出现:

interface MyInterface<T>
{
    int MyFunction(T variable); // T is now a parameter
}

再次将其与两个函数相关联,

public int DoSomething(Base variable)
{
    // Do stuff
}  
public int DoSomethingElse(Derived variable)
{
    return DoSomething((Base)variable);
}

希望您看到情况如何逆转,但本质上是相同类型的转换。

再次使用相同的类

public class Base
public class Derived : Base
public class Thing<T>: MyInterface<T> { }

和相同的对象,

MyInterface<Base> base = new Thing<Base>;
MyInterface<Derived> derived = new Thing<Derived>;

如果你尝试将它们设置为相等,

base = derived;

你的编译器会再次对你大喊大叫,你有与以前相同的选项,

base = (MyInterface<Base>)derived;

或者

interface MyInterface<in T> //changed
{
    int MyFunction(T variable); // T is still a parameter
}

基本上在泛型仅用作返回类型时使用接口方法。当它将被用作方法参数时使用。使用委托时也适用相同的规则。

有一些奇怪的例外,但我不会在这里担心它们。

提前对任何粗心错误表示歉意=)

I had to think long and hard on how to explain this well. Explaining is seems to be just as hard as understanding it.

Imagine you have a base class Fruit. And you have two subclasses Apple and Banana.

     Fruit
      / \
Banana   Apple

You create two objects:

Apple a = new Apple();
Banana b = new Banana();

For both of these objects you can typecast them into the Fruit object.

Fruit f = (Fruit)a;
Fruit g = (Fruit)b;

You can treat derived classes as if they were their base class.

However you cannot treat a base class like it was a derived class

a = (Apple)f; //This is incorrect

Lets apply this to the List example.

Suppose you created two Lists:

List<Fruit> fruitList = new List<Fruit>();
List<Banana> bananaList = new List<Banana>();

You can do something like this...

fruitList.Add(new Apple());

and

fruitList.Add(new Banana());

because it is essentially typecasting them as you add them into the list. You can think of it like this...

fruitList.Add((Fruit)new Apple());
fruitList.Add((Fruit)new Banana());

However, applying the same logic to the reverse case raises some red flags.

bananaList.Add(new Fruit());

is the same as

bannanaList.Add((Banana)new Fruit());

Because you cannot treat a base class like a derived class this produces errors.

Just in case your question was why this causes errors I'll explain that too.

Here's the Fruit class

public class Fruit
{
    public Fruit()
    {
        a = 0;
    }
    public int A { get { return a; } set { a = value } }
    private int a;
}

and here's the Banana class

public class Banana: Fruit
{
   public Banana(): Fruit() // This calls the Fruit constructor
   {
       // By calling ^^^ Fruit() the inherited variable a is also = 0; 
       b = 0;
   }
   public int B { get { return b; } set { b = value; } }
   private int b;
}

So imagine that you again created two objects

Fruit f = new Fruit();
Banana ba = new Banana();

remember that Banana has two variables "a" and "b", while Fruit only has one, "a".
So when you do this...

f = (Fruit)b;
f.A = 5;

You create a complete Fruit object.
But if you were to do this...

ba = (Banana)f;
ba.A = 5;
ba.B = 3; //Error!!!: Was "b" ever initialized? Does it exist?

The problem is that you don't create a complete Banana class.Not all the data members are declared / initialized.

Now that I'm back from the shower and got my self a snack heres where it gets a little complicated.

In hindsight I should have dropped the metaphor when getting into the complicated stuff

lets make two new classes:

public class Base
public class Derived : Base

They can do whatever you like

Now lets define two functions

public Base DoSomething(int variable)
{
    return (Base)DoSomethingElse(variable);
}  
public Derived DoSomethingElse(int variable)
{
    // Do stuff 
}

This is kind of like how "out" works you should always be able to use a derived class as if it were a base class, lets apply this to an interface

interface MyInterface<T>
{
    T MyFunction(int variable);
}

The key difference between out/in is when the Generic is used as a return type or a method parameter, this the the former case.

lets define a class that implements this interface:

public class Thing<T>: MyInterface<T> { }

then we create two objects:

MyInterface<Base> base = new Thing<Base>;
MyInterface<Derived> derived = new Thing<Derived>;

If you were do this:

base = derived;

You would get an error like "cannot implicitly convert from..."

You have two choices, 1) explicitly convert them or, 2) tell the complier to implicitly convert them.

base = (MyInterface<Base>)derived; // #1

or

interface MyInterface<out T>  // #2
{
    T MyFunction(int variable);
}

The second case comes in to play if your interface looks like this:

interface MyInterface<T>
{
    int MyFunction(T variable); // T is now a parameter
}

relating it to the two functions again

public int DoSomething(Base variable)
{
    // Do stuff
}  
public int DoSomethingElse(Derived variable)
{
    return DoSomething((Base)variable);
}

hopefully you see how the situation has reversed but is essentially the same type of conversion.

Using the same classes again

public class Base
public class Derived : Base
public class Thing<T>: MyInterface<T> { }

and the same objects

MyInterface<Base> base = new Thing<Base>;
MyInterface<Derived> derived = new Thing<Derived>;

if you try to set them equal

base = derived;

your complier will yell at you again, you have the same options as before

base = (MyInterface<Base>)derived;

or

interface MyInterface<in T> //changed
{
    int MyFunction(T variable); // T is still a parameter
}

Basically use out when the generic is only going to be used as a return type of the interface methods. Use in when it is going to be used as a Method parameter. The same rules apply when using delegates too.

There are strange exceptions but I'm not going to worry about them here.

Sorry for any careless mistakes in advance =)

假情假意假温柔 2024-09-20 19:10:18

C# 4.0 中的协变和逆变都指使用派生类而不是基类的能力。 in/out 关键字是编译器提示,指示类型参数是否将用于输入和输出。

方差 C# 4.0 中的协方差由 out 关键字辅助,这意味着使用 out 类型参数的派生类的泛型类型是可以的。因此,

IEnumerable<Fruit> fruit = new List<Apple>();

AppleFruit,因此 List 可以安全地用作 IEnumerable

逆变

由于 是 in 关键字,它表示输入类型,通常在委托中。原理是一样的,就是说委托可以接受更多的派生类。

public delegate void Func<in T>(T param);

这意味着如果我们有一个 Func,它可以转换为 Func

Func<Fruit> fruitFunc = (fruit)=>{};
Func<Apple> appleFunc = fruitFunc;

如果它们基本上是同一件事,为什么被称为协变/逆变呢?

因为即使原理相同,从派生到基的安全转换,当用于输入类型时,我们可以安全地将派生程度较低的类型 (Func) 转换为派生程度较高的类型 ( Func),这是有道理的,因为任何采用 Fruit 的函数也可以采用 Apple

Both covariance and contravariance in C# 4.0 refer to the ability of using a derived class instead of base class. The in/out keywords are compiler hints to indicate whether or not the type parameters will be used for input and output.

Covariance

Covariance in C# 4.0 is aided by out keyword and it means that a generic type using a derived class of the out type parameter is OK. Hence

IEnumerable<Fruit> fruit = new List<Apple>();

Since Apple is a Fruit, List<Apple> can be safely used as IEnumerable<Fruit>

Contravariance

Contravariance is the in keyword and it denotes input types, usually in delegates. The principle is the same, it means that the delegate can accept more derived class.

public delegate void Func<in T>(T param);

This means that if we have a Func<Fruit>, it can be converted to Func<Apple>.

Func<Fruit> fruitFunc = (fruit)=>{};
Func<Apple> appleFunc = fruitFunc;

Why are they called co/contravariance if they are basically the same thing?

Because even though the principle is the same, safe casting from derived to base, when used on the input types, we can safely cast a less derived type (Func<Fruit>) to a more derived type (Func<Apple>), which makes sense, since any function that takes Fruit, can also take Apple.

情绪 2024-09-20 19:10:18

让我分享一下我对这个话题的看法。

免责声明:忽略空赋值,我使用它们来保持代码相对较短,它们足以查看编译器想要告诉我们的内容。

让我们从类的层次结构开始:

class Animal { }

class Mammal : Animal { }

class Dog : Mammal { }

现在定义一些接口,以说明 inout 泛型修饰符实际执行的操作:

interface IInvariant<T>
{
    T Get(); // ok, an invariant type can be both put into and returned
    void Set(T t); // ok, an invariant type can be both put into and returned
}

interface IContravariant<in T>
{
    //T Get(); // compilation error, cannot return a contravariant type
    void Set(T t); // ok, a contravariant type can only be **put into** our class (hence "in")
}

interface ICovariant<out T>
{
    T Get(); // ok, a covariant type can only be **returned** from our class (hence "out")
    //void Set(T t); // compilation error, cannot put a covariant type into our class
}

好的,那为什么还要使用带有 in 的接口呢?out 修饰符是否限制我们?让我们看看:


不变性

让我们从不变性开始(没有 in,没有 out 修饰符)

不变性实验

考虑 IInvariant

  • IInvariant.Get() - 返回一个哺乳动物
  • IInvariant.Set(Mammal) - 接受一个哺乳动物

如果我们尝试: IInvariant<哺乳动物> invariantMammal = (IInvariant)null?

  • 调用 IInvariant.Get() 的人都期望得到一个 Mammal,但是 IInvariant.Get() - 返回一个 Animal。并非所有动物都是哺乳动物,因此它们不相容
  • 调用 IInvariant.Set(Mammal) 的人都期望可以传递 Mammal。由于 IInvariant.Set(Animal) 接受任何动物(包括哺乳动物),因此它兼容
  • 结论:这样的分配是不兼容

如果我们尝试:IInvariantinvariantMammal = (IInvariant)null?

  • 调用 IInvariant.Get() 的人会期望得到一个 Mammal,IInvariant.Get() - 返回一个 Dog,每只 Dog是哺乳动物,因此它兼容
  • 调用 IInvariant.Set(Mammal) 的人都期望可以传递 Mammal。由于 IInvariant.Set(Dog) 接受狗(而不是所有哺乳动物都为狗),因此它不兼容
  • 结论:这样的赋值是不兼容

让我们检查一下我们是否正确

IInvariant<Animal> invariantAnimal1 = (IInvariant<Animal>)null; // ok
IInvariant<Animal> invariantAnimal2 = (IInvariant<Mammal>)null; // compilation error
IInvariant<Animal> invariantAnimal3 = (IInvariant<Dog>)null; // compilation error

IInvariant<Mammal> invariantMammal1 = (IInvariant<Animal>)null; // compilation error
IInvariant<Mammal> invariantMammal2 = (IInvariant<Mammal>)null; // ok
IInvariant<Mammal> invariantMammal3 = (IInvariant<Dog>)null; // compilation error

IInvariant<Dog> invariantDog1 = (IInvariant<Animal>)null; // compilation error
IInvariant<Dog> invariantDog2 = (IInvariant<Mammal>)null; // compilation error
IInvariant<Dog> invariantDog3 = (IInvariant<Dog>)null; // ok

这个很重要:值得注意的是,这取决于泛型类型参数是否在类层次结构中较高或较低时,泛型类型本身由于不同原因不兼容

好的,让我们看看如何利用它。


协方差 (out)

当您使用 out 泛型修饰符时,您就具有协方差(见上文)

如果我们的类型如下所示:ICovariant;,它声明了两件事:

  • 我的一些方法返回一个哺乳动物(因此 out 通用修饰符) - 这很无聊
  • 我的方法都不接受哺乳动物 - 但这很有趣,因为这是 out 泛型修饰符施加的实际限制

我们如何从 out 修饰符限制中受益?回顾一下上面“不变性实验”的结果。现在尝试看看当对协方差进行相同的实验时会发生什么?

协方差实验

如果我们尝试一下会怎样:ICovariantcovariantMammal = (ICovariant)null?

  • 调用 ICovariant.Get() 的人都期望得到一个 Mammal,但是 ICovariant.Get() - 返回一个 Animal。并非所有动物都是哺乳动物,因此它们不相容
  • ICovariant.Set(Mammal) - 由于 out 修饰符限制,这不再是问题!
  • 结论这样的分配是不兼容

如果我们尝试:ICovariantcovariantMammal = (ICovariant)null?

  • 调用 ICovariant.Get() 的人会期望得到一个 Mammal,ICovariant.Get() - 返回一个 Dog,每只 Dog是哺乳动物,因此它兼容
  • ICovariant.Set(Mammal) - 由于 out 修饰符限制,这不再是问题!
  • 结论这样的赋值是COMPATIBLE

让我们用代码来确认它:

ICovariant<Animal> covariantAnimal1 = (ICovariant<Animal>)null; // ok
ICovariant<Animal> covariantAnimal2 = (ICovariant<Mammal>)null; // ok!!!
ICovariant<Animal> covariantAnimal3 = (ICovariant<Dog>)null; // ok!!!

ICovariant<Mammal> covariantMammal1 = (ICovariant<Animal>)null; // compilation error
ICovariant<Mammal> covariantMammal2 = (ICovariant<Mammal>)null; // ok
ICovariant<Mammal> covariantMammal3 = (ICovariant<Dog>)null; // ok!!!

ICovariant<Dog> covariantDog1 = (ICovariant<Animal>)null; // compilation error
ICovariant<Dog> covariantDog2 = (ICovariant<Mammal>)null; // compilation error
ICovariant<Dog> covariantDog3 = (ICovariant<Dog>)null; // ok

逆变(in

当你使用in 泛型修饰符(见上文)

如果我们的类型看起来像:IContravariant,它声明了两件事:

  • 我的一些方法接受哺乳动物(因此 in 通用修饰符) - 这很无聊
  • 我的方法都没有返回哺乳动物 - 但这很有趣,因为这是 in 施加的实际限制 > 泛型修饰符

逆变实验

如果我们尝试一下会怎样:IContravariant; contravariantMammal = (IContravariant)null?

  • IContravariant.Get()- 由于 in 修饰符限制,这不再是问题!
  • 调用 IContravariant.Set(Mammal) 的人都期望可以传递 Mammal。由于 IContravariant.Set(Animal) 接受任何动物(包括哺乳动物),因此它兼容
  • 结论:这样的分配是COMPATIBLE

如果我们尝试:IContravariantcontravariantMammal = (IContravariant)null?

  • IContravariant.Get()- 由于 in 修饰符限制,这不再是问题!
  • 调用 IContravariant.Set(Mammal) 的人都期望可以传递 Mammal。由于IContravariant.Set(Dog)接受狗(而不是所有哺乳动物都为狗),因此它不兼容
  • 结论:这样的赋值是不兼容的,

让我们用代码来确认一下:

IContravariant<Animal> contravariantAnimal1 = (IContravariant<Animal>)null; // ok
IContravariant<Animal> contravariantAnimal2 = (IContravariant<Mammal>)null; // compilation error
IContravariant<Animal> contravariantAnimal3 = (IContravariant<Dog>)null; // compilation error

IContravariant<Mammal> contravariantMammal1 = (IContravariant<Animal>)null; // ok!!!
IContravariant<Mammal> contravariantMammal2 = (IContravariant<Mammal>)null; // ok
IContravariant<Mammal> contravariantMammal3 = (IContravariant<Dog>)null; // compilation error

IContravariant<Dog> contravariantDog1 = (IContravariant<Animal>)null; // ok!!!
IContravariant<Dog> contravariantDog2 = (IContravariant<Mammal>)null; // ok!!!
IContravariant<Dog> contravariantDog3 = (IContravariant<Dog>)null; // ok

顺便说一句,这感觉有点违反直觉,不是吗?

// obvious
Animal animal = (Dog)null; // ok
Dog dog = (Animal)null; // compilation error, not every Animal is a Dog

// but this looks like the other way around
IContravariant<Animal> contravariantAnimal = (IContravariant<Dog>) null; // compilation error
IContravariant<Dog> contravariantDog = (IContravariant<Animal>) null; // ok

为什么不两者都呢?

那么我们可以同时使用 inout 泛型修饰符吗? - 显然不是

为什么?回顾一下 inout 修饰符施加了哪些限制。如果我们想让泛型类型参数同时具有协变和逆变,我们基本上会说:

  • 我们接口的方法都没有返回 T
  • 我们接口的方法都没有接受 T >

这本质上会使我们的通用接口非通用

怎么记住呢?

你可以使用我的技巧:)

  1. “协变”比“逆变”短,并且这与它们的修饰符的长度(分别是“out”和“in”)
  2. 相反相反 varraint 有点直观(参见上面的示例)

Let me share my take on this topic.

Disclaimer: ignore null assignments, I'm using them to keep the code relatively short and they are just enough to see what compiler wants to tell us.

Let's start with a hierarchy of classes:

class Animal { }

class Mammal : Animal { }

class Dog : Mammal { }

Now define some interfaces, to illustrate what in and out generic modifiers actually do:

interface IInvariant<T>
{
    T Get(); // ok, an invariant type can be both put into and returned
    void Set(T t); // ok, an invariant type can be both put into and returned
}

interface IContravariant<in T>
{
    //T Get(); // compilation error, cannot return a contravariant type
    void Set(T t); // ok, a contravariant type can only be **put into** our class (hence "in")
}

interface ICovariant<out T>
{
    T Get(); // ok, a covariant type can only be **returned** from our class (hence "out")
    //void Set(T t); // compilation error, cannot put a covariant type into our class
}

Ok, so why bother using interfaces with in and out modifiers if they restrict us? Let's see:


Invariance

Lets start with invariance (no in, no out modifiers)

Invariance experiment

Consider IInvariant<Mammal>

  • IInvariant<Mammal>.Get() - returns a Mammal
  • IInvariant<Mammal>.Set(Mammal) - accepts a Mammal

What if we try: IInvariant<Mammal> invariantMammal = (IInvariant<Animal>)null?

  • Whoever calls IInvariant<Mammal>.Get() expects a Mammal, but IInvariant<Animal>.Get() - returns an Animal. Not every Animal is a Mammal so it's incompatible.
  • Whoever calls IInvariant<Mammal>.Set(Mammal) expects that a Mammal can be passed. Since IInvariant<Animal>.Set(Animal) accepts any Animal (including Mammal), it's compatible
  • CONCLUSION: such assignment is incompatible

And what if we try: IInvariant<Mammal> invariantMammal = (IInvariant<Dog>)null?

  • Whoever calls IInvariant<Mammal>.Get() expects a Mammal, IInvariant<Dog>.Get() - returns a Dog, every Dog is a Mammal, so it's compatible.
  • Whoever calls IInvariant<Mammal>.Set(Mammal) expects that a Mammal can be passed. Since IInvariant<Dog>.Set(Dog) accepts only Dogs (and not every Mammal as a Dog), it's incompatible.
  • CONCLUSION: such assignment is incompatible

Let's check if we're right

IInvariant<Animal> invariantAnimal1 = (IInvariant<Animal>)null; // ok
IInvariant<Animal> invariantAnimal2 = (IInvariant<Mammal>)null; // compilation error
IInvariant<Animal> invariantAnimal3 = (IInvariant<Dog>)null; // compilation error

IInvariant<Mammal> invariantMammal1 = (IInvariant<Animal>)null; // compilation error
IInvariant<Mammal> invariantMammal2 = (IInvariant<Mammal>)null; // ok
IInvariant<Mammal> invariantMammal3 = (IInvariant<Dog>)null; // compilation error

IInvariant<Dog> invariantDog1 = (IInvariant<Animal>)null; // compilation error
IInvariant<Dog> invariantDog2 = (IInvariant<Mammal>)null; // compilation error
IInvariant<Dog> invariantDog3 = (IInvariant<Dog>)null; // ok

THIS ONE IS IMPORTANT: It's worth noticing that depending on whether the generic type parameter is higher or lower in class hierarchy, the generic types themselves are incompatible for different reasons.

Ok, so let's find out how could we exploit it.


Covariance (out)

You have covariance when you use out generic modifier (see above)

If our type looks like: ICovariant<Mammal>, it declares 2 things:

  • Some of my methods return a Mammal (hence out generic modifier) - this is boring
  • None of my methods accept a Mammal - this is interesting though, because this is the actual restriction imposed by the out generic modifier

How can we benefit from out modifier restrictions? Look back at the results of the "Invariance experiment" above. Now try to see what happens when make the same experiment for covariance?

Covariance experiment

What if we try: ICovariant<Mammal> covariantMammal = (ICovariant<Animal>)null?

  • Whoever calls ICovariant<Mammal>.Get() expects a Mammal, but ICovariant<Animal>.Get() - returns an Animal. Not every Animal is a Mammal so it's incompatible.
  • ICovariant.Set(Mammal) - this is no longer an issue thanks to the out modifier restrictions!
  • CONCLUSION such assignment is incompatible

And what if we try: ICovariant<Mammal> covariantMammal = (ICovariant<Dog>)null?

  • Whoever calls ICovariant<Mammal>.Get() expects a Mammal, ICovariant<Dog>.Get() - returns a Dog, every Dog is a Mammal, so it's compatible.
  • ICovariant.Set(Mammal) - this is no longer an issue thanks to the out modifier restrictions!
  • CONCLUSION such assignment is COMPATIBLE

Let's confirm it with the code:

ICovariant<Animal> covariantAnimal1 = (ICovariant<Animal>)null; // ok
ICovariant<Animal> covariantAnimal2 = (ICovariant<Mammal>)null; // ok!!!
ICovariant<Animal> covariantAnimal3 = (ICovariant<Dog>)null; // ok!!!

ICovariant<Mammal> covariantMammal1 = (ICovariant<Animal>)null; // compilation error
ICovariant<Mammal> covariantMammal2 = (ICovariant<Mammal>)null; // ok
ICovariant<Mammal> covariantMammal3 = (ICovariant<Dog>)null; // ok!!!

ICovariant<Dog> covariantDog1 = (ICovariant<Animal>)null; // compilation error
ICovariant<Dog> covariantDog2 = (ICovariant<Mammal>)null; // compilation error
ICovariant<Dog> covariantDog3 = (ICovariant<Dog>)null; // ok

Contravariance (in)

You have contravariance when you use in generic modifier (see above)

If our type looks like: IContravariant<Mammal>, it declares 2 things:

  • Some of my methods accept a Mammal (hence in generic modifier) - this is boring
  • None of my methods return a Mammal - this is interesting though, because this is the actual restriction imposed by the in generic modifier

Contravariance experiment

What if we try: IContravariant<Mammal> contravariantMammal = (IContravariant<Animal>)null?

  • IContravariant<Mammal>.Get() - this is no longer an issue thanks to the in modifier restrictions!
  • Whoever calls IContravariant<Mammal>.Set(Mammal) expects that a Mammal can be passed. Since IContravariant<Animal>.Set(Animal) accepts any Animal (including Mammal), it's compatible
  • CONCLUSION: such assignment is COMPATIBLE

And what if we try: IContravariant<Mammal> contravariantMammal = (IContravariant<Dog>)null?

  • IContravariant<Mammal>.Get() - this is no longer an issue thanks to the in modifier restrictions!
  • Whoever calls IContravariant<Mammal>.Set(Mammal) expects that a Mammal can be passed. Since IContravariant<Dog>.Set(Dog) accepts only Dogs (and not every Mammal as a Dog), it's incompatible.
  • CONCLUSION: such assignment is incompatible

Let's confirm it with the code:

IContravariant<Animal> contravariantAnimal1 = (IContravariant<Animal>)null; // ok
IContravariant<Animal> contravariantAnimal2 = (IContravariant<Mammal>)null; // compilation error
IContravariant<Animal> contravariantAnimal3 = (IContravariant<Dog>)null; // compilation error

IContravariant<Mammal> contravariantMammal1 = (IContravariant<Animal>)null; // ok!!!
IContravariant<Mammal> contravariantMammal2 = (IContravariant<Mammal>)null; // ok
IContravariant<Mammal> contravariantMammal3 = (IContravariant<Dog>)null; // compilation error

IContravariant<Dog> contravariantDog1 = (IContravariant<Animal>)null; // ok!!!
IContravariant<Dog> contravariantDog2 = (IContravariant<Mammal>)null; // ok!!!
IContravariant<Dog> contravariantDog3 = (IContravariant<Dog>)null; // ok

BTW, this feels a bit counterintuitive, doesn't it?

// obvious
Animal animal = (Dog)null; // ok
Dog dog = (Animal)null; // compilation error, not every Animal is a Dog

// but this looks like the other way around
IContravariant<Animal> contravariantAnimal = (IContravariant<Dog>) null; // compilation error
IContravariant<Dog> contravariantDog = (IContravariant<Animal>) null; // ok

Why not both?

So can we use both in and out generic modifiers? - obviously not.

Why? Look back at what restrictions do in and out modifiers impose. If we wanted to make our generic type parameter both covariant and contravariant, we would basically say:

  • None of the methods of our interface returns T
  • None of the methods of our interface accepts T

Which would essentially make our generic interface non-generic.

How to remember it?

You can use my tricks :)

  1. "covariant" is shorter than "contravaraint" and this opposite to the lengths of their modifiers ("out" and "in" respectively)
  2. contravaraint is a little counterintuitive (see the example above)
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文