TypeScript(十四)變體(協(xié)變與逆變)
目錄
前言
本文收錄于TypeScript知識(shí)總結(jié)系列文章,歡迎指正!
第一次接觸到變體這個(gè)概念是在深入理解TypeScript中,類(lèi)型之間的轉(zhuǎn)換稱(chēng)為變體或者變型,在TS中,類(lèi)型之間能否互相賦值,會(huì)不會(huì)報(bào)錯(cuò),安不安全這些都與變體有關(guān)。本文將帶大家了解ts中的變體
在Java中,每一個(gè)類(lèi)都是一個(gè)個(gè)體,比如,我們定義了一個(gè)Dog和Cat兩個(gè)類(lèi),這二者的結(jié)構(gòu)相同。
// Dog.java
public class Dog {
String color;
int age;
}
// Cat.java
public class Cat {
String color;
int age;
}
// Main.java
public class Main {
Dog myCat = new Cat();// cannot convert from Cat to Dog
Cat myDog = new Dog();// cannot convert from Dog to Cat
Cat myCat2 = new Cat();// 允許
Dog myDog2 = new Dog();// 允許
}
此時(shí)使用控制變量對(duì)其二者進(jìn)行實(shí)例化,可以看到,雖然Dog和Cat的屬性相同,都有color以及age屬性,但是不能互相聲明類(lèi)型及賦
"鴨子類(lèi)型"
鴨子類(lèi)型(duck typing)是一種動(dòng)態(tài)類(lèi)型機(jī)制的編程概念,它指的是一個(gè)對(duì)象的類(lèi)型由它能干什么決定,而不是它屬于什么類(lèi)決定,它在運(yùn)行時(shí)就會(huì)對(duì)對(duì)象的類(lèi)型進(jìn)行判斷。鴨子類(lèi)型的思想是:如果它看起來(lái)像鴨子,游起來(lái)像鴨子,叫起來(lái)像鴨子,那么它就是鴨子。
TS中的類(lèi)型檢查就是鴨子類(lèi)型系統(tǒng),如果兩個(gè)對(duì)象類(lèi)型擁有相同的屬性或方法,那么它們就被認(rèn)定為是相同類(lèi)型
我們?cè)赥S中實(shí)現(xiàn)上面的效果
class Dog {
color: string
age: number
}
class Cat {
color: string
age: number
}
const dog: Cat = new Dog()// 允許
const cat: Dog = new Cat()// 允許
可以看到,TypeScript和JAVA的模式不同,只要結(jié)構(gòu)相同就可以重復(fù)使用類(lèi)型,這與其他語(yǔ)言不太一樣。
子類(lèi)型化
在學(xué)習(xí)數(shù)學(xué)的集合時(shí),一個(gè)集合A是另一個(gè)集合B的子集,那么A可以被視為B的一個(gè)子類(lèi)型;正方形可以視作矩形的一種子類(lèi),因?yàn)樗^承了矩形的性質(zhì),同時(shí)又在矩形的基礎(chǔ)上增加了一些附加的約束條件
定義
在計(jì)算機(jī)科學(xué)中,我們常說(shuō)的類(lèi)的繼承是對(duì)父類(lèi)的拓展,而子類(lèi)型化(Subtyping)是父類(lèi)型的拓展,它是指一種類(lèi)型(子類(lèi)型)是另一種類(lèi)型(超類(lèi)型)的一種特殊形式。與繼承相同,它可以添加或覆蓋超類(lèi)型的屬性和方法。它是較為抽象的拓展方式,沒(méi)有拓展具體的值,而是拓展類(lèi)型。超類(lèi)型經(jīng)過(guò)子類(lèi)型化操作后的類(lèi)型產(chǎn)物稱(chēng)為子類(lèi)型。
特點(diǎn)
賦值兼容性
如果IDog是IAnimal的子類(lèi)型,那么IDog類(lèi)型的變量可以賦值給IAnimal類(lèi)型的變量,舉個(gè)例子說(shuō)說(shuō)
interface IAnimal {
name: string
}
interface IDog extends IAnimal {
color: string
}
const animal: IAnimal = {
name: "阿黃"
}
const dog: IDog = Object.assign(animal, {
color: "black"
})
const _animal: IAnimal = dog // 可以執(zhí)行
其中IDog繼承自IAnimal,如果使用變量將這兩種類(lèi)型實(shí)現(xiàn),則可以將子類(lèi)型的變量賦予給父類(lèi)型的變量。
反身性
任何類(lèi)型都是它自己的子類(lèi)型。
interface IAnimal {
name: string
}
const animal: IAnimal = {
name: "阿黃"
}
const _animal: IAnimal = animal
傳遞性
如果IDog是IAnimal的子類(lèi)型,IWhiteDog是IDog的子類(lèi)型,那么IWhiteDog也是IAnimal的子類(lèi)型
interface IAnimal {
name: string
}
interface IDog extends IAnimal {
color: string
}
interface IWhiteDog extends IDog {
isWhite: boolean
}
const animal: IAnimal = {
name: "阿黃"
}
const dog: IDog = Object.assign(animal, {
color: "black"
})
const whiteDog: IWhiteDog = Object.assign(dog, {
isWhite: true
})
const _animal: IAnimal = whiteDog // 可以執(zhí)行
除了上述幾點(diǎn)外,子類(lèi)型還有變量協(xié)變性及函數(shù)參數(shù)逆變性的特點(diǎn),我們?cè)谙挛闹姓归_(kāi)講講
協(xié)變
如果理解了上面的鴨子類(lèi)型和子類(lèi)型化的概念,協(xié)變(Covariance)就不難理解。它是一種類(lèi)型的轉(zhuǎn)換,就像子類(lèi)型化中的例子,如果類(lèi)型IDog是類(lèi)型IAnimal的子類(lèi)型,那么dog可以賦值給animal,這個(gè)過(guò)程就是協(xié)變,即協(xié)變多變少(子類(lèi)型賦值給父類(lèi)型)
為了更好理解上面的例子以及后續(xù)的案例,我們寫(xiě)一個(gè)工具類(lèi)型IsExtends,用于判斷兩個(gè)類(lèi)型之間是否是類(lèi)型的繼承關(guān)系
type IsExtends<Son, Parent> = Son extends Parent ? true : false;
type animalExtendsDog = IsExtends<IAnimal, IDog>// false
type dogExtendsAnimal = IsExtends<IDog, IAnimal>// true
逆變
逆變(Contravariance)與協(xié)變相反,如果將父類(lèi)賦值給子類(lèi)成立,則稱(chēng)為逆變。我們將上述的例子修改一下變成以下代碼,就是逆變的過(guò)程,即逆變少變多(父類(lèi)型賦值給子類(lèi)型)
const _dog: IDog = animal // 無(wú)法執(zhí)行,類(lèi)型 "IAnimal" 中缺少屬性 "color",但類(lèi)型 "IDog" 中需要該屬性。
然而,一般情況下上面這段代碼是會(huì)拋錯(cuò)的,提示缺少屬性,此時(shí)我們可以借助類(lèi)型斷言進(jìn)行轉(zhuǎn)換
const _dog: IDog = animal as IDog // 可以執(zhí)行
但是這么寫(xiě)不太安全,如果animal中缺少I(mǎi)Dog的屬性可能會(huì)拋錯(cuò)
除此之外,函數(shù)的參數(shù)具有逆變性(不進(jìn)行類(lèi)型檢查就具有雙變特征),我們可以借助TS的函數(shù)進(jìn)行轉(zhuǎn)變,首先我們寫(xiě)個(gè)工具類(lèi)型ToFun,將類(lèi)型作為參數(shù)傳入函數(shù)中
type ToFun<P> = (params: P) => void
然后我們將之前兩個(gè)類(lèi)型IAnimal和IDog傳入函數(shù)中就會(huì)發(fā)現(xiàn)結(jié)果與前文截然相反
type IsExtends<Son, Parent> = Son extends Parent ? true : false;
type ToFun<P> = (params: P) => void
type animalExtendsDogFn = IsExtends<ToFun<IAnimal>, ToFun<IDog>>// true
type dogExtendsAnimalFn = IsExtends<ToFun<IDog>, ToFun<IAnimal>>// false
tips:如果兩個(gè)都是true,可以將tsconfig中的strictFunctionTypes或strict打開(kāi),此時(shí)會(huì)檢測(cè)函數(shù)參數(shù)的變體
此時(shí),我們可以將變量轉(zhuǎn)換為函數(shù)的形式,達(dá)到逆變的效果
const animalFn: (animal: IAnimal) => void = (animal) => { }
const _dog: ToFun<IDog> = animalFn // 可以執(zhí)行
雙變
雙變(Bivariance)在許多地方被稱(chēng)為是雙向協(xié)變,個(gè)人認(rèn)為不太準(zhǔn)確,雙變是指類(lèi)型同時(shí)具有協(xié)變和逆變的性質(zhì),稱(chēng)為協(xié)變與逆變可能比較合適。我們將tsconfig中的strictFunctionTypes與strict關(guān)閉,上面的例子就會(huì)顯示兩個(gè)true
type IsExtends<Son, Parent> = Son extends Parent ? true : false;
type ToFun<P> = (params: P) => void
type animalExtendsDogFn = IsExtends<ToFun<IAnimal>, ToFun<IDog>>// true
type dogExtendsAnimalFn = IsExtends<ToFun<IDog>, ToFun<IAnimal>>// true
我們使用變量試試
const animalFn: (animal: IAnimal) => void = (animal) => { }
const dogFn: (dog: IDog) => void = (dog) => { }
const _dog: ToFun<IDog> = animalFn // 可以執(zhí)行
const _animal: ToFun<IAnimal> = dogFn // 可以執(zhí)行
不變
不變(Invariance)的概念就比較簡(jiǎn)單了,兩種類(lèi)型既不會(huì)發(fā)生協(xié)變,也不會(huì)逆變,完全沒(méi)有關(guān)系的兩種類(lèi)型
interface IAnimal {
name: string
}
interface IDog {
color: string
}
let animal: IAnimal = {
name: "阿黃"
}
let dog: IDog = {
color: "black"
}
dog = animal// 不能執(zhí)行,缺少對(duì)應(yīng)屬性
animal = dog// 不能執(zhí)行,缺少對(duì)應(yīng)屬性
思考
看個(gè)例子
思考這個(gè)的例子,如果使用上面講到的協(xié)變與逆變的概念應(yīng)該不難理解。變量(返回值)協(xié)變,參數(shù)逆變
我們把原文的例子使用TS實(shí)現(xiàn)一下
首先新建三個(gè)類(lèi)型,分別是IAnimal,IDog,IWhiteDog,從動(dòng)物到白狗層層繼承
interface IAnimal {
name: string
}
interface IDog extends IAnimal {
type: string
}
interface IWhiteDog extends IDog {
isWhite: boolean
}
接著我們使用工具類(lèi)型實(shí)現(xiàn)一個(gè)函數(shù)創(chuàng)建,以及之前判斷繼承的工具
type Fun<P, R> = (params: P) => R// 創(chuàng)建以P為參數(shù),R為返回值的函數(shù)
type IsExtends<Son, Parent> = Son extends Parent ? true : false;// 是否是繼承關(guān)系
最后使用上述兩種工具對(duì)類(lèi)型進(jìn)行檢測(cè):除了自身外,當(dāng)函數(shù)的參數(shù)是IAnimal,返回值是IWhiteDog類(lèi)型時(shí),此函數(shù)是IDogFn的子類(lèi)型
type IDogFn = Fun<IDog, IDog> // IDog, IDog
type IAnimalWhiteDogFn = Fun<IAnimal, IWhiteDog>// IAnimal, IWhiteDog
type IAnimalFn = Fun<IAnimal, IAnimal>// IAnimal, IAnimal
type IWhiteDogFn = Fun<IWhiteDog, IWhiteDog>// IWhiteDog, IWhiteDog
type IWhiteDogAnimalFn = Fun<IWhiteDog, IAnimal>// IWhiteDog, IAnimal
type IsExtendsAnimalWhiteDog = IsExtends<IAnimalWhiteDogFn, IDogFn>// true
type IsExtendsAnimal = IsExtends<IAnimalFn, IDogFn>// false
type IsExtendsWhiteDog = IsExtends<IWhiteDogFn, IDogFn>// false
type IsExtendsWhiteDogAnimal = IsExtends<IWhiteDogAnimalFn, IDogFn>// false
函數(shù)返回值和變量一樣是協(xié)變的,所以子類(lèi)遵循常規(guī)的繼承取IWhiteDog;參數(shù)是逆變的,因此與正常的繼承行為相反,取IAnimal
原因是什么?
返回值
返回值是協(xié)變的這點(diǎn)不難理解,函數(shù)執(zhí)行時(shí)結(jié)果是RHS(Right Hand Side)右操作數(shù),即函數(shù)返回值同樣是賦值給變量的,我們還是使用上文賦值兼容性的例子做一點(diǎn)修改,在外層加個(gè)函數(shù)
interface IAnimal {
name: string
}
interface IDog extends IAnimal {
color: string
}
const animal = () => ({
name: "阿黃"
})
const dog = () => Object.assign(animal, {
color: "black"
})
const _animal: typeof animal = dog // 可以執(zhí)行
參數(shù)
傳入的參數(shù)訪(fǎng)問(wèn)子類(lèi)型中的屬性是不安全的。我們針對(duì)參數(shù)的子類(lèi)型舉個(gè)例子,首先我們?cè)黾右粋€(gè)IBlackDog類(lèi)型,并將IDogFn以及其他的變量實(shí)現(xiàn)
interface IAnimal {
name: string
}
interface IDog extends IAnimal {
type: string
}
interface IWhiteDog extends IDog {
isWhite: boolean
}
interface IBlackDog extends IDog {
isBlack: boolean
}
type Fun<P, R> = (params: P) => R// 函數(shù)
type IDogFn = Fun<IDog, IDog> // dog函數(shù)
const animal: IAnimal = {
name: "阿黃"
}
const dog: IDog = Object.assign(animal, {
type: "dog"
})
const whiteDog: IWhiteDog = Object.assign(dog, {
isWhite: true
})
const blackDog: IBlackDog = Object.assign(dog, {
isBlack: true
})
接著創(chuàng)建一個(gè)函數(shù)參數(shù)接收一個(gè)函數(shù),這個(gè)函數(shù)結(jié)構(gòu)是Fun<IDog, IDog>。此時(shí)由下面的代碼可以推導(dǎo)出,為何協(xié)變的參數(shù)是不安全的,可能有點(diǎn)繞
const example = (_fn: IDogFn): void => {
_fn(blackDog)// _fn參數(shù)限制了IDog類(lèi)型,所以實(shí)參可以傳遞blackDog,whiteDog,dog。這里我們傳入blackDog
}
example(whiteDogFn)// 拋錯(cuò),參數(shù)“_whiteDog”和“params” 的類(lèi)型不兼容。
const whiteDogFn = (_whiteDog: IWhiteDog) => {
_whiteDog.isWhite = false// 形參取IWhiteDog,但是實(shí)參傳了blackDog,此時(shí)就會(huì)拋錯(cuò),找不到isWhite,因?yàn)閎lackDog只有isBlack。所以使用形參使用IWhiteDog是不安全的,必須傳遞IDog類(lèi)型,或者IAnimal,因?yàn)镮Animal有的屬性,IDog都有
return dog
}
const animalFn = (_animal: IAnimal) => {
return dog
}
example(animalFn)// 允許執(zhí)行
針對(duì)上面的代碼做個(gè)解釋?zhuān)?/strong>
我們定義了一個(gè)函數(shù)whiteDogFn,接收一個(gè)參數(shù)IWhiteDog,此時(shí)我們可以直接調(diào)用IWhiteDog中的屬性isWhite,到這里都還算正常。但是接下來(lái)我們將這個(gè)函數(shù)代入到example函數(shù)中,為什么會(huì)拋錯(cuò)?因?yàn)镮DogFn類(lèi)型限制我們傳入blackDog,whiteDog,dog這三種類(lèi)型,如果此時(shí)我們傳入blackDog則會(huì)有問(wèn)題,因?yàn)閎lackDog沒(méi)有isWhite這個(gè)屬性。怎么做才能解決這個(gè)問(wèn)題呢?從源頭上控制函數(shù)的參數(shù)類(lèi)型,即使用逆變的方式限制whiteDogFn函數(shù)的參數(shù),限制為IAnimal,此時(shí)IAnimal只提供name這個(gè)屬性,我們只能調(diào)用這個(gè)屬性,并且這個(gè)屬性是IAnimal,IDog,IWhiteDog,IBlackDog這四個(gè)類(lèi)型都有的,此時(shí)使用該函數(shù)程序就是安全的
總結(jié)
以上就是文章全部?jī)?nèi)容了,本文詳細(xì)講述了TS中的變體概念,深入講解了子類(lèi)型化操作,協(xié)變,逆變,雙變,不變的概念,其中協(xié)變特點(diǎn)是多變少,逆變則是少變多,雙變即集成協(xié)變與逆變,不變則雙向都無(wú)法賦值,最后介紹了一下關(guān)于函數(shù)參數(shù)和返回值的變體規(guī)則,說(shuō)明了參數(shù)逆變與返回值協(xié)變的原因。
感謝你的閱讀,如果覺(jué)得文章不錯(cuò)的話(huà),還希望支持一下博主!
相關(guān)文章
javascript - 如何理解ts的函數(shù)參數(shù)雙向協(xié)變? - SegmentFault 思否

浙公網(wǎng)安備 33010602011771號(hào)