Friday Q&A 2015-11-20:協變與逆變

作者:Mike Ash,原文鏈接,原文日期:2015-11-20
譯者:Cee;校對:千葉知風;定稿:numbbbbb

在現代的編程語言中,子類型(Subtypes)和超類型(Supertypes)已經成為了非常常見的一部分了。協變(Convariance)和逆變(Contravariance)則能告訴我們什么時候使用子類型或超類型會優于原來使用的類型。這在我們使用的大多數編程語言中非常的常見,但是很多開發者仍然對這些概念感到模糊不清。今天我們就來詳細討論一下。

子類型(Subtypes)和超類型(Supertypes)

我們都知道子類(Subclass)是什么。當你創建一個子類的時候,你就在創建一個子類型。用一個經典的例子來講,就是用 Animal 的子類去創建一只 Cat

    class Animal {
        ...
    }

    class Cat: Animal {
        ...
    }

這讓 Cat 成為了 Animal 的子類型,也就意味著所有的 Cat 都是 Animal。但并不意味著所有的 Animal 都是 Cat

子類型通常能夠替代超類型。很明顯懂一點編程知識的任何程序員都知道,在 Swift 中,下面的代碼的第一行能夠正常的運行,然而第二行則不能:

    let animal: Animal = Cat()
    let cat: Cat = Animal()

對于函數類型也是適用的:

    func animalF() -> Animal {
        return Animal()
    }

    func catF() -> Cat {
        return Cat()
    }

    let returnsAnimal: () -> Animal = catF
    let returnsCat: () -> Cat = animalF

這些在 Objective-C 下也能實現,只不過要用 block,而且語法上會顯得比較丑。所以我堅定地使用 Swift。

注意,以下的代碼是有問題的:

    func catCatF(inCat: Cat) -> Cat {
        return inCat
    }

    let animalAnimal: Animal -> Animal = catCatF

很困惑,不是嗎?不用擔心,整篇文章就是為了徹底了解為什么第一個版本是可行而第二個版本是不可行的。除此之外,我們在探索的過程中還會了解很多非常有用的東西。

重寫(Override)方法

類似的事情在重寫方法中也能正確地執行,想象一下有這樣一個類:

    class Person {
        func purchaseAnimal() -> Animal
    }

現在我們建立它的子類,然后重寫父類的方法,并改變返回值的類型:

    class CrazyCatLady: Person {
        override func purchaseAnimal() -> Cat
    }

這樣做對嗎?對。為什么呢?

Liskov 替換原則被用于指導何時該使用子類。簡明扼要的來說,它指出任何子類的實例總是能夠替代父類的實例。比如你有一個 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 函數放入 Person 類中,所以我們能夠知道一個人所養的特定的動物:

    class Person {
        func purchaseAnimal() -> Animal
        func pet(animal: Animal)
    }

自然,CrazyCatLady 只擁有寵物貓:

    class CrazyCatLady: Person {
        override func purchaseAnimal() -> Cat
        override func pet(animal: Cat)
    }

現在這樣對嗎?不對!

為了理解為什么不對,我們來看一下使用這個方法的代碼片段:

    let person: Person = getAPerson()
    let animal: Animal = getAnAnimal()
    person.pet(animal)

假設 getAPerson 方法返回了一位 CrazyCatLady,第一行非常的正確:

    let person: Person = getAPerson()

如果 getAnAnimal 方法返回了一只 Dog,它也是 Animal 的子類但是和 Cat 有截然不同的表現。接下來的一行看上去也非常的正確:

    let animal: Animal = getAnAnimal()

接下來我們的 person 變量中有一位 CrazyCatLady,以及在 animal 變量中有一只 Dog,然后執行了這一行:

    person.pet(animal)

爆炸了嚕!CrazyCatLadypet 方法期望參數是一只 Cat。對于這只 Dog 就顯得無計可施。這個方法也有可能會訪問其他的屬性或者調用其他 Dog 類所不具備的方法。

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

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

那在重寫方法時使用不同的類型究竟對不對呢?對!事實上,你需要超類型(Supertype)。舉一個例子,假設 AnimalThing 的子類,那么當我們重寫 pet 方法時,參數類型變為 Thing

    override func pet(thing: Thing)

這保證了可替換性。如果是一個 Person,那么這個方法所傳進來的參數類型始終是 Animal,這是 Thing 的一種。

有個重要的規則來了:函數的返回值可以換成原類型的子類型,在層級上了一級;反之函數的參數可以換成原類型的超類型,在層級上了一級。

單獨的函數(Standalone functions)

這種子類型和超類型的關系我們已經在類上面了解得很清楚了。它能夠通過類與類之間的層級關系直接推出。那么如果是單獨的函數關系呢?

    let f1: A -> B = ...
    let f2: C -> D = f1

這種關系什么時候是對的,什么時候又是錯的呢?

這可以被看做是 Liskov 替換原則的一種精簡版本。 事實上,你可以把函數想象成是非常小的(mini-objects)、只有一個方法的對象。當你有兩個不同的對象類型時,怎么做才能夠讓這兩個對象也遵循我們的原則呢?只有當原對象類型是后者類型的子類型就可以了。那什么時候函數是另一個函數的子類型呢?正如上面所見,當前者的參數是后者的超類型并且返回值是后者的子類型即可。

