作者:Mike Ash捧杉,原文鏈接陕见,原文日期:2015-11-20
譯者:Cee;校對:千葉知風(fēng)味抖;定稿:numbbbbb
在現(xiàn)代的編程語言中评甜,子類型(Subtypes)和超類型(Supertypes)已經(jīng)成為了非常常見的一部分了。協(xié)變(Convariance)和逆變(Contravariance)則能告訴我們什么時候使用子類型或超類型會優(yōu)于原來使用的類型仔涩。這在我們使用的大多數(shù)編程語言中非常的常見忍坷,但是很多開發(fā)者仍然對這些概念感到模糊不清。今天我們就來詳細討論一下。
子類型(Subtypes)和超類型(Supertypes)
我們都知道子類(Subclass)是什么佩研。當你創(chuàng)建一個子類的時候柑肴,你就在創(chuàng)建一個子類型。用一個經(jīng)典的例子來講韧骗,就是用 Animal
的子類去創(chuàng)建一只 Cat
:
class Animal {
...
}
class Cat: Animal {
...
}
這讓 Cat
成為了 Animal
的子類型嘉抒,也就意味著所有的 Cat
都是 Animal
。但并不意味著所有的 Animal
都是 Cat
袍暴。
子類型通常能夠替代超類型些侍。很明顯懂一點編程知識的任何程序員都知道,在 Swift 中政模,下面的代碼的第一行能夠正常的運行岗宣,然而第二行則不能:
let animal: Animal = Cat()
let cat: Cat = Animal()
對于函數(shù)類型也是適用的:
func animalF() -> Animal {
return Animal()
}
func catF() -> Cat {
return Cat()
}
let returnsAnimal: () -> Animal = catF
let returnsCat: () -> Cat = animalF
這些在 Objective-C 下也能實現(xiàn),只不過要用 block淋样,而且語法上會顯得比較丑耗式。所以我堅定地使用 Swift。
注意趁猴,以下的代碼是有問題的:
func catCatF(inCat: Cat) -> Cat {
return inCat
}
let animalAnimal: Animal -> Animal = catCatF
很困惑刊咳,不是嗎?不用擔心儡司,整篇文章就是為了徹底了解為什么第一個版本是可行而第二個版本是不可行的娱挨。除此之外,我們在探索的過程中還會了解很多非常有用的東西捕犬。
重寫(Override)方法
類似的事情在重寫方法中也能正確地執(zhí)行跷坝,想象一下有這樣一個類:
class Person {
func purchaseAnimal() -> Animal
}
現(xiàn)在我們建立它的子類,然后重寫父類的方法碉碉,并改變返回值的類型:
class CrazyCatLady: Person {
override func purchaseAnimal() -> Cat
}
這樣做對嗎柴钻?對。為什么呢垢粮?
Liskov 替換原則被用于指導(dǎo)何時該使用子類贴届。簡明扼要的來說,它指出任何子類的實例總是能夠替代父類的實例蜡吧。比如你有一個 Animal
毫蚓,你就能用 Cat
替代它;你也總是能夠用 CrazyCatLady
替代 Person
斩跌。
下面是使用 Person
作為例子寫的一段代碼,接下來會有解釋來解釋清楚:
let person: Person = getAPerson()
let animal: Animal = person.purchaseAnimal()
animal.pet()
想象一下當 getAPerson
返回一位 CrazyCatLady
捞慌。整段代碼還可行嗎耀鸦?CrazyCatLady.purchaseAnimal
會返回一只 Cat
。這個實例被放入了 animal
中。Cat
是 Animal
的一種袖订,所以它也能夠做 Animal
能夠做的事情氮帐,包括 pet
方法。類似洛姑,CrazyCatLady
返回的 Cat
也是有效的上沐。
我們這時把 pet
函數(shù)放入 Person
類中,所以我們能夠知道一個人所養(yǎng)的特定的動物:
class Person {
func purchaseAnimal() -> Animal
func pet(animal: Animal)
}
自然楞艾,CrazyCatLady
只擁有寵物貓:
class CrazyCatLady: Person {
override func purchaseAnimal() -> Cat
override func pet(animal: Cat)
}
現(xiàn)在這樣對嗎参咙?不對!
為了理解為什么不對硫眯,我們來看一下使用這個方法的代碼片段:
let person: Person = getAPerson()
let animal: Animal = getAnAnimal()
person.pet(animal)
假設(shè) getAPerson
方法返回了一位 CrazyCatLady
蕴侧,第一行非常的正確:
let person: Person = getAPerson()
如果 getAnAnimal
方法返回了一只 Dog
,它也是 Animal
的子類但是和 Cat
有截然不同的表現(xiàn)两入。接下來的一行看上去也非常的正確:
let animal: Animal = getAnAnimal()
接下來我們的 person
變量中有一位 CrazyCatLady
净宵,以及在 animal
變量中有一只 Dog
,然后執(zhí)行了這一行:
person.pet(animal)
爆炸了嚕裹纳!CrazyCatLady
的 pet
方法期望參數(shù)是一只 Cat
择葡。對于這只 Dog
就顯得無計可施。這個方法也有可能會訪問其他的屬性或者調(diào)用其他 Dog
類所不具備的方法剃氧。
這段代碼原本是完全正確的敏储。首先它得到 Person
和 Animal
,然后調(diào)用 Person
中的方法讓人擁有這個 Animal
她我。上面的問題在于我們把 CrazyCatLady.pet
方法的參數(shù)類型變成了 Cat
虹曙。這破壞了 Liskov 替換原則:此時的 CrazyCatLady
并不能在任意的地方替代 Person
的使用。
感謝編譯器給我們留了一手番舆。它明白使用子類型用于重寫方法的參數(shù)類型是不正確的酝碳,會拒絕編譯這個代碼。
那在重寫方法時使用不同的類型究竟對不對呢恨狈?對疏哗!事實上,你需要超類型(Supertype)禾怠。舉一個例子返奉,假設(shè) Animal
是 Thing
的子類,那么當我們重寫 pet
方法時吗氏,參數(shù)類型變?yōu)?Thing
:
override func pet(thing: Thing)
這保證了可替換性芽偏。如果是一個 Person
,那么這個方法所傳進來的參數(shù)類型始終是 Animal
弦讽,這是 Thing
的一種污尉。
有個重要的規(guī)則來了:函數(shù)的返回值可以換成原類型的子類型膀哲,在層級上降了一級;反之函數(shù)的參數(shù)可以換成原類型的超類型被碗,在層級上升了一級某宪。
單獨的函數(shù)(Standalone functions)
這種子類型和超類型的關(guān)系我們已經(jīng)在類上面了解得很清楚了。它能夠通過類與類之間的層級關(guān)系直接推出锐朴。那么如果是單獨的函數(shù)關(guān)系呢兴喂?
let f1: A -> B = ...
let f2: C -> D = f1
這種關(guān)系什么時候是對的,什么時候又是錯的呢焚志?
這可以被看做是 Liskov 替換原則的一種精簡版本衣迷。 事實上,你可以把函數(shù)想象成是非常小的(mini-objects)娩嚼、只有一個方法的對象蘑险。當你有兩個不同的對象類型時,怎么做才能夠讓這兩個對象也遵循我們的原則呢岳悟?只有當原對象類型是后者類型的子類型就可以了佃迄。那什么時候函數(shù)是另一個函數(shù)的子類型呢?正如上面所見贵少,當前者的參數(shù)是后者的超類型并且返回值是后者的子類型即可呵俏。
把這個方法應(yīng)用在這兒,上面的代碼當 A
是 C
的超類型且 B
是 D
的子類型時可以正常的執(zhí)行滔灶。用具體的例子來說:
let f1: Animal -> Animal = ...
let f2: Cat -> Thing = f1
參數(shù)和返回值的類型朝著相反的方向移動普碎。可能不是你所想的那樣录平,但是這就是能讓函數(shù)正確執(zhí)行的唯一方法麻车。
這又是一個重要的規(guī)則:一個函數(shù)若是另外一個函數(shù)的子類型,那么它的參數(shù)是原函數(shù)參數(shù)的超類型斗这,返回值是原函數(shù)返回值的子類型(譯者注:又叫做 Robustness 原則)动猬。
屬性(Property)
如果是只讀的屬性那就很簡單。子類的屬性必須是父類屬性的子類型表箭。只讀的屬性本質(zhì)上是一個不接收參數(shù)而返回成員值的函數(shù)赁咙,所以上述的規(guī)則依舊適用。
可讀可寫的屬性其實也非常的簡單免钻。子類的屬性必須和父類的屬性類型相同彼水。一個可讀可寫的屬性其實由一對函數(shù)組成。Getter
是一個不接收參數(shù)而返回成員值的函數(shù)极舔,Setter
則是一個需要傳入一個參數(shù)但無需返回值的函數(shù)凤覆。看下面的例子:
var animal: Animal
// 這等價于:
func getAnimal() -> Animal
func setAnimal(animal: Animal)
正如我們之前得到的結(jié)論一樣拆魏,函數(shù)的參數(shù)和返回值需要各自向上和向下改變一級盯桦。然而參數(shù)和返回值的類型卻是固定的澡绩,所以它們的類型都不能被改變:
// 注意到 animal 的類型是 Animal
// 這樣不對(向下)
override func getAnimal() -> Cat
override func setAnimal(animal: Cat)
// 這樣也不對(向上)
override func getAnimal() -> Thing
override func setAnimal(animal: Thing)
泛型(Generics)
那如果是泛型呢?給定泛型類型的參數(shù)俺附,什么時候又是正確的呢?
let var1: SomeType<A> = ...
let var2: SomeType<B> = var1
理論上來說溪掀,這要看泛型參數(shù)是如何使用的事镣。一個泛型類型參數(shù)本身并不做什么事情,但是它會被用作于屬性的類型揪胃、函數(shù)方法的參數(shù)類型和返回類型璃哟。
如果泛型參數(shù)僅僅被用作函數(shù)返回值的類型和只讀屬性身上,那么 B
需要是 A
的超類型:
let var1: SomeType<Cat> = ...
let var2: SomeType<Animal> = var1
如果泛型參數(shù)僅被用作于函數(shù)方法的參數(shù)類型喊递,那么 B
需要是 A
的子類型:
let var1: SomeType<Animal> = ...
let var2: SomeType<Cat> = var1
如果泛型參數(shù)在上述提到的兩方面都被使用了随闪,那么當且僅當 A
和 B
是相同類型的時候才是有效的。這也同樣適用于當泛型參數(shù)作為可讀可寫屬性的情況骚勘。
這就是理論部分铐伴,看上去有些復(fù)雜但其實很簡短。與此同時俏讹,Swift 尋求到了其簡便的解決之道当宴。對于兩個需要相互匹配的泛型類型,Swift 要求它們的泛型參數(shù)的類型也需要相同泽疆。子類型和超類型都是不被允許的户矢,盡管理論上可行。
Objective-C 事實上比 Swift 更好一些殉疼。一個在 Objective-C 中的泛型參數(shù)可以在聲明時增加 __covariant
關(guān)鍵字來表示它能夠接受子類型梯浪,而在聲明時增加 __contravariant
關(guān)鍵字來表示它能夠接受超類型。這在 NSArray
和其他的類的接口中有所體現(xiàn):
objective-c
@interface NSArray<__covariant ObjectType> : NSObject ...
協(xié)變和逆變(Convariance and Contravariance)
那些細心的讀者會注意到:在標題中提到的兩個詞至今為止我通篇未提∑澳龋現(xiàn)在我們既然了解了這些概念挂洛,那就來談一下這幾個專業(yè)術(shù)語。
協(xié)變(Convariance)指可接受子類型恋腕。重寫只讀的屬性是「協(xié)變的」抹锄。
逆變(Contravariance)指可接受超類型。重寫方法中的參數(shù)是「逆變的」荠藤。
不變(Invariance)指既不接受子類型伙单,又不接受超類型。Swift 中泛型是「不變的」哈肖。
雙向協(xié)變(Bivariate)指既接受子類型吻育,又接受超類型。我想不到在 Objective-C 或 Swift 中的任何例子淤井。
你會發(fā)現(xiàn)這種專業(yè)術(shù)語非常難記布疼。那就對了摊趾,因為這并不重要。只要你懂得子類型游两、超類型砾层,以及什么時候在特定位置適用一個類的子類或者超類就夠了。在需要用到術(shù)語的時候看一下就夠了贱案。
小結(jié)
協(xié)變和逆變決定了在特定位置該怎樣使用子類型或超類型肛炮。通常出現(xiàn)在重寫方法以及改變傳入?yún)?shù)或者返回值類型的地方。這種情況下我們已經(jīng)知道返回值必須是原來的子類型宝踪,而參數(shù)是原來的超類型侨糟。整個指導(dǎo)我們這么做的原則就叫做 Liskov 替換原則,意思是任何子類的實例總是能夠使用在父類的實例所使用的地方瘩燥。子類型和超類型就是從這條原則中衍生出來秕重。
今天就到這兒了。記得回來探索更多有趣的事情厉膀;或者說就來探索有趣的事情溶耘。「更多」可能在這不適用服鹅,因為協(xié)變這件事并不是那么的令人激動汰具。無論如何,我們的 Friday Q&A 都會聽從讀者的建議菱魔,所以有什么更高的建議或者文章的話留荔,記得給我們寫信!
譯者注:
- Swift 中的泛型的確是「不變的(Invariance)」澜倦,但是 Swift 標準庫中的 Collection 類型通常情況下是「協(xié)變的(Convariance)」聚蝶。舉個例子:
import UIKit
class Thing<T> { // 亦可以使用結(jié)構(gòu)體 struct 聲明
var thing: T
init(_ thing: T) { self.thing = thing }
}
var foo: Thing<UIView> = Thing(UIView())
var bar: Thing<UIButton> = Thing(UIButton())
foo = bar // 報錯:error: cannot assign value of type 'Thing<UIButton>' to type 'Thing<UIView>'
// Array 則不會報錯
var views: Array<UIView> = [UIView()]
var buttons: Array<UIButton> = [UIButton()]
views = buttons
- Swift 中的 Protocol 不支持這里的類型改變。如果某個協(xié)議是繼承自另外一個協(xié)議而且嘗試著「重寫」父協(xié)議的方法藻治,Swift 會把它當做是另外一個方法碘勉。舉個例子:
class Thing {}
class Animal: Thing {}
class Cat: Animal {}
protocol SuperP {
func f(animal: Animal) -> Animal
}
protocol SubP1: SuperP {
func f(thing: Thing) -> Cat
}
protocol SubP2: SuperP {
func f(cat: Cat) -> Thing
}
class ImplementsSubP1: SubP1 {
func f(thing: Thing) -> Cat {
return Cat()
}
}
class ImplementsSubP2: SubP2 {
func f(cat: Cat) -> Thing {
return Thing()
}
}
// ImplementSubP1 和 ImplementSubP2 將不被認為遵循了 SuperP 的協(xié)議
本文由 SwiftGG 翻譯組翻譯,已經(jīng)獲得作者翻譯授權(quán)桩卵,最新文章請訪問 http://swift.gg验靡。