Typescript协变与逆变简单理解

目录
  • 1. 协变和逆变简单理解
  • 2. 协变举例
  • 3. 逆变举例
  • 4. 更简单点的理解
  • 5. 参考

1. 协变和逆变简单理解

先简单说下协变和逆变的理解。

首先,无论协变还是逆变,必然是存在于有继承关系的类当中,这个应该好理解吧。如果你只有一个类,那没有什么好变的。

其次,无论协变还是逆变,既然是变,那必然是存在不同类之间的对象的赋值,比如子类对象赋值给父类对象,父类对象赋值给子类对象,这样才叫做变。

结合上面两条,我觉得协变和逆变在我的字典中就能定义成:支持子类对象赋值给父类对象的情况称之为协变;反之,支持父类对象赋值给子类对象的情况称之为逆变。

举个栗子,我们先假定我们有这么几个类

class Animal {}
class Dog extends Animal {}
class Greyhound extends Dog {}

那么按照上面的理解,要整出一个示例的话,首先我们这里类的继承关系这个条件有了,其次我们要整出的就是这几个类赋值的情况,那么用实参和形参的方式来demo应该是很不错的选择。

2. 协变举例

那么协变的情况我们可以用代码表示为

class Animal {}
class Dog extends Animal {
    bark(): void {
        console.log("Bark")
    }
}
class Greyhound extends Dog {}
function makeDogBark(dog:Dog) : void {
    dog.bark()
}
let dog: Dog = new Dog();
let greyhound: Greyhound = new Greyhound();
let animal: Animal = new Animal();
makeDogBark(greyhound) // OK。 子类赋值给父类
makeDogBark(animal) // Error。编译器会报错,父类不能赋值给子类

我们如果有面向对象基础的话,相信对上面这段代码不难理解, 子类赋值给父类,即协变的情况,在面向对象编程中是非常常见的,且这是实现语言多态特性的基础。而多态,却又是实现众多设计模式的基础。

3. 逆变举例

当我们将函数作为参数进行传递时,就需要注意逆变的情况。比如下面的makeAnimalAction这个函数,就尝试错误的让一只猫去做出狗吠的动作。

class Animal {
    doAnimalThing(): void {
        console.log("I am a Animal!")
    }
}
class Dog extends Animal {
    doDogThing(): void {
        console.log("I am a Dog!")
    }
}
class Cat extends Animal {
    doCatThing(): void {
        console.log("I am a Cat!")
    }
}
function makeAnimalAction(animalAction: (animal: Animal) => void) : void {
    let cat: Cat = new Cat()
    animalAction(cat)
}
function dogAction(dog: Dog) {
    dog.doDogThing()
}
makeAnimalAction(dogAction) // TS Error at compilation, since we are trying to use `doDogThing()` to a `Cat`

这里作为实参的dogAction函数接受一个Dog类型的参数,而makeAnimalAction的形参animalAction接受一个Dog的父类Animal类型的参数,返回值都是void,那么按照正常的思路,这时应该可以像上面协变的例子一样进行正常的赋值的。

但事实上编译是不能通过的,因为最终makeAnimalAction中的代码会尝试以cat为参数去调用dogAction,然后让一个cat去执行doDogThing。

所以这里我们把函数作为参数传递时,如果该函数里面的参数牵涉到有继承关系的类,就要特别注意下逆变情况的发生。

不过有vscode等代码编辑工具的错误提示支持的话,应该也很容易排除这种错误。

4. 更简单点的理解

我觉得将上面的例子稍微改动下,将makeAnimalAction的形参的类型抽出来定义成一个type,应该会有助于我们理解上面的代码。

class Animal {
    doAnimalThing(): void {
        console.log("I am a Animal!")
    }
}
class Dog extends Animal {
    doDogThing(): void {
        console.log("I am a Dog!")
    }
}
class Cat extends Animal {
    doCatThing(): void {
        console.log("I am a Cat!")
    }
}
function makeAnimalAction(animalAction: AnimalAction) : void {
    let cat: Cat = new Cat()
    animalAction(cat)
}
type AnimalAction =  (animal: Animal) => void
type DogAction =  (dog: Dog) => void
let dogAction: DogAction = (dog: Dog) => {
    dog.doDogThing()
}
const animalAction: AnimalAction = dogAction // Error: 和上面一样的逆变导致的错误
makeAnimalAction(animalAction)
  • animalAction(animal: Animal)函数,我们可以将其理解成一个可以让动物做动物都有的动作的函数。因此我们可以传dog、cat或者animal进去作为参数,因为它们都是动物,然后animalAction内部可以调用animal.doAnimalThing方法,但不能调用doCatThing或者doDogThing这些方法,因为这些不是所有动物共有的方法。
  • dogAction(dog: Dog)函数, 同上,我们可以将其理解成一个可以让狗狗做狗狗都有的动作的函数。因此可传dog,greyHound这些狗狗对象作为参数,因为对他们都是狗狗,然后dogAction内部可以调用dog.doDogThing和dog.doAnimalThing, 因为这些都是狗狗共有的动作。但是不能调用dog.doGrenHoundThing,因为这不是狗狗共有的动作,只有狗狗的子类灰狗用欧这样的函数。