把這個方法應用在這兒,上面的代碼當 AC 的超類型且 BD 的子類型時可以正常的執行。用具體的例子來說:

    let f1: Animal -> Animal = ...
    let f2: Cat -> Thing = f1

參數和返回值的類型朝著相反的方向移動??赡懿皇悄闼氲哪菢?,但是這就是能讓函數正確執行的唯一方法。

這又是一個重要的規則:一個函數若是另外一個函數的子類型,那么它的參數是原函數參數的超類型,返回值是原函數返回值的子類型(譯者注:又叫做 Robustness 原則)。

屬性(Property)

如果是只讀的屬性那就很簡單。子類的屬性必須是父類屬性的子類型。只讀的屬性本質上是一個不接收參數而返回成員值的函數,所以上述的規則依舊適用。

可讀可寫的屬性其實也非常的簡單。子類的屬性必須和父類的屬性類型相同。一個可讀可寫的屬性其實由一對函數組成。Getter 是一個不接收參數而返回成員值的函數,Setter 則是一個需要傳入一個參數但無需返回值的函數??聪旅娴睦樱?/p>

    var animal: Animal
    // 這等價于:
    func getAnimal() -> Animal
    func setAnimal(animal: Animal)

正如我們之前得到的結論一樣,函數的參數和返回值需要各自向上和向下改變一級。然而參數和返回值的類型卻是固定的,所以它們的類型都不能被改變:

    // 注意到 animal 的類型是 Animal
    // 這樣不對(向下)
    override func getAnimal() -> Cat
    override func setAnimal(animal: Cat)

    // 這樣也不對(向上)
    override func getAnimal() -> Thing
    override func setAnimal(animal: Thing)

泛型(Generics)

那如果是泛型呢?給定泛型類型的參數,什么時候又是正確的呢?

    let var1: SomeType<A> = ...
    let var2: SomeType<B> = var1

理論上來說,這要看泛型參數是如何使用的。一個泛型類型參數本身并不做什么事情,但是它會被用作于屬性的類型、函數方法的參數類型和返回類型。

如果泛型參數僅僅被用作函數返回值的類型和只讀屬性身上,那么 B 需要是 A 的超類型:

    let var1: SomeType<Cat> = ...
    let var2: SomeType<Animal> = var1

如果泛型參數僅被用作于函數方法的參數類型,那么 B 需要是 A 的子類型:

    let var1: SomeType<Animal> = ...
    let var2: SomeType<Cat> = var1

如果泛型參數在上述提到的兩方面都被使用了,那么當且僅當 AB 是相同類型的時候才是有效的。這也同樣適用于當泛型參數作為可讀可寫屬性的情況。

這就是理論部分,看上去有些復雜但其實很簡短。與此同時,Swift 尋求到了其簡便的解決之道。對于兩個需要相互匹配的泛型類型,Swift 要求它們的泛型參數的類型也需要相同。子類型和超類型都是不被允許的,盡管理論上可行。

Objective-C 事實上比 Swift 更好一些。一個在 Objective-C 中的泛型參數可以在聲明時增加 __covariant 關鍵字來表示它能夠接受子類型,而在聲明時增加 __contravariant 關鍵字來表示它能夠接受超類型。這在 NSArray 和其他的類的接口中有所體現:

objective-c
    @interface NSArray<__covariant ObjectType> : NSObject ...

協變和逆變(Convariance and Contravariance)

那些細心的讀者會注意到:在標題中提到的兩個詞至今為止我通篇未提?,F在我們既然了解了這些概念,那就來談一下這幾個專業術語。

協變(Convariance)指可接受子類型。重寫只讀的屬性是「協變的」。

逆變(Contravariance)指可接受超類型。重寫方法中的參數是「逆變的」。

不變(Invariance)指既不接受子類型,又不接受超類型。Swift 中泛型是「不變的」。

雙向協變(Bivariate)指既接受子類型,又接受超類型。我想不到在 Objective-C 或 Swift 中的任何例子。

你會發現這種專業術語非常難記。那就對了,因為這并不重要。只要你懂得子類型、超類型,以及什么時候在特定位置適用一個類的子類或者超類就夠了。在需要用到術語的時候看一下就夠了。

小結

協變和逆變決定了在特定位置該怎樣使用子類型或超類型。通常出現在重寫方法以及改變傳入參數或者返回值類型的地方。這種情況下我們已經知道返回值必須是原來的子類型,而參數是原來的超類型。整個指導我們這么做的原則就叫做 Liskov 替換原則,意思是任何子類的實例總是能夠使用在父類的實例所使用的地方。子類型和超類型就是從這條原則中衍生出來。

今天就到這兒了。記得回來探索更多有趣的事情;或者說就來探索有趣的事情?!父唷箍赡茉谶@不適用,因為協變這件事并不是那么的令人激動。無論如何,我們的 Friday Q&A 都會聽從讀者的建議,所以有什么更高的建議或者文章的話,記得給我們寫信!


譯者注:

  1. Swift 中的泛型的確是「不變的(Invariance)」,但是 Swift 標準庫中的 Collection 類型通常情況下是「協變的(Convariance)」。舉個例子:
import UIKit 

class Thing<T> { // 亦可以使用結構體 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 不支持這里的類型改變。如果某個協議是繼承自另外一個協議而且嘗試著「重寫」父協議的方法,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 的協議

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 http://swift.gg

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容