Typescript協(xié)變與逆變簡(jiǎn)單理解
1. 協(xié)變和逆變簡(jiǎn)單理解
先簡(jiǎn)單說下協(xié)變和逆變的理解。
首先,無論協(xié)變還是逆變,必然是存在于有繼承關(guān)系的類當(dāng)中,這個(gè)應(yīng)該好理解吧。如果你只有一個(gè)類,那沒有什么好變的。
其次,無論協(xié)變還是逆變,既然是變,那必然是存在不同類之間的對(duì)象的賦值,比如子類對(duì)象賦值給父類對(duì)象,父類對(duì)象賦值給子類對(duì)象,這樣才叫做變。
結(jié)合上面兩條,我覺得協(xié)變和逆變?cè)谖业淖值渲芯湍芏x成:支持子類對(duì)象賦值給父類對(duì)象的情況稱之為協(xié)變;反之,支持父類對(duì)象賦值給子類對(duì)象的情況稱之為逆變。
舉個(gè)栗子,我們先假定我們有這么幾個(gè)類
class Animal {}
class Dog extends Animal {}
class Greyhound extends Dog {}那么按照上面的理解,要整出一個(gè)示例的話,首先我們這里類的繼承關(guān)系這個(gè)條件有了,其次我們要整出的就是這幾個(gè)類賦值的情況,那么用實(shí)參和形參的方式來demo應(yīng)該是很不錯(cuò)的選擇。
2. 協(xié)變舉例
那么協(xié)變的情況我們可以用代碼表示為
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。編譯器會(huì)報(bào)錯(cuò),父類不能賦值給子類
我們?nèi)绻忻嫦驅(qū)ο蠡A(chǔ)的話,相信對(duì)上面這段代碼不難理解, 子類賦值給父類,即協(xié)變的情況,在面向?qū)ο缶幊讨惺欠浅3R姷模疫@是實(shí)現(xiàn)語(yǔ)言多態(tài)特性的基礎(chǔ)。而多態(tài),卻又是實(shí)現(xiàn)眾多設(shè)計(jì)模式的基礎(chǔ)。
3. 逆變舉例
當(dāng)我們將函數(shù)作為參數(shù)進(jìn)行傳遞時(shí),就需要注意逆變的情況。比如下面的makeAnimalAction這個(gè)函數(shù),就嘗試錯(cuò)誤的讓一只貓去做出狗吠的動(dòng)作。
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`這里作為實(shí)參的dogAction函數(shù)接受一個(gè)Dog類型的參數(shù),而makeAnimalAction的形參animalAction接受一個(gè)Dog的父類Animal類型的參數(shù),返回值都是void,那么按照正常的思路,這時(shí)應(yīng)該可以像上面協(xié)變的例子一樣進(jìn)行正常的賦值的。
但事實(shí)上編譯是不能通過的,因?yàn)樽罱KmakeAnimalAction中的代碼會(huì)嘗試以cat為參數(shù)去調(diào)用dogAction,然后讓一個(gè)cat去執(zhí)行doDogThing。
所以這里我們把函數(shù)作為參數(shù)傳遞時(shí),如果該函數(shù)里面的參數(shù)牽涉到有繼承關(guān)系的類,就要特別注意下逆變情況的發(fā)生。
不過有vscode等代碼編輯工具的錯(cuò)誤提示支持的話,應(yīng)該也很容易排除這種錯(cuò)誤。
4. 更簡(jiǎn)單點(diǎn)的理解
我覺得將上面的例子稍微改動(dòng)下,將makeAnimalAction的形參的類型抽出來定義成一個(gè)type,應(yīng)該會(huì)有助于我們理解上面的代碼。
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: 和上面一樣的逆變導(dǎo)致的錯(cuò)誤
makeAnimalAction(animalAction)- animalAction(animal: Animal)函數(shù),我們可以將其理解成一個(gè)可以讓動(dòng)物做動(dòng)物都有的動(dòng)作的函數(shù)。因此我們可以傳dog、cat或者animal進(jìn)去作為參數(shù),因?yàn)樗鼈兌际莿?dòng)物,然后animalAction內(nèi)部可以調(diào)用animal.doAnimalThing方法,但不能調(diào)用doCatThing或者doDogThing這些方法,因?yàn)檫@些不是所有動(dòng)物共有的方法。
- dogAction(dog: Dog)函數(shù), 同上,我們可以將其理解成一個(gè)可以讓狗狗做狗狗都有的動(dòng)作的函數(shù)。因此可傳dog,greyHound這些狗狗對(duì)象作為參數(shù),因?yàn)閷?duì)他們都是狗狗,然后dogAction內(nèi)部可以調(diào)用dog.doDogThing和dog.doAnimalThing, 因?yàn)檫@些都是狗狗共有的動(dòng)作。但是不能調(diào)用dog.doGrenHoundThing,因?yàn)檫@不是狗狗共有的動(dòng)作,只有狗狗的子類灰狗用歐這樣的函數(shù)。
以上兩個(gè)都是協(xié)變的情況。下面我們看下逆變所導(dǎo)致的錯(cuò)誤那一行。
animalAction = dogAction,如果有C/C++經(jīng)驗(yàn)的,就可以理解成一個(gè)函數(shù)指,指向另外一個(gè)函數(shù),否則理解成一個(gè)函數(shù)復(fù)制給另外一個(gè)函數(shù)也可以。
假如這個(gè)語(yǔ)句可以執(zhí)行,那么執(zhí)行之前,dogAction(dog: Dog)只能接受Dog和GreyHound類型的對(duì)象,然后去做狗狗都有的動(dòng)作。
執(zhí)行之后,因?yàn)楝F(xiàn)在animalAction指向了dogAction,但是animalAction自身的參數(shù)是(animal: Animal),即可以接受所有動(dòng)物類型的對(duì)象。
所以最終這里animalAction就變成了這幅模樣(隱隱約約覺得這是理解的關(guān)鍵):
function animalAction(animal: Animal) {
animal.doDogThing()
}
這很明顯就是不合理的嘛!所有狗狗都是動(dòng)物,但這里反過來就不行,不是所有動(dòng)物都能做狗狗能做的事情,比如這里傳個(gè)Cat對(duì)象進(jìn)來,那豈不就是讓貓去做狗狗的事情了嗎。
而反過來,這里假如我們先定義了animalAction, 然后我們讓dogAction = animalAction,這種做法卻是可行的。我們看最終dogAction變成
function dogAction(dog: Dog) {
dog.doAnimalThing()
}
即dogAction(dog:Dog)指向了animalAction(animal: Animal), 也就是一個(gè)以父類型的對(duì)象為參數(shù)的函數(shù)賦予給了一個(gè)以子類型的對(duì)象為參數(shù)的函數(shù),這和我們協(xié)變時(shí)候的對(duì)象之間的賦值時(shí),只能子對(duì)象賦值給父對(duì)象的做法是相反的。我想,這應(yīng)該也是為什么叫做逆變的原因吧。
本來這里在我頭腦過的時(shí)候感覺應(yīng)該很容易說清楚的,沒有想到寫下來的時(shí)候還是得寫這么一大堆,希望能有幫助吧。
5. 參考
https://dev.to/codeozz/how-i-understand-covariance-contravariance-in-typescript-2766
到此這篇關(guān)于Typescript協(xié)變與逆變簡(jiǎn)單理解的文章就介紹到這了,更多相關(guān)Typescript協(xié)變與逆變內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在vs2010中調(diào)試javascript代碼方法
只在IE瀏覽器中測(cè)試成功了,在谷歌瀏覽中沒有測(cè)試成功,其他瀏覽器沒有測(cè)試。2011-02-02
15 個(gè) JavaScript Web UI 庫(kù)
本文介紹了 15 個(gè)非常強(qiáng)大的 JavaScript Web UI 庫(kù),非常適合各種各種規(guī)模的富 Web 應(yīng)用的開發(fā)。2010-05-05
原生js實(shí)現(xiàn)省市區(qū)三級(jí)聯(lián)動(dòng)代碼分享
這篇文章主要介紹了原生js實(shí)現(xiàn)省市區(qū)三級(jí)聯(lián)動(dòng)功能以及代碼分享,對(duì)此有需要的朋友可以參考學(xué)習(xí)下。2018-02-02
DD_belatedPNG,IE6下PNG透明解決方案(國(guó)外)
今天介紹DD_belatedPNG,只需要一個(gè)理由,就是它支持backgrond-position與background-repeat.這是其他js插件不具備的.2010-12-12
JavaScript+Canvas實(shí)現(xiàn)帶跳動(dòng)效果的粒子動(dòng)畫
這篇文章主要為大家詳細(xì)介紹了如何通過JavaScript和Canvas實(shí)現(xiàn)帶跳動(dòng)效果的粒子動(dòng)畫,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以參考一下2023-03-03
JavaScript中reduce()的5個(gè)基本用法示例
這篇文章主要給大家介紹了關(guān)于JavaScript中reduce()的5個(gè)基本用法示例,文中通過示例代碼以及圖文介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用js具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
基于JavaScript實(shí)現(xiàn)圖片點(diǎn)擊彈出窗口而不是保存
這篇文章主要介紹了基于JavaScript實(shí)現(xiàn)圖片點(diǎn)擊彈出窗口而不是保存的相關(guān)資料,需要的朋友可以參考下2016-02-02

