38

首先,我已经阅读了许多关于 SO 的解释以及关于协变和逆变的博客,非常感谢Eric Lippert制作了这么多关于变和逆变的系列。

但是,我有一个更具体的问题,我想稍微了解一下。

据我了解,Eric 的解释是 Covariance 和 Contravariance 都是描述转换的形容词。协变变换是保留类型顺序的变换,而逆变变换是反转它的变换。

我以我认为大多数开发人员直观理解的方式理解协方差。

//covariant operation
Animal someAnimal = new Giraffe(); 
//assume returns Mammal, also covariant operation
someAnimal = Mammal.GetSomeMammal(); 

这里的返回操作是协变的,因为我们保留了两个 Animal 仍然大于 Mammal 或 Giraffe 的大小。关于这一点,大多数返回操作都是协变的,逆变操作没有意义。

  //if return operations were contravariant
  //the following would be illegal
  //as Mammal would need to be stored in something
  //equal to or less derived than Mammal
  //which would mean that Animal is now less than or equal than Mammal
  //therefore reversing the relationship
  Animal someAnimal =  Mammal.GetSomeMammal(); 

对于大多数开发人员来说,这段代码当然没有意义。

我的困惑在于逆变参数参数。如果你有一个方法,比如

bool Compare(Mammal mammal1, Mammal mammal2);

我一直都知道输入参数总是强制逆变行为。这样,如果将类型用作输入参数,则其行为应该是逆变的。

但是下面的代码有什么区别

Mammal mammal1 = new Giraffe(); //covariant
Mammal mammal2 = new Dolphin(); //covariant

Compare(mammal1, mammal2); //covariant or contravariant?
//or
Compare(new Giraffe(), new Dolphin()); //covariant or contravariant?

出于同样的原因,你不能做这样的事情你不能做

   //not valid
   Mammal mammal1 = new Animal();

   //not valid
   Compare(new Animal(), new Dolphin());

我想我要问的是,是什么让方法参数通过逆变转换。

对不起,很长的帖子,也许我理解错了。

编辑:

根据下面的一些对话,我了解例如使用委托层可以清楚地显示逆变性。考虑以下示例

//legal, covariance
Mammal someMammal = new Mammal();
Animal someAnimal = someMammal;

// legal in C# 4.0, covariance (because defined in Interface)
IEnumerable<Mammal> mammalList = Enumerable.Empty<Mammal>();
IEnumerable<Animal> animalList = mammalList;

//because of this, one would assume
//that the following line is legal as well

void ProcessMammal(Mammal someMammal);

Action<Mammal> processMethod = ProcessMammal;
Action<Animal> someAction = processMethod;

当然这是非法的,因为有人可以将任何 Animal 传递给 someAction,而 ProcessMammal 期望任何是 Mammal 或更具体(小于 Mammal)的东西。这就是为什么 someAction 只能是 Action 或更具体的 (Action)

然而,这在中间引入了一层代表,为了实现逆变投影,是否必须在中间有一个代表?如果我们将 Process 定义为一个接口,我们会将参数参数声明为逆变类型,只是因为我们不希望有人能够做我上面展示的委托?

public interface IProcess<out T>
{
    void Process(T val);
}
4

5 回答 5

28

更新:哎呀。事实证明,我在最初的回答中混淆了方差和“作业兼容性”。相应地编辑了答案。我还写了一篇博文,希望能更好地回答这些问题:协方差和逆变常见问题解答

回答:我猜你的第一个问题的答案是在这个例子中你没有逆变:

bool Compare(Mammal mammal1, Mammal mammal2); 
Mammal mammal1 = new Giraffe(); //covariant - no             
Mammal mammal2 = new Dolphin(); //covariant - no            

Compare(mammal1, mammal2); //covariant or contravariant? - neither            
//or             
Compare(new Giraffe(), new Dolphin()); //covariant or contravariant? - neither

此外,这里甚至没有协方差。您所拥有的称为“分配兼容性”,这意味着您始终可以将派生程度更高的类型的实例分配给派生程度较低的类型的实例。

在 C# 中,数组、委托和泛型接口支持变体。正如 Eric Lippert 在他的博客文章中所说,协方差和赋值兼容性有什么区别?是最好将方差视为类型的“投影”。

