逆变(Contravariance)/协变(Covariance)

1. 基本概念

官方: 协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型。协变/逆变只支持委托、泛型接口。数组由于历史的原因也支持协变/逆变。在 C# 中,协变和逆变能够实现数组类型、委托类型和泛型类型参数的隐式引用转换。 协变保留分配兼容性,逆变则与之相反。协变允许方法具有的返回类型比接口的泛型类型参数定义的返回类型的派生程度更大只有引用类型才支持使用泛型接口中的变体。

协变:IFoo<父类> = IFoo<子类>;interface IFoo<out T>{}

逆变:IBar<子类> =  IBar<父类>;interface IBar<in T>{}

以下代码演示分配兼容性、协变和逆变之间的差异。

string str = "test";
derived type.
object obj = str; // Covariance.
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings;

//static void SetObject(object o) { }
Action<object> actObject = SetObject;
Action<string> actString = actObject;

2、下面我谈谈个人对协变和逆变的理解

 “协变”相当于隐式转换,基类集合=派生类集合等于让基类集合指针指向派生类集合,基类集合可以用子类集合中方法。对于基类集合只是不能使用基类集合中属于子类的方法,基类集合没有性能上的损失,所以可以做回返回值。

“逆变”相当于强制转换,派生类集合=基类集合, 等于让派生类集合的指针指向基类集合,等于让派生类集合的指针指向基类集合。由于基类的功能小于派生类集合。就造成派生类可使用的方法减少,功能降低。所以不能作为返回值。

我们先来看一段代码:

List<汽车> 一群汽车 = new List<汽车>();
List<车子> 一群车子 = 一群汽车;

显然,上面那段代码是会报错的, 虽然汽车继承于车子,可以隐士转换为车子,但是List<汽车>并不继承于List<车子>,所以上面的转换,是行不通的。

IEnumerable<汽车> 一群汽车 = new List<汽车>();
IEnumerable<车子> 一群车子 = 一群汽车;

然而这样却是可以的。那么IEnumerable接口有什么不同呢,我们且看编译器的提示:

我们可以看到,泛型参数的,用了一个“out”关键字作为声明。看来,关键是这个在起作用了。我们先到这个为止,以上是抛出话题。

接下来我们看一种情况:

当尝试编译下面这个把in泛型参数用作方法返回值的泛型接口时:

public interface IPlayB<in T>
{
T Test();
}

出现了如下编译错误:

错误    1    方差无效: 类型参数“T”必须为“CovarianceAndContravariance.IPlayB<T>.Test()”上有效的 协变式。“T”为 逆变。

我们把in改成out ,就不会提示错误了,为什么会出现这种情况?

3. 自问自答

1)协变、逆变 为什么只能针对泛型接口或者委托?而不能针对泛型类?

      因为它们都只能定义方法成员(接口不能定义字段),而方法成员在创建对象的时候是不涉及到对象内存分配的,所以它们是类型(内存)安全的。

2)协变、逆变 为什么是类型安全的?

本质上是里氏替换原则,由里氏替换原则可知:派生程度小的是派生程度大的子集,所以子类替换父类的位置整个程序功能都不会发生改变。

3)官方对 协变、逆变 的定义现在是否能看懂?

      上面看懂了,官方定义肯定也是没问题的。派生程度小可以理解为基类,派生程度大可以理解为子类或派生类,至于为什么用程度这个词,是因为继承链的深度是没限制的。

4)为什么 in 、out 只能是单向的(输入或输出)?

因为若类型参数同时为输入参数和输出参数,则必然会有一种转换不符合里氏替换原则,即将父类型的变量赋值给子类型的变量,这是不安全的所以需要明确指定 in 或 out。

4、泛型接口类型变体

4.1协变允许方法具有的返回类型比接口的泛型类型参数定义的返回类型的派生程度更大。协变中泛型类型参数只能做 返回值类型,不能做方法的参数类型

不知道啥意思

4.2逆变允许方法具有的实参类型比接口的泛型形参定义的类型的派生程度更小。

不知道啥意思

4.3只有引用类型才支持使用泛型接口中的变体。 值类型不支持变体

IEnumerable<int> integers = new List<int>();
// The following statement generates a compiler error,
.
IEnumerable<Object> objects = integers; // 这个是错误的,因为int是值类型,

 4、逆变和协变的应用

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; namespace Galaxy
{
class Program
{ static void Main(string[] args)
{
Dog aDog = new Dog();
Animal aAnimal = aDog;
List<Dog> lstDogs = new List<Dog>();
// List<Animal> lstAnimal2 = lstDogs;
IMyList<Dog> myDogs = new MyList<Dog>(); IMyList<Animal> myAnimals = myDogs; }
}
public interface IAnimal
{
public void AnimalRun(); }
public abstract class Animal: IAnimal
{
public void AnimalRun()
{ Console.WriteLine("动物再跑");
} }
public class Dog : Animal
{ }
public interface IMyList<out T> { T GetElement(); } public class MyList<T> : IMyList<T> { public T GetElement() { return default(T); } } }