以上两个都是协变的情况。下面我们看下逆变所导致的错误那一行。

animalAction = dogAction,如果有C/C++经验的,就可以理解成一个函数指,指向另外一个函数,否则理解成一个函数复制给另外一个函数也可以。

假如这个语句可以执行,那么执行之前,dogAction(dog: Dog)只能接受Dog和GreyHound类型的对象,然后去做狗狗都有的动作。

执行之后,因为现在animalAction指向了dogAction,但是animalAction自身的参数是(animal: Animal),即可以接受所有动物类型的对象。

所以最终这里animalAction就变成了这幅模样(隐隐约约觉得这是理解的关键):

function animalAction(animal: Animal) {
 animal.doDogThing()
}

这很明显就是不合理的嘛!所有狗狗都是动物,但这里反过来就不行,不是所有动物都能做狗狗能做的事情,比如这里传个Cat对象进来,那岂不就是让猫去做狗狗的事情了吗。

而反过来,这里假如我们先定义了animalAction, 然后我们让dogAction = animalAction,这种做法却是可行的。我们看最终dogAction变成

function dogAction(dog: Dog) {
 dog.doAnimalThing()
}

即dogAction(dog:Dog)指向了animalAction(animal: Animal), 也就是一个以父类型的对象为参数的函数赋予给了一个以子类型的对象为参数的函数,这和我们协变时候的对象之间的赋值时,只能子对象赋值给父对象的做法是相反的。我想,这应该也是为什么叫做逆变的原因吧。

本来这里在我头脑过的时候感觉应该很容易说清楚的,没有想到写下来的时候还是得写这么一大堆,希望能有帮助吧。

5. 参考

https://dev.to/codeozz/how-i-understand-covariance-contravariance-in-typescript-2766

