Friday Q&A 2015-11-20:協(xié)變與逆變

作者: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 中。CatAnimal 的一種袖订,所以它也能夠做 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)

爆炸了嚕裹纳!CrazyCatLadypet 方法期望參數(shù)是一只 Cat择葡。對于這只 Dog 就顯得無計可施。這個方法也有可能會訪問其他的屬性或者調(diào)用其他 Dog 類所不具備的方法剃氧。

這段代碼原本是完全正確的敏储。首先它得到 PersonAnimal,然后調(diào)用 Person 中的方法讓人擁有這個 Animal她我。上面的問題在于我們把 CrazyCatLady.pet 方法的參數(shù)類型變成了 Cat虹曙。這破壞了 Liskov 替換原則:此時的 CrazyCatLady 并不能在任意的地方替代 Person 的使用。

感謝編譯器給我們留了一手番舆。它明白使用子類型用于重寫方法的參數(shù)類型是不正確的酝碳,會拒絕編譯這個代碼。

那在重寫方法時使用不同的類型究竟對不對呢恨狈?對疏哗!事實上,你需要超類型(Supertype)禾怠。舉一個例子返奉,假設(shè) AnimalThing 的子類,那么當我們重寫 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)用在這兒,上面的代碼當 AC 的超類型且 BD 的子類型時可以正常的執(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ù)在上述提到的兩方面都被使用了随闪,那么當且僅當 AB 是相同類型的時候才是有效的。這也同樣適用于當泛型參數(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 都會聽從讀者的建議菱魔,所以有什么更高的建議或者文章的話留荔,記得給我們寫信


譯者注:

  1. 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
  1. 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验靡。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市雏节,隨后出現(xiàn)的幾起案子胜嗓,更是在濱河造成了極大的恐慌,老刑警劉巖钩乍,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件辞州,死亡現(xiàn)場離奇詭異,居然都是意外死亡寥粹,警方通過查閱死者的電腦和手機变过,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進店門埃元,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人媚狰,你說我怎么就攤上這事岛杀。” “怎么了崭孤?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵楞件,是天一觀的道長。 經(jīng)常有香客問我裳瘪,道長,這世上最難降的妖魔是什么罪针? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任彭羹,我火速辦了婚禮,結(jié)果婚禮上泪酱,老公的妹妹穿的比我還像新娘派殷。我一直安慰自己,他們只是感情好墓阀,可當我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布毡惜。 她就那樣靜靜地躺著,像睡著了一般斯撮。 火紅的嫁衣襯著肌膚如雪经伙。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天勿锅,我揣著相機與錄音帕膜,去河邊找鬼。 笑死溢十,一個胖子當著我的面吹牛垮刹,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播张弛,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼荒典,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了吞鸭?” 一聲冷哼從身側(cè)響起寺董,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎刻剥,沒想到半個月后螃征,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡透敌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年盯滚,在試婚紗的時候發(fā)現(xiàn)自己被綠了踢械。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡魄藕,死狀恐怖内列,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情背率,我是刑警寧澤话瞧,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站寝姿,受9級特大地震影響交排,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜饵筑,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一埃篓、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧根资,春花似錦架专、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至裤纹,卻和暖如春委刘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鹰椒。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工钱雷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人吹零。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓罩抗,卻偏偏與公主長得像,于是被迫代替她去往敵國和親灿椅。 傳聞我的和親對象是個殘疾皇子套蒂,可洞房花燭夜當晚...
    茶點故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容