5、变体

如果泛型接口或委托的泛型参数被声明为协变或逆变,该泛型接口或委托则被称为“变体”。

C# 逆变(Contravariance)/协变(Covariance) - 个人的理解的更多相关文章

  1. C#中的协变(Covariance)和逆变(Contravariance)

    摘要 ● 协变和逆变的定义是什么?给我们带来了什么便利?如何应用? ● 对于可变的泛型接口,为什么要区分成协变的和逆变的两种?只要一种不是更方便吗? ● 为什么还有不可变的泛型接口,为什么有的泛型接口 ...

  2. Java中的逆变与协变

    看下面一段代码 Number num = new Integer(1); ArrayList<Number> list = new ArrayList<Integer>(); ...

  3. Java中的逆变与协变(转)

    看下面一段代码 Number num = new Integer(1); ArrayList<Number> list = new ArrayList<Integer>(); ...

  4. Java中的逆变与协变 专题

    结论先行: PECS总结: 要从泛型类取数据时,用extends: 协变 要往泛型类写数据时,用super: 逆变 既要取又要写,就不用通配符(即extends与super都不用) 不变 List&l ...

  5. scala 学习: 逆变和协变

    scala 逆变和协变的概念网上有很多解释, 总结一句话就是 参数是逆变的或者不变的,返回值是协变的或者不变的. 但是为什么是这样的? 协变: 当s 是A的子类, 那么func(s) 是func(A) ...

  6. Scala 深入浅出实战经典 第81讲:Scala中List的构造是的类型约束逆变、协变、下界详解

    王家林亲授<DT大数据梦工厂>大数据实战视频 Scala 深入浅出实战经典(1-97讲)完整视频.PPT.代码下载:百度云盘:http://pan.baidu.com/s/1c0noOt6 ...

  7. C#4.0新特性(3):变性 Variance(逆变与协变)

    一句话总结:协变让一个粗粒度接口(或委托)可以接收一个更加具体的接口(或委托)作为参数(或返回值):逆变让一个接口(或委托)的参数类型(或返回值)类型更加具体化,也就是参数类型更强,更明确. 通常,协 ...

  8. Java 逆变与协变的名词说明

    最近在研究Thinking in Java的时候,感觉逆变与协变有点绕,特意整理一下,方便后人.我参考于Java中的逆变与协变,但是该作者整理的稍微有点过于概念化,我在这里简单的说一下 我对于协变于逆 ...

  9. C# 逆变与协变

    该文章中使用了较多的 委托delegate和Lambda表达式,如果你并不熟悉这些,请查看我的文章<委托与匿名委托>.<匿名委托与Lambda表达式>以便帮你建立完整的知识体系 ...

随机推荐

  1. golang中接口详解

    package main import ( "fmt" ) type Sayer interface { say() } type Mover interface { move() ...

  2. java多态成员变量、成员函数(非静态)、静态函数特点

    1 package face_09; 2 3 /* 4 * 多态时, 5 * 成员的特点: 6 * 1,成员变量. 7 * 编译时:参考引用型变量所属类中的是否有调用的成员变量,有,编译通过:没有,编 ...

  3. 删除文件行末尾的^M符号方法

    有时发现某些文件的末尾总是带有^M符号 cat /etc/hosts.allow|col -b > /etc/hosts.allow.old

  4. 《HelloGitHub》第 70 期

    兴趣是最好的老师,HelloGitHub 让你对编程感兴趣! 简介 HelloGitHub 分享 GitHub 上有趣.入门级的开源项目. https://github.com/521xueweiha ...

  5. 中文分词,自然语言处理器NLP。 六月份版本已上线。

    一,  没有对比,就没有伤害,我们分词的优势在哪里?走一波测试. 跑一下CaCl2,看看效果. 二   想要分什么词汇,自己自定义即可. 目前每个月都会出一个新的版本,主要是和金融相关的词汇. 这是6 ...

  6. plsql 将游标读取到table中

    -- 将游标中的数据 读取到table中 根据部门编号获得emp所有信息. declare cursor c(no emp.deptno%type)is select * from emp where ...

  7. Spring源码-IOC部分-自定义IOC容器及Bean解析注册【4】

    实验环境:spring-framework-5.0.2.jdk8.gradle4.3.1 Spring源码-IOC部分-容器简介[1] Spring源码-IOC部分-容器初始化过程[2] Spring ...

  8. NOI Online 2021 入门组 T1

    Description 题目描述 Alice.Bob 和 Cindy 三个好朋友得到了一个圆形蛋糕,他们打算分享这个蛋糕. 三个人的需求量分别为 \(a, b, c\),现在请你帮他们切蛋糕,规则如下 ...

  9. java的装箱和拆箱详解

    ========================================================================================= 在我看来,学习jav ...

  10. 简单说说ES6新特性

    ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了. 它的目标,是使得 JavaScript 语言可以用来编写复杂的大型 ...