讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 4.9 泛型 >

4.9 泛型

泛型是一種類型佔位符,實際的類型會在稍後進行填充。由於Swift有嚴格的類型,所以泛型是非常有用的一個特性。在不犧牲嚴格類型的情況下,有時你不能或是不想在代碼中的某處精確指定類型。

重要的是要理解泛型並沒有放鬆Swift嚴格的類型。特別地,泛型並未將類型解析推遲到運行期。在使用泛型時,代碼依然要指定真實的類型;這個真實的類型在編譯期就會完全指定好!代碼中如果需要某個類型,那麼可以使用泛型,這樣就不必完全指定好類型了,不過當其他代碼使用這部分代碼時就需要指定好類型。佔位符就是泛型,不過在使用泛型時,它會被解析為實際的特定類型。

Optional就是個很好的示例。任何類型的值都可以包裝到Optional中,不過你永遠不必擔心某個Optional中包裝的是什麼類型,這是怎麼做到的呢?因為Optional是個泛型類型,這正是Optional的工作原理。

我之前已經說過Optional是個枚舉,它有兩個Case:.None與.Some。如果Optional的Case是.Some,那麼它就會有一個關聯值,即被該Optional所包裝的值。不過這個關聯值的類型是什麼呢?一方面,我們會說它可以是任何類型;畢竟,任何東西都可以被包裝到Optional中。另一方面,包裝某個值的任何Optional都會包裝某個特定類型的值。在展開Optional時,被展開的值需要轉換為它原本的類型,這樣才能向其發送恰當的消息。

該問題的解決方案就是Swift泛型。Swift頭文件中Optional枚舉聲明的開頭如下代碼所示:


enum Optional< Wrapped> {
    // ...
}
  

上述語法表示:「在聲明中,我使用了一個假的類型(類型佔位符),叫作Wrapped。」它是個真實且單一的類型,不過現在不想過多地表示它的信息。你需要知道的是,當我說Wrapped時,我指的是一個特定的類型。在創建實際的Optional時,類型Wrapped的含義就一目瞭然了,接下來我再說Wrapped時,你應該將其替換為它所表示的類型。

下面再來看看Optional聲明:


enum Optional<Wrapped> {
    case None
    case Some(Wrapped)
    init(_ some: Wrapped)
    // ...
}>
  

我們已經將Wrapped聲明成了一個佔位符,接下來就可以使用它了。有一個Case為.None,還有一個Case為.Some,它有一個關聯值,類型為Wrapped。我們還有一個初始化器,它接收一個類型為Wrapped的參數。因此,初始化時所使用的類型就是Wrapped,它也是關聯到.Some Case的值的類型。

正是由於初始化器參數的類型與.Some關聯值的類型之間的這種同一性才使得後者能夠被解析出來。在Optional枚舉的聲明中,Wrapped是個佔位符。不過在實際情況下,當創建實際的Optional時,它會被某個確定的類型值所初始化。很多時候,我們會使用問號語法糖(String?類型),初始化器則會在背後得到調用。出於清晰的目的,下面來顯式調用初始化器:


let s = Optional("howdy")
  

上述代碼會針對這個特定的Optional實例對Wrapped類型進行解析。顯然,"howdy"是個String,因此編譯器知道,對於這個特定的Optional<Wrapped>來說,Wrapped是個String。在底層Optional枚舉聲明中凡是出現Wrapped的地方,編譯器都會將其替換為String。因此,從編譯器的角度來看,變量s所引用的這個特定Optional的聲明如下所示:


enum Optional <String>{
    case None
    case Some(String)
    init(_ some: String)
    // ...
}
  

這是Optional聲明的偽代碼,其中Wrapped佔位符已經被String類型所替換。我們可以說s是個Optional<String>。事實上,這是合法的語法!我們可以像下面這樣創建相同的Optional:


let s : Optional<String> = "howdy"
  

大量內建的Swift類型都涉及泛型。事實上,該語言特性在設計時就充分考慮了Swift類型;正是由於泛型的存在,Swift類型才能實現自己的目的。

4.9.1 泛型聲明