到此这篇关于Typescript协变与逆变简单理解的文章就介绍到这了,更多相关Typescript协变与逆变内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • typescript常见高级技巧总结

    用了一段时间的 typescript 之后,深感中大型项目中 typescript 的必要性,它能够提前在编译期避免许多 bug,如很恶心的拼写问题.而越来越多的 package 也开始使用 ts,学习 ts 已是势在必行. 以下是我在工作中总结到的比较实用的 typescript 技巧. 01 keyof keyof 与 Object.keys 略有相似,只不过 keyof 取 interface 的键. interface Point { x: number; y: number; } //

  • 关于TypeScript声明合并

    目录 介绍 基础概念 合并接口 合并命名空间 命名空间与类和函数和枚举类型合并 合并命名空间和类 非法的合并 模块扩展 全局扩展 介绍 TypeScript中有些独特的概念可以在类型层面上描述JavaScript对象的模型. 这其中尤其独特的一个例子是“声明合并”的概念. 理解了这个概念,将有助于操作现有的JavaScript代码. 同时,也会有助于理解更多高级抽象的概念. 对本文件来讲,“声明合并”是指编译器将针对同一个名字的两个独立声明合并为单一声明. 合并后的声明同时拥有原先两个声明的特性

  • Typescript协变与逆变简单理解

    目录 1. 协变和逆变简单理解 2. 协变举例 3. 逆变举例 4. 更简单点的理解 5. 参考 1. 协变和逆变简单理解 先简单说下协变和逆变的理解. 首先,无论协变还是逆变,必然是存在于有继承关系的类当中,这个应该好理解吧.如果你只有一个类,那没有什么好变的. 其次,无论协变还是逆变,既然是变,那必然是存在不同类之间的对象的赋值,比如子类对象赋值给父类对象,父类对象赋值给子类对象,这样才叫做变. 结合上面两条,我觉得协变和逆变在我的字典中就能定义成:支持子类对象赋值给父类对象的情况称之为协变

  • 详解c# 协变和逆变

    基本概念 协变:能够使用比原始指定的派生类型的派生程度更大(更具体)的类型.例如 IFoo<父类> = IFoo<子类> 逆变:能够使用比原始指定的派生类型的派生程度更新(更抽象)的类型.例如 IBar<子类> = IBar<父类> 关键字out和in 协变和逆变在泛型参数中的表现方式,out关键字表示协变,in关键字表示逆变.二者只能在泛型接口或者委托中使用. 理解协变和逆变 看完上面的定义是不是一脸懵逼~~~.看不懂就对了,且定义语句的歧义性很大.让我们

  • 基于.Net中的协变与逆变的深入分析

    关于协变和逆变要从面向对象继承说起.继承关系是指子类和父类之间的关系:子类从父类继承所以子类的实例也就是父类的实例.比如说Animal是父类,Dog是从Animal继承的子类:如果一个对象的类型是Dog,那么他必然是Animal.协变逆变正是利用继承关系 对不同参数类型或返回值类型 的委托或者泛型接口之间做转变.我承认这句话很绕,如果你也觉得绕不妨往下看看.如果一个方法要接受Dog参数,那么另一个接受Animal参数的方法肯定也可以接受这个方法的参数,这是Animal向Dog方向的转变是逆变.如

  • C#4.0新特性之协变与逆变实例分析

    本文实例讲述了C#4.0新特性的协变与逆变,有助于大家进一步掌握C#4.0程序设计.具体分析如下: 一.C#3.0以前的协变与逆变 如果你是第一次听说这个两个词,别担心,他们其实很常见.C#4.0中的协变与逆变(Covariance and contravariance)有了进一步的完善,主要是两种运行时的(隐式)泛型类型参数转换.简单来讲,所谓协变(Covariance)是指把类型从"小"升到"大",比如从子类升级到父类:逆变则是指从"大"变到

  • C#中的协变与逆变深入讲解

    什么是协变与逆变 MSDN的解释: https://msdn.microsoft.com/zh-cn/library/dd799517.aspx 协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型. 泛型类型参数支持协变和逆变,可在分配和使用泛型类型方面提供更大的灵活性. 一开始我总是分不清协变和逆变,因为MSDN的解释实在是严谨有余而易读不足. 其实从中文的字面上来理解这两个概念就挺容易的

  • 一篇文章看懂C#中的协变、逆变

    1. 基本概念 官方:协变和逆变都是术语,前者指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,后者指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型.[MSDN] 公式: 协变:IFoo<父类> = IFoo<子类>: 逆变:IBar<子类> =  IBar<父类>: 暂时不理解没关系,您接着往下看. 2. 协变(Covariance) 1) out关键字 对于泛型类型参数,out 关键字可指定类型参数是协变的. 可以在泛型接口

  • C#中的协变与逆变小结

    一:什么是协变与逆变 协变指能够使用比原始指定的派生类型的派生程度更大(更具体的)的类型,逆变指能够使用比原始指定的派生类型的派生程度更小(不太具体的)的类型 只有泛型接口和泛型委托参数支持协变和逆变 二:引言 using System; using System.Collections.Generic; class MainClass { static void Main() { object o = "str"; List<object> oList = new Lis

  • 图文详解C#中的协变与逆变

    目录 前言 协变和逆变 总结 前言 这篇文章简单说说C#中的协变和逆变. 在C#编程中,由于存在类型之间的强制转换,很容易会出现所谓的类型可变性说法,存在协变.逆变.不变三种. 就比如前一篇文章介绍的泛型概念,如果创建了泛型类型的实例,编译器会接受泛型类型声明以及类型参数来创建构造类型.但是在日常使用过程中,我们可能会将派生类型分配给基类型的变量,有时候会出现错误. 这里就存在一个赋值兼容性问题. 每一个变量都有一种类型,可以将派生类对象的实例赋值给基类变量(好比之前子类声明的变量可以赋值给父类

  • c#协变和逆变实例分析

    本文实例讲述了c#协变和逆变的原理及应用.分享给大家供大家参考.具体如下: 由子类向父类方向转变是协变,用out关键字标识,由父类向子类方向转变是逆变,用in关键字 协变和逆变的应用   一. 数组的协变 复制代码 代码如下: Animal[] animalArray = new Dog[]{}; 说明:声明的数组数据类型是Animal,而实际上赋值时给的是Dog数组:每一个Dog对象都可以安全的转变为Animal.Dog向Animal方法转变是沿着继承链向上转变的所以是协变   二. 委托中的

  • Kotlin中协变、逆变和不变示例详解

    前言 Kotlin 泛型的基本语法类似于 Java ,不过出于型变安全,不支持 Java 中的<? extends T>,<?super T> 通配符型变约束,而是采用类似 C# 的 in,out 用于支持协变和逆变,这同时避免了处理子类型化,父类化的问题(即Java泛型中典型的List<T> 不是 List<Object>的子类型的问题): 基本的泛型语法可以参考官方中文文档:https://www.kotlincn.net/docs/reference/

随机推荐