协方差更容易理解,因为它遵循赋值兼容性规则(派生程度更高的数组可以分配给派生程度较低的数组,“object[] objs = new string[10];”)。逆变反转了这些规则。例如,假设您可以执行“string[] strings = new object[10];”之类的操作。当然,由于显而易见的原因,您不能这样做。但这将是逆变(但同样,数组不是逆变的,它们仅支持协变)。

以下是 MSDN 中的示例,我希望能向您展示逆变的真正含义(我现在拥有这些文档,因此如果您认为文档中的某些内容不清楚,请随时给我反馈):

  1. 在泛型集合的接口中使用方差

    Employee[] employees = new Employee[3];
    // You can pass PersonComparer, 
    // which implements IEqualityComparer<Person>,
    // although the method expects IEqualityComparer<Employee>.
    IEnumerable<Employee> noduplicates =
        employees.Distinct<Employee>(new PersonComparer());
    
  2. 在委托中使用差异

    // Event hander that accepts a parameter of the EventArgs type.
    private void MultiHandler(object sender, System.EventArgs e)
    {
       label1.Text = System.DateTime.Now.ToString();
    }
    public Form1()
    {
        InitializeComponent();
        // You can use a method that has an EventArgs parameter,
        // although the event expects the KeyEventArgs parameter.
        this.button1.KeyDown += this.MultiHandler;
        // You can use the same method 
        // for an event that expects the MouseEventArgs parameter.
        this.button1.MouseClick += this.MultiHandler;
     }
    
  3. 对 Func 和 Action Generic Delegates 使用 Variance

     static void AddToContacts(Person person)
     {
       // This method adds a Person object
       // to a contact list.
     }
    
     // The Action delegate expects 
     // a method that has an Employee parameter,
     // but you can assign it a method that has a Person parameter
     // because Employee derives from Person.
     Action<Employee> addEmployeeToContacts = AddToContacts;
    

希望这可以帮助。

于 2009-12-30T00:15:04.023 回答
16

协变和逆变不是实例化类时可以观察到的东西。因此,在查看简单的类实例化时谈论其中一个是错误的,例如在您的示例中: Animal someAnimal = new Giraffe(); //covariant operation

这些术语不对操作进行分类。术语协方差、逆变和不变性描述了类的某些方面与其子类之间的关系。

协方差
表示一个方面的变化类似于继承的方向。
逆变
表示一个方面的变化与继承的方向相反。
不变性
意味着一个方面不会从一个类变为它的子类。

在谈到 Cov.、Contrav. 时,我们一般会考虑以下几个方面。和投资:

  • 方法
    • 参数类型
    • 返回类型
    • 其他与签名相关的方面,如抛出的异常。
  • 泛型

让我们看几个例子来更好地理解这些术语。

class T
class T2 extends T
 
//Covariance: The return types of the method "method" have the same
//direction of inheritance as the classes A and B.
class A { T method() }
class B extends A { T2 method() }
 
//Contravariance: The parameter types of the method "method" have a
//direction of inheritance opposite to the one of the classes A and B.
class A { method(T2 t) }
class B { method(T t) }
在这两种情况下,“方法”都会被覆盖!此外,上述示例是Cov 的唯一合法事件。和反差。在面向对象的语言中。:

  • 协方差 - 返回类型和异常抛出语句
  • 逆变 - 输入参数
  • 不变性 - 输入和输出参数

让我们看一些反例来更好地理解上面的列表:

//Covariance of return types: OK
class Monkey { Monkey clone() }
class Human extends Monkey { Human clone() }
 
Monkey m = new Human();
Monkey m2 = m.clone(); //You get a Human instance, which is ok,
                       //since a Human is-a Monkey.
 
//Contravariance of return types: NOT OK
class Fruit
class Orange extends Fruit
 
class KitchenRobot { Orange make() }
class Mixer extends KitchenRobot { Fruit make() }
 
KitchenRobot kr = new Mixer();
Orange o = kr.make(); //Orange expected, but got a fruit (too general!)
 
//Contravariance of parameter types: OK
class Food
class FastFood extends Food
 
class Person { eat(FastFood food) }
class FatPerson extends Person { eat(Food food) }
 
Person p = new FatPerson();
p.eat(new FastFood()); //No problem: FastFood is-a Food, which FatPerson eats.
 
//Covariance of parameter types: NOT OK
class Person { eat(Food food) }
class FatPerson extends Person { eat(FastFood food) }
 