下面列出了在什麼地方可以聲明Swift泛型:

使用Self的泛型協議

在協議中,關鍵字Self(注意首字母大寫)會將協議轉換為泛型。Self是個佔位符,表示使用者的類型。比如,下面這個Flier協議聲明了一個接收Self參數的方法:


protocol Flier {
    func flockTogetherWith(f:Self)
}
  

這表示,如果Bird對像類型使用了Flier協議,那麼flockTogetherWith的實現就需要將其f參數聲明為Bird。

使用空類型別名的泛型協議

協議可以聲明類型別名,不必定義類型別名表示什麼。也就是說,typealias語句並不會包含等號。這會將協議轉換為泛型;別名的名字(也叫作關聯類型)是個佔位符。比如:


protocol Flier {
    typealias Other
    func flockTogetherWith(f:Other)
    func mateWith(f:Other)
}
  

使用者會在泛型使用類型別名的地方聲明特定的類型,從而解析出佔位符。如果Bird結構體使用了Flier協議,並將flockTogetherWith的f參數聲明為Bird,那麼該聲明就會針對這個特定的使用者將Other解析為Bird,現在Bird也需要將mateWith的f參數聲明為Bird類型:


struct Bird : Flier {
    func flockTogetherWith(f:Bird) {}
    func mateWith(f:Bird) {}
}   

這種形式的泛型協議從根本上來說與前一種形式一樣;如果寫成f:Other,那麼Swift就會知道它表示f:Self.Other,實際上這麼寫是合法的(也更加清晰)。

泛型函數

函數聲明可以對其參數、返回類型以及在函數體中使用泛型佔位符。請在函數名後的尖括號中聲明佔位符的名字:


func takeAndReturnSameThing<T> (t:T) -> T {
    return t
}
  

調用者會在函數聲明中佔位符出現的地方使用特定的類型,從而解析出佔位符:


let thing = takeAndReturnSameThing("howdy")
  

調用中所用的實參"howdy"的類型會將T解析為String;因此,對takeAndReturn-SameThing的調用也會返回一個String,捕獲結果的變量thing也會被推斷為String。

泛型對像類型

對像類型聲明可以在花括號中使用泛型佔位符類型。請在對像類型名後面的尖括號中聲明佔位符名字:


struct HolderOfTwoSameThings<T> {
    var firstThing : T
    var secondThing : T
    init(thingOne:T, thingTwo:T) {
        self.firstThing = thingOne
        self.secondThing = thingTwo
    }
}
  

該對像類型的使用者會在對像類型聲明中佔位符出現的地方使用特定的類型,從而解析出佔位符:


let holder = HolderOfTwoSameThings(thingOne:"howdy", thingTwo:"getLost")
  

初始化器調用中所使用的thingOne實參"howdy"的類型會將T解析為String;因此,thingTwo也一定是個String,屬性firstThing與secondThing都是String。

對於使用了尖括號語法的泛型函數與對像類型,尖括號中可以包含多個佔位符名,中間通過逗號分隔,比如:


func flockTwoTogether<T, U>(f1:T, _ f2:U) {}
  

現在,flockTwoTogether的兩個參數可以被解析為兩個不同的類型(不過也可以相同)。

4.9.2 類型約束

到目前為止,所有示例都可以使用任何類型替代佔位符。此外,你可以限制用於解析特定佔位符的類型,這叫作類型限制。最簡單的類型限制形式是其首次出現時,在佔位符名後面加上一個冒號和一個類型名。冒號後面的類型名可以是類名或是協議名。

回到Flier及其flockTogetherWith函數。假設flockTogetherWith的參數類型需要被使用者聲明為使用了Flier的類型。你不能在協議中將參數類型聲明為Flier:


protocol Flier {
    func flockTogetherWith(f:Flier)
}
  

上述代碼表示:只有聲明的函數flockTogetherWith的f參數是Flier類型,你才能使用該協議:


struct Bird : Flier {
    func flockTogetherWith(f:Flier) {}
}
  

這並不是我們想要的!我們需要的是,Bird應該可以使用Flier協議,同時將f聲明為某個Flier使用者類型,如Bird。方式就是將佔位符限制為Flier。比如,我們可以這樣做:


protocol Flier {
    typealias Other : Flier
    func flockTogetherWith(f:Other)
}
  

遺憾的是,這麼做是不合法的:協議不能將自身作為類型約束。解決辦法就是再聲明一個協議,然後讓Flier使用這個協議,並且將Other約束到這個協議上:


protocol Superflier {}
protocol Flier : Superflier {
    typealias Other : Superflier
    func flockTogetherWith(f:Other)
}
  

現在,Bird就是個合法的使用者了:


struct Bird : Flier {
    func flockTogetherWith(f:Bird) {}
}
  

在泛型函數或泛型對像類型中,類型限制位於尖括號中。比如:


func flockTwoTogether<T:Flier>(f1:T, _ f2:T) {}
  

現在不能使用兩個String參數調用flockTwoTogether了,因為String並不是Flier。此外,如果Bird與Insect都使用了Flier,那麼flockTwoTogether可以通過兩個Bird參數或兩個Insect參數調用,但不能一個是Bird,另一個是Insect,因為T僅僅是一個佔位符而已,表示Flier使用者類型。

對於佔位符的類型限制通常用於告訴編譯器,某個消息可以發送給佔位符類型的實例。比如,假設我們要實現一個函數myMin,它會從相同類型的一個列表中返回最小值。下面是一個看起來還不錯的泛型函數實現,不過有個問題,即它無法編譯通過:


func myMin<T>(things:T ...) -> T {
    var minimum = things[0]
    for ix in 1..<things.count {
        if things[ix] < minimum { // compile error
            minimum = things[ix]
        }
    }
    return minimum
}
  

問題在於比較things[ix]<minimum。編譯器怎麼知道類型T(things[ix]與minimum的類型)所解析出的類型能夠使用小於運算符進行比較呢?它不知道,這也是上述代碼無法編譯通過的原因所在。解決方案就是向編譯器承諾,T解析出的類型能夠使用小於運算符。方式就是將T限制為Swift內建的Comparable協議;使用Comparable協議可以確保使用者能夠使用小於運算符:


func myMin<T:Comparable>(things:T ...) -> T {
  

現在的myMin可以編譯通過,因為只有將T解析為使用了Comparable的對象類型它才能被調用,因此它也可以使用小於運算符進行比較。自然地,你覺得可以進行比較的內建對像類型(如Int、Double、String及Character等)實際上都使用了Comparable協議!查閱Swift頭文件,你會發現內建的min全局函數就是按照這種方式聲明的,原因與此相同。

泛型協議(聲明中使用了Self或擁有關聯類型的協議)只能用在泛型類型中,並且作為類型限制。如下代碼無法編譯通過:


protocol Flier {
    typealias Other
    func fly
}
func flockTwoTogether(f1:Flier, _ f2:Flier) { // compile error
    f1.fly
    f2.fly
}
  

要想將泛型Flier協議作為類型,你需要編寫一個泛型並將Flier作為類型限制,如下代碼所示:


protocol Flier {
    typealias Other
    func fly
}
func flockTwoTogether<T1:Flier, T2:Flier>(f1:T1, f2:T2) {
    f1.fly
    f2.fly
}
  

4.9.3 顯式特化

到目前為止,所有示例中泛型的使用者都是通過推斷來解析佔位符類型的。不過,還有一種解析方式:使用者可以手工解析類型,這叫作顯式特化。在某些情況下,顯式特化是強制的,即如果佔位符類型無法通過推斷得出,那就需要使用顯式特化。有兩種形式的顯式特化:

擁有關聯類型的泛型協議

協議使用者可以通過typealias聲明手工解析出協議的關聯類型,方式是使用協議別名與顯式類型賦值。比如:


protocol Flier {
    typealias Other
}
struct Bird : Flier {
    typealias Other = String
}
  

泛型對像類型

泛型對像類型的使用者可以通過相同的尖括號語法手工解析出對象的佔位符類型,尖括號用於聲明泛型,裡面的是實際的類型名。比如:


class Dog<T> {
    var name : T?
}
let d = Dog<String>
  

(這解釋了本章之前與第3章介紹的Optional<String>類型。)

不能顯式特化泛型函數。不過,你可以使用非泛型函數(使用了泛型類型的佔位符)來聲明泛型類型;泛型類型的顯式特化會解析出佔位符,因此也能解析出函數:


protocol Flier {
    init
}
struct Bird : Flier {
    init {}
}
struct FlierMaker<T:Flier> {
    static func makeFlier -> T {
        return T
    }
}
let f = FlierMaker<Bird>.makeFlier // returns a Bird
  

如果類是泛型的,那麼你可以對其子類化,前提是可以解析出泛型(這是Swift 2.0的新特性)。可以通過匹配的泛型子類或顯式解析出父類泛型來做到這一點。比如,下面是個泛型Dog:


class Dog<T> {
   var name : T?
}
  

你可以將其子類化為泛型,其佔位符與父類佔位符相匹配:


class NoisyDog<T> : Dog<T> {}
  

這麼做是合法的,因為對NoisyDog佔位符T的解析也會解析Dog佔位符T。另一種方式是子類化一個明確指定的Dog:


class NoisyDog : Dog<String> {}
  

4.9.4 關聯類型鏈

如果具有關聯類型的泛型協議使用了泛型佔位符,那麼我們可以通過對佔位符名使用點符號將關聯類型名鏈接起來,從而指定其類型。

來看下面這個示例。假設有一個遊戲程序,士兵與弓箭手彼此為敵。我通過將Soldier結構體與Archer結構體納入擁有Enemy關聯類型的Fighter協議中來表示這一點,Enemy本身又被限制為是一個Fighter(這裡還是需要另外一個協議,Fighter會使用該協議):


protocol Superfighter {}
protocol Fighter : Superfighter {
    typealias Enemy : Superfighter
}
  

下面手工為這兩個結構體解析這個關聯類型:


struct Soldier : Fighter {
    typealias Enemy = Archer
}
struct Archer : Fighter {
    typealias Enemy = Soldier
}
  

現在來創建一個泛型結構體,表示這些戰士對面的營地:


struct Camp<T:Fighter> {
}
  

假設一個營地可以容納來自對方陣營的一個間諜。那麼間諜的類型應該是什麼呢?如果是Soldier營地,那麼它就是Archer;如果是Archer營地,那麼它就是Soldier。更為一般地,由於T是個Fighter,那麼它應該是Fighter的Enemy類型。我可以通過將關聯類型名鏈接到佔位符名來清楚地表達這一點:


struct Camp<T:Fighter> {
    var spy : T.Enemy?
}
  

結果就是,針對某個特定的Camp,如果T被解析為Soldier,那麼T.Enemy就表示Fighter,反之亦然。我們為Capm的spy類型創建了正確的規則。如下代碼無法編譯通過:


var c = Camp<Soldier>
c.spy = Soldier // compile error
  

我們嘗試將錯誤類型的對象賦給這個Camp的spy屬性。但如下代碼可以編譯通過:


var c = Camp<Soldier>
c.spy = Archer
  

使用更長的關聯類型名鏈也是可以的,特別是當泛型協議有一個關聯類型,這個關聯類型本身又被強制約束為一個擁有關聯類型的泛型協議時更是如此。

比如,下面為每一類Fighter賦予一個有特色的武器:士兵有劍,弓箭手有弓。創建一個Sword結構體和一個Bow結構體,並將它們置於Wieldable協議之下:


protocol Wieldable {
}
struct Sword : Wieldable {
}
struct Bow : Wieldable {
}
  

向Fighter添加一個Weapon關聯類型,Weapon被強制約束為Wieldable,這次還是手工解析每一種Fighter類型的Weapon:


protocol Superfighter {
    typealias Weapon : Wieldable
}
protocol Fighter : Superfighter {
    typealias Enemy : Superfighter
}
struct Soldier : Fighter {
    typealias Weapon = Sword
    typealias Enemy = Archer
}
struct Archer : Fighter {
    typealias Weapon = Bow
    typealias Enemy = Soldier
}
  

假設每個Fighter都可以竊取敵人的武器,我為Fighter泛型協議添加一個steal(weapon:from:)方法。Fighter泛型協議該如何表示參數類型才能讓其使用者通過恰當的類型來聲明這個方法呢?

from:參數類型是該Fighter的Enemy。我們已經知道該如何表示它了:它是由佔位符、點符號以及關聯類型名構成的。這裡的佔位符就是該協議的使用者,即Self。因此,from:參數類型就是Self.Enemy。那麼weapon:參數類型又是什麼呢?它是Enemy的Weapon!因此,weapon:參數類型就是Self.Enemy.Weapon:


protocol Fighter : Superfighter {
    typealias Enemy : Superfighter
    func steal(weapon:Self.Enemy.Weapon, from:Self.Enemy)
}
  

(上述代碼可以編譯通過,省略Self表達的也是相同的含義。不過,Self依然是整個鏈條的隱式起始點,我覺得加上Self會讓代碼的含義變得更加清晰。)

如下Soldier與Archer的聲明正確地使用了Fighter協議,代碼會編譯通過:


struct Soldier : Fighter {
    typealias Weapon = Sword
    typealias Enemy = Archer
    func steal(weapon:Bow, from:Archer) {
    }
}
struct Archer : Fighter {
    typealias Weapon = Bow
    typealias Enemy = Soldier
    func steal (weapon:Sword, from:Soldier) {
    }
}
  

這個示例是假想出來的(但我希望能說明問題),不過其表示的概念卻不是。Swift頭文件大量使用了關聯類型鏈,關聯類型鏈Generator.Element使用得非常多,因為它表示了序列元素的類型。SequenceType泛型協議有一個關聯類型Generator,它被約束為泛型GeneratorType協議的使用者,反過來它會有一個關聯類型Element。

4.9.5 附加約束

簡單的類型約束會對類型進行限制,使其能夠將佔位符解析為單個類型。有時,你需要對可解析的類型做進一步的限制:這就需要附加約束了。

在泛型協議中,類型別名約束中的冒號與類型聲明中的冒號是一個意思。這樣,其後面可以跟著多個協議,或是後跟一個父類再加上多個協議:


class Dog {
}
class FlyingDog : Dog, Flier {
}
protocol Flier {
}
protocol Walker {
}
protocol Generic {
    typealias T : Flier, Walker
    typealias U : Dog, Flier
}
  

在Generic協議中,關聯類型T只能被解析為使用了Flier協議與Walker協議的類型,關聯類型U只能被解析為Dog(或Dog子類)並使用了Flier協議的類型。

在泛型函數或對像類型的尖括號中,這種語法是非法的;相反,你可以附加一個where字句,其中包含一個或多個逗號分隔的對所聲明的佔位符的附加約束:


func flyAndWalk<T where T:Flier, T:Walker> (f:T) {}
func flyAndWalk2<T where T:Flier, T:Dog> (f:T) {}
  

Where子句還可以對已經包含了佔位符的泛型協議的關聯類型進行附加限制,方式是使用關聯類型鏈(參見4.9.4節的介紹)。如下偽代碼表明了我的意圖:我省略了where子句的內容,將注意力放在where子句所限制的內容上:


protocol Flier {
    typealias Other
}
func flockTogether<T:Flier where T.Other /*???*/ > (f:T) {}
  

如你所見,佔位符T已經被限制為了一個Flier。Flier本身是個泛型協議,並且有一個關聯類型Other。這樣,無論什麼類型解析T,它都會解析Other。Where子句進一步限制了到底什麼類型可以解析T,這是通過限制可解析Other的類型來做到的。

我們可以對關聯類型鏈施加什麼限制呢?一種可能是與上述示例相同的限制,一個冒號,後跟它需要使用的協議;或是通過它必須繼承的類來做到這一點。如下示例使用了協議:


protocol Flier {
    typealias Other
}
struct Bird : Flier {
    typealias Other = String
}
struct Insect : Flier {
    typealias Other = Bird
}
func flockTogether<T:Flier where T.Other:Equatable> (f:T) {}
  

Bird與Insect都使用了Flier,不過這並不是說它們都可以作為flockTogether函數調用的參數。flockTogether函數可以通過Bird實參調用,因為Bird的Other關聯類型會被解析為String,而String使用了內建的Equatable協議。不過,flockTogether卻不能通過Insect實參調用,因為Insect的Other關聯類型會被解析為Bird,而Bird並沒有使用Equatable協議:


flockTogether(Bird) // okay
flockTogether(Insect) // compile error
  

如下示例使用了類:


protocol Flier {
    typealias Other
}
class Dog {
}
class NoisyDog : Dog {
}
struct Pig : Flier {
    typealias Other = NoisyDog // or Dog
}
func flockTogether<T:Flier where T.Other:Dog> (f:T) {}
  

flockTogether函數可以通過Pig實參調用,因為Pig使用了Flier,並且會將Other解析為Dog或Dog的子類:


flockTogether(Pig) // okay
  

除了冒號,我們還可以使用等號==並且後跟一個類型。關聯類型鏈最後的類型必須是這個精確的類型,而不能僅僅是協議使用者或子類。比如:


protocol Flier {
    typealias Other
}
protocol Walker {
}
struct Kiwi : Walker {
}
struct Bird : Flier {
    typealias Other = Kiwi
}
struct Insect : Flier {
    typealias Other = Walker
}
func flockTogether<T:Flier where T.Other == Walker> (f:T) {}
  

flockTogether函數可以通過Insect實參調用,因為Insect使用了Flier並且會將Other解析為Walker。不過,它不能通過Bird實參調用。Bird使用了Flier,並且會將Other解析為Walker的使用者,即Kiwi;不過,這並不滿足==限制。

在上一個示例中使用==Dog也會得到同樣的結果。如果Pig將Other解析為NoisyDog,那麼Pig實參就不再是可接受的了;Pig必須要將Other解析為Dog本身,這樣才能成為可接受的實參。

==運算符右側的類型本身可以是個關聯類型鏈。兩個鏈中末尾被解析出的類型必須要相同。比如:


protocol Flier {
    typealias Other
}
struct Bird : Flier {
    typealias Other = String
}
struct Insect : Flier {
    typealias Other = Int
}
func flockTwoTogether<T:Flier, U:Flier where T.Other == U.Other>
(f1:T, _ f2:U) {}
  

flockTwoTogether函數可以通過Bird與Bird調用,也可以通過Insect與Insect調用;不過,不能一個是Insect,另一個是Bird,因為它們不會將Other關聯類型解析為相同的類型。

Swift頭文件大量使用了帶有==運算符的where子句,特別是用它來限制序列類型。比如,String的appendContentsOf方法聲明了兩次,如下代碼所示:


mutating func appendContentsOf(other: String)
mutating func appendContentsOf<S : SequenceType
where S.Generator.Element == Character>(newElements: S)
  

第3章介紹過appendContentsOf可以將一個String連接到另一個String上。不過appendContentsOf並不僅僅可以將String連接到String上!字符序列也可以:


var s = "hello"
s.appendContentsOf(" world".characters) // "hello world"
  

Character數組也可以:


s.appendContentsOf(["!" as Character])
  

它們都是字符序列,第2個appendContentsOf方法聲明中的泛型指定了這一點。它是個序列,因為其類型使用了SequenceType協議。不過,並不是任何序列都可以;其Generator.Element關聯類型鏈必須要解析為Character。如前所述,Generator.Element鏈是Swift用於表示序列元素類型概念的一種方式。

Array結構體也有一個appendContentsOf方法,不過其聲明有些不同:


mutating func appendContentsOf<S : SequenceType
    where S.Generator.Element == Element>(newElements: S)
  

序列只能是一種類型。如果序列包含了String元素,那麼你可以向其添加更多的元素,但只能是String元素;你不能向String元素序列添加Int元素序列。數組是序列;它是個泛型,其Element佔位符是其元素的類型。因此,Array結構體在其appendContentsOf方法聲明中通過==運算符來強制使用這個規則:實參序列的元素類型必須要與現有數組的元素類型相同。