协变,逆变与不变

能在使用父类型的场景中改用子类型的被称为协变。

能在使用子类型的场景中改用父类型的被称为逆变。

不能做到以上两点的被称为不变。

以上的场景通常包括数组,继承和泛型。

协变逆变与泛型(C#,Java)

在C#中,泛型参数的类型缺省是不变的,但是我们可以在定义泛型接口或委托时通过给参数类型加上out或in来标注该参数类型是协变还是逆变。

  • 协变意味着你能把 IEnumerable<string> 用在需要 IEnumerable<object> 的地方。

    这里 IEnumerable<out T> 是协变,使用 out 标注。使用 out 标注的原因是协变参数一般处于输出(output)者的地位,只读不写(read,get)。
  • 逆变意味着你能把 IComparable<object> 用在需要 IComparable<string> 的地方。

    这里 IComparable<in T> 是逆变,使用 in 标注。使用 in 标注的原因是逆变一般处于输入(input)者的地位,只写不读(write,put)。
  • Covariance and contravariance real world example
public interface IEnumerator<out T>
{
T Current { get; } // 输出(output)者,只读不写
bool MoveNext();
void Reset();
}
IEnumerator<string> strEnum = new Enumerator<string>();
IEnumerator<object> objEnum = strEnum; public interface IComparer<in T>
{
int Compare(T a, T b); // 输入(input)者,只写不读
}
IComparer<object> objComp = new Comparer<object>();
IComparer<string> strComp = objComp;

在Java中,泛型参数的类型是不变的。但是我们可以在使用泛型类或接口时在参数类型的位置上使用通配符加上extends或super来指定该参数类型是协变还是逆变。

  • List<? extends T> 是协变(参数类型只能使用T及其子类型,T是上限),用于生产者(指函数的入口参数),只读不写。
  • List<? super T> 是逆变(参数类型只能使用T及其父类型,T是下限),用于消费者(指函数的出口参数或返回值),只写不读。
  • Difference between <? super T> and <? extends T> in Java

泛型中协变逆变的类型安全性

  • 协变泛型参数是生产者,处于输出者地位,只读不写。

    协变泛型参数使用子类型代替父类型是类型安全的,这是因为在读取时子类对象可以被看做父类对象。
  • 逆变泛型参数是消费者,处于输入者地位,只写不读。

    逆变泛型参数使用父类型代替子类型是类型安全的。这是因为在写入时子类对象可以被安全的转换成父类对象。
协变泛型参数类型 逆变泛型参数类型
入口 函数 出口
输出者 输入者
子类 => 父类 ======> 子类 <= 父类
public class Collections {
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++)
dest.set(i, src.get(i));
}
}

协变与数组(C#,Java)

在C#和Java中,数组都是协变的。子类数组是父类数组的子类型,因而在调用参数是父类数组的函数时能够传入子类数组。

  • 在C#中,string[] 是 object[] 的子类型。
  • 在Java中,String[] 是 Object[] 的子类型。
  • 协变数组类型是类型不安全的,处理不当会抛出异常。
  • 协变数组类型虽然类型不安全,但是数组类型能够协变是一个合理的选择。

    这是因为早期C#和Java都缺乏泛型,如果数组不允许协变,将无法使用多态。
  • Why are arrays covariant but generics are invariant?

协变逆变与继承

C++和Java支持协变返回值类型(covariant return type),也就是允许基类虚函数(方法)在子类中被覆盖(重写)时,子类中相应虚函数(方法)的返回值类型不同于基类虚函数(方法)的返回值类型,条件是前者是后者的子类型。C#不支持这一特性。

class VehicleFactory {
public:
virtual Vehicle * create() const { return new Vehicle(); }
virtual ~VehicleFactory() {}
}; class CarFactory : public VehicleFactory {
public:
virtual Car * create() const override { return new Car(); } // 协变返回值类型
// 子类虚函数create的返回值类型Car *是基类虚函数create的返回值类型Vehicle *的子类型。
};

某些语言支持逆变(协变)参数类型(contravariant/covariant argument type),也就是允许基类虚函数(方法)在子类中被覆盖(重写)时,子类中相应虚函数(方法)的参数类型不同于基类虚函数(方法)的参数类型,条件是前者是后者的父(子)类型。C++,C#以及Java均不支持逆变或协变参数类型。

继承中协变逆变的类型安全性

  • 在子类中使用子类型返回值类型代替父类中的父类型返回值类型(协变返回值类型)是类型安全的。
  • 在子类中使用父类型参数类型代替父类中的子类型参数类型(逆变参数类型)是类型安全的。
  • 在子类中使用子类型参数类型代替父类中的父类型参数类型(协变参数类型)是类型不安全的。
逆变参数类型 协变返回值类型
参数 函数 返回值
父类 => 子类 ======> 父类 <= 子类

协变逆变,父子类型关系以及类型转换(转型)

协变意味着能在使用父类型的场景中改用子类型,也就是在新场景中使用子类型后仍然得到子类型。

逆变意味着能在使用子类型的场景中改用父类型,也就是在新场景中使用父类型后却能得到子类型。

下面改用符号语言:

S是T的子类型(S能够被安全的转型成T),记作 S <: T。

协变:在新场景 F 中,父子类型的关系被维持,即 F(S) <: F(T)。

逆变:在新场景 F 中,父子类型的关系被逆转,即 F(T) <: F(S)。

不变:在新场景 F 中,父子类型的关系不存在,即 F(S) 与 F(T) 两者不相关。

协变逆变与函数类型

函数 f 的参数类型为A,返回值类型为B,记作 A -> B。

协变:B <: B' => A -> B <: A -> B'(协变返回值类型)

逆变:A <: A' => A' -> B <: A -> B(逆变参数类型)

函数型语言中函数类型通常满足协变返回值类型和逆变参数类型。

Can parameters be contra- or covariant in Python?

协变逆变与C++

在C++中,指针和引用都是协变的,也就是在使用父类指针和引用的场景中能够安全的使用子类指针和引用。

这是因为子类指针和引用是父类指针和引用的子类型,语法上前者能够被安全地隐式地转换成后者,

在C++中,模板的参数类型是不变的。但是标准库的某些类型支持协变和逆变。

  • shared_ptr类型支持协变,即 S <: T => shared_ptr<S> <: shared_ptr<T>。
  • unique_ptr类型支持协变,即 S <: T => unique_ptr<S> <: unique_ptr<T>。
  • 以上两者是协变的原因是以上两者都是智能指针,需要模仿原生指针的特性。
  • function的类型参数中的返回值类型支持协变,即 B <: B' => function<B(A)> <: function<B'(A)>。
  • function的类型参数中的参数类型支持逆变,即 A <: A' => function<B(A')> <: function<B(A)>。
#include <iostream>
#include <memory>
#include <functional>
using namespace std; struct Base {};
struct Derived : Base {}; int main()
{
shared_ptr<Derived> p = nullptr;
shared_ptr<Base> p2 = p; // shared_ptr类型支持协变
function<Derived*(Base*)> f = nullptr;
function<Base*(Derived*)> f2 = f; // function的函数参数类型支持逆变,function的函数返回值类型支持协变
}

Covariance and Contravariance in C++ Standard Library

协变(covariance),逆变(contravariance)与不变(invariance)的更多相关文章

  1. C# 逆变(Contravariance)/协变(Covariance) - 个人的理解

    逆变(Contravariance)/协变(Covariance) 1. 基本概念 官方: 协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始 ...

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

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

  3. (转)Scala中协变(+)、逆变(-)、上界(<:)、下界(>:)简单介绍

    看源码的时候看到: trait ExtensionId[T <: Extension] { 没见过这个符号啊<: Scala上界(<:)和下界(>:) 1) U >: T ...

  4. C#4.0新增功能03 泛型中的协变和逆变

    连载目录    [已更新最新开发文章,点击查看详细] 协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体 ...

  5. Scala 基础(十六):泛型、类型约束-上界(Upper Bounds)/下界(lower bounds)、视图界定(View bounds)、上下文界定(Context bounds)、协变、逆变和不变

    1 泛型 1)如果我们要求函数的参数可以接受任意类型.可以使用泛型,这个类型可以代表任意的数据类型. 2)例如 List,在创建 List 时,可以传入整型.字符串.浮点数等等任意类型.那是因为 Li ...

  6. 了解C#的协变和逆变

    前言 在引用类型系统时,协变.逆变和不变性具有如下定义. 这些示例假定一个名为 Base 的基类和一个名为 Derived的派生类. Covariance 使你能够使用比原始指定的类型派生程度更大的类 ...

  7. .NET C#杂谈(1):变体 - 协变、逆变与不变

    0. 文章目的:   介绍变体的概念,并介绍其对C#的意义 1. 阅读基础   了解C#进阶语言功能的使用(尤其是泛型.委托.接口) 2. 从示例入手,理解变体   变体这一概念用于描述存在继承关系的 ...

  8. [改善Java代码]警惕泛型是不能协变和逆变的

    什么叫做协变(covariance)和逆变(contravariance)? 在变成语言的类型框架中,协变和逆变是指宽类型和窄类型在某种情况下(如参数,泛型,返回值)替换或交换的特性,简单的说,协变是 ...

  9. c#4.0新特性之协变与逆变

    1.C#3.0以前的协变与逆变 如果你是第一次听说这个两个词,别担心,他们其实很常见.C#4.0中的协变与逆变[1](Covariance and contravariance)有了进一步的完善,主要 ...

  10. 详解C#的协变和逆变

    一.使用协变(Covariance)和逆变(Contravariance )能够实现数组之间.委托实例和方法之间.泛型委托实例之间.泛型接口的变量和泛型类型的对象之间.泛型接口的变量之间的隐式转换:使 ...

随机推荐

  1. Dynamics CRM 2011 怎么根据记录的etc参数值找到实体英文名和根据etc参数值或英文名称找到其实体中文名称

    一.平常我们可以打开CRM2011一条已创建的记录,通过JScript方法获取实体英文名的方法是:按F12,输入contentIFrame.Xrm.Page.data.entity.getEntity ...

  2. 带你走进Linux(Ubuntu)

    类Unix系统目录结构 ubuntu没有盘符这个概念,只有一个根目录/,所有文件都在它下面 /:根目录,一般根目录下只存放目录,在Linux下有且只有一个根目录.所有的东西都是从这里开始.当你在终端里 ...

  3. java 网络编程TCP

    客户端 服务端

  4. bzoj4419 发微博

    Description 刚开通的SH微博共有n个用户(1..n标号),在短短一个月的时间内,用户们活动频繁,共有m条按时间顺序的记录: ! x   表示用户x发了一条微博: + x y 表示用户x和用 ...

  5. Apache2.4.7 + php5 + mysql thinkphp

    1. LAMP 的安装sudo apt-get install apache2 2.安装PHP sudo apt-get install  libapache2-mod-php5 php5 php5- ...

  6. LNMP中常见的502错误及处理方法

    LNMP配置完成以后,经常遇到502 Bad Gateway的错误提示,究其原因多为2种.下面对这两方面的问题进行分析: 1. 配置方面的错误 配置错误中,或者因为php-fpm找不到路径,或者是权限 ...

  7. 【转载】Vmware Vconverter从物理机迁移系统到虚拟机P2V

    本文完整记录了如何从物理服务器,保持所有环境配置信息,纹丝不动的迁移到虚拟机上,俗称 P2V .采用的工具是VMware公司的 VMware vcenter vconverter standalone ...

  8. 1116 Come on! Let's C (20 分)

    1116 Come on! Let's C (20 分) "Let's C" is a popular and fun programming contest hosted by ...

  9. SpringBoot学习记(一)第一个SpringBoot Web服务

    工具IDEA 一.构建项目 1.选择Spring Initializr 2.填写项目信息 3.勾选webService 4.勾选Thymeleaf 5.项目建立完成,启动类自动生成 二.写个Contr ...

  10. pandas的map函数与apply函数的区别

    import pandas as pd import numpy as np df = pd.DataFrame(np.random.randn(4,3),columns=list("ABC ...