Person p = new FatPerson();
p.eat(new Food()); //Oops! FastFood expected, but got Food (too general).

这个话题太复杂了,我可以持续很长时间。我建议你检查 Cov。和反差。自己的泛型。此外,您需要了解动态绑定的工作原理才能完全理解示例(准确调用了哪些方法)。

这些术语源自 Liskov 替换原则,该原则定义了将数据类型建模为另一个数据类型的子类型的必要标准。您可能还想调查它。

于 2009-12-30T03:02:27.573 回答
10

我的理解是,协变/反变的不是子类型关系,而是这些类型(例如委托和泛型)之间的操作(或投影)。所以:

Animal someAnimal = new Giraffe();

不是协变的,而是这只是分配兼容性,因为 Giraffe 类型“小于”Animal 类型。当您在这些类型之间有一些投影时,协方差成为一个问题,例如:

IEnumerable<Giraffe> giraffes = new[] { new Giraffe() };
IEnumerable<Animal> animals = giraffes;

这在 C#3 中无效,但它应该是可能的,因为长颈鹿序列是动物序列。投影T -> IEnumerable<T>保留了类型关系的“方向”,因为Giraffe < AnimalIEnumerable<Giraffe> < IEnumerable<Animal>(请注意,赋值要求左侧的类型至少与右侧一样宽)。

逆变换反转类型关系:

Action<Animal> printAnimal = a => {System.Console.WriteLine(a.Name)};
Action<Giraffe> printGiraffe = printAnimal;

这在 C#3 中也是不合法的,但它应该是合法的,因为任何采取动物的行动都可以应对被传递给长颈鹿的情况。但是,因为Giraffe < AnimalAction<Animal> < Action<Giraffe>投影已经颠倒了类型关系。这在 C#4 中是合法的。

因此,要回答您示例中的问题:

//the following are neither covariant or contravariant - since there is no projection this is just assignment compatibility
Mammal mammal1 = new Giraffe();
Mammal mammal2 = new Dolphin();

//compare is contravariant with respect to its arguments - 
//the delegate assignment is legal in C#4 but not in C#3
Func<Mammal, Mammal, bool> compare = (m1, m2) => //whatever
Func<Giraffe, Dolphin, bool> c2 = compare;

//always invalid - right hand side must be smaller or equal to left hand side
Mammal mammal1 = new Animal();

//not valid for same reason - animal cannot be assigned to Mammal
Compare(new Animal(), new Dolphin());
于 2009-12-26T15:15:00.843 回答
2

这样看:如果我有一个处理 Subtype Mammal 的函数 func,形式为Mammal m = Func(g(Mammal)),我可以将 Mammal 换成包含 Mammal的东西,这里是 Base Animal

就理解下图的运动类比而言,您可以像在板球中那样赤手接球,但也可以(并且更容易)使用棒球手套接球。

你在左边看到的是协方差,你在参数部分看到的是逆变。

在此处输入图像描述

您可能想知道“为什么左边的绿色曲线比红色曲线大?通常比基本类型做得更多的子类型不应该更大吗?” 答案:不。括号的大小表示允许的对象的种类,如维恩图。一组哺乳动物小于一组动物。同样,f(Mammal) 比 f(Animal) 小,因为它只支持较小的对象集。(即处理哺乳动物的函数不会处理所有动物,但处理动物的函数总是可以处理哺乳动物)。因此,这种关系是倒置的,因为可以传入 f(animal) 而不是 f(mammal),从而使其具有逆变性。

于 2017-06-01T16:05:05.743 回答
1

(根据评论编辑)

这篇关于该主题的 MSDN 文章描述了协变和逆变,因为它适用于将函数与委托进行匹配。委托类型的变量:

public delegate bool Compare(Giraffe giraffe, Dolphin dolphin);

可以(因为逆变)填充函数:

public bool Compare(Mammal mammal1, Mammal mammal2)
{
    return String.Compare(mammal1.Name, mammal2.Name) == 0;
}

根据我的阅读,它与直接调用函数无关,而是将函数与委托匹配。我不确定它是否可以归结为您演示的级别,单个变量或对象分配是逆变的或协变的。但是根据链接的文章,委托的分配以对我有意义的方式使用逆变或协变。因为委托的签名包含比实际实例更多的派生类型,这被称为“逆变”,与“协变”不同,其中委托的返回类型比实际实例的派生更少。

于 2009-12-26T13:56:05.030 回答