协变,逆变与不变

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

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

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

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

协变逆变与泛型(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. git 报错及解决

    报错:fatal: refusing to merge unrelated histories==== 解决办法:git pull加上参数,如:git pull –allow-unrelated-hi ...

  2. 因环境变量设置问题引起的command not found

    ls command not found vi command not found 只要的原因是因为环境变量的问题,编辑profile文件没有写正确,导致在命令行下 ls等命令不能够识别.解决办法:在 ...

  3. Java Web Service 学习笔记

    一.服务端 1. 创建Java工程 2. 创建接口HostipalServiceInterface package ws_server; import javax.jws.WebMethod; imp ...

  4. Microsoft Dynamics CRM 2011 面向Internet部署 (IFD) CRM 登录出现会话超时的解决办法

    一.IFD 登录的时候,过了一段时间,会马上出现“您的会话已过期”,怎么解决这个问题呢,可以通过改变这个时间.具体图如二 Link to Dynamics CRM Wiki Home Page 二.S ...

  5. gcc gdb调试 & 命令行带参 (一) ******

    用GDB调试程序 GDB概述———— GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具.或许,各位比较喜欢那种图形界面方式的,像VC.BCB等IDE的调试,但如果你是在UNIX平台下做软 ...

  6. 服务端REST与SOAP的探讨(转)

    声明: 闲来逛论坛看到一篇不错的文章,阅读后受益匪浅. 本文从一个简单的应用场景出发,使用REST和SOAP两种不同的架构风格实现,通过对REST与SOAP Web服务具体对比,旨在帮助读者更深刻理解 ...

  7. Qt 常用类——QStandardItemModel

    转载:落叶知秋时 类QabstractItemModel,QabstractListModel,QAbstractTableModel不保存数据,用户需要从这些类派生出子类,并在子类中定义某种数据结构 ...

  8. 导入wordpress数据库到mysql报错

    mysql字符集编码错误的导入数据会提示错误了,这个和插入数据一样如果保存的数据与mysql编码不一样那么肯定会出现导入乱码或插入数据丢失的问题,下面我们一起来看一个例子. 恢复数据库报错:由于字符集 ...

  9. javascript事件处理程序的3个阶段

    第一阶段:HTML事件处理阶段.就是在元素里面添加onclick之类的属性来调用某个函数. <input type="button" value="单击" ...

  10. Apache Doris通过supervisor进行进程管理

    下面一段文字是摘自doris官方文档:注:在生产环境中,所有实例都应使用守护进程启动,以保证进程退出后,会被自动拉起,如 Supervisor.如需使用守护进程启动,需要修改各个 start_xx.s ...