讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 4.10 擴展 >

4.10 擴展

擴展是將自己的代碼注入其他地方聲明的對象類型中的一種方式;你所擴展的是一個已有的對象類型。你可以擴展自定義的對象類型,也可以擴展Swift或Cocoa的對象類型,在這種情況下,你實際上是將功能添加到了不屬於你自己的類型當中!

擴展聲明只能位於文件的頂部。要想聲明擴展,請使用關鍵字extension,後跟已有的對象類型名,然後可以添加冒號,後跟該類型需要使用的協議列表名(這一步是可選的),最後是花括號,裡面是通常的對象類型聲明的內容,其限制如下所示:

·擴展不能重寫已有的成員(不過它可以重載已有的方法)。

·擴展不能聲明存儲屬性(不過可以聲明計算屬性)。

·類的擴展不能聲明指定初始化器和析構器(不過可以聲明便捷初始化器)。

4.10.1 擴展對像類型

根據以往的經驗,我有時會擴展內建的Swift或Cocoa類型,從而以屬性或方法的形式封裝一些缺失的功能。如下示例來自於真實的應用。

在紙牌遊戲中,我需要洗牌,而紙牌會存儲在數組中。我會擴展內建的Array類型,並添加一個shuffle方法:


extension Array {
    mutating func shuffle  {
        for i in (0..<self.count).reverse {
            let ix1 = i
            let ix2 = Int(arc4random_uniform(UInt32(i+1)))
            (self[ix1], self[ix2]) = (self[ix2], self[ix1])
        }
    }
}
  

Cocoa的Core Graphics框架有很多有用的函數都與CGRect結構體有關,Swift已經擴展了CGRect,並添加了一些輔助性的屬性與方法;不過它並未提供獲取CGRect中心點(CGPoint)的便捷方法,這在實際開發中是經常需要的,於是我擴展了CGRect,為其添加一個center屬性:


extension CGRect {
    var center : CGPoint {
        return CGPointMake(self.midX, self.midY)
    }
}
  

擴展可以聲明靜態或類方法;由於對像類型通常都是全局可見的,所以這是給全局函數指定恰當命名空間的絕佳方式。比如,在我開發的一個應用中,我經常會使用某個顏色(UIColor)。相對於重複創建這個顏色,更好的方式是將生成它的代碼封裝到全局函數中。不過,相對於讓這個函數成為全局的,我使之成為UIColor的一個類方法,這麼做是非常恰當的:


extension UIColor {
    class func myGoldenColor -> UIColor {
        return self.init(red:1.000, green:0.894, blue:0.541, alpha:0.900)
    }
}
  

現在,我只需通過UIColor.myGolden()就可以在代碼中使用該顏色了,這與內建的類方法如UIColor.redColor()是非常相似的。

擴展的另一個用途是讓內建的Cocoa類能夠處理你的私有數據類型。比如,在我開發的Zotz應用中,我定義了一個枚舉,其原始值是歸檔或反歸檔Card屬性時所用的鍵字符串:


enum Archive : String {
    case Color = \"itsColor\"
    case Number = \"itsNumber\"
    case Shape = \"itsShape\"
    case Fill = \"itsFill\"
}
  

這裡唯一的問題在於為了在歸檔時能夠使用該枚舉,我每次都需要帶上其rawValue:


coder.encodeObject(s1, forKey:Archive.Color.rawValue)
coder.encodeObject(s2, forKey:Archive.Number.rawValue)
coder.encodeObject(s3, forKey:Archive.Shape.rawValue)
coder.encodeObject(s4, forKey:Archive.Fill.rawValue)
  

這麼做太醜陋了。優雅的解決辦法(WWDC 2015視頻中所推薦的做法)是告訴coder所屬的類NSCoder當forKey:參數是歸檔而非String時應該怎麼做。在擴展中,我重載了encodeObject:forKey:方法:


extension NSCoder {
    func encodeObject(objv: AnyObject?, forKey key: Archive) {
        self.encodeObject(objv, forKey:key.rawValue)
    }
}
  

實際上,我將對rawValue的調用從代碼中移出並放到了NSCoder的代碼中。現在,歸檔Card時就可以不調用rawValue了:


coder.encodeObject(s1, forKey:Archive.Color)
coder.encodeObject(s2, forKey:Archive.Number)
coder.encodeObject(s3, forKey:Archive.Shape)
coder.encodeObject(s4, forKey:Archive.Fill)
  

對自定義對像類型的擴展有助於代碼組織。經常應用的一個約定是為對像類型需要使用的每個協議添加擴展,比如:


class ViewController: UIViewController {
    // ... UIViewController method overrides go here ...
}
extension ViewController : UIPopoverPresentationControllerDelegate {
    // ... UIPopoverPresentationControllerDelegate methods go here ...
}
extension ViewController : UIToolbarDelegate {
    // ... UIToolbarDelegate methods go here ...
}
  

如果你認為多個小文件要比一個大文件好,那麼對自定義對像類型的擴展也是將該對像類型分散到多個文件中的一種方式。

在擴展Swift結構體時,初始化器會有一件奇怪的事情出現:我們可以聲明一個初始化器,同時又保留隱式初始化器:


struct Digit {
    var number : Int
}
extension Digit {
    init {
        self.init(number:42)
    }
}
  

上述代碼表示,你可以通過調用顯式聲明的初始化器Digit(),或是調用隱式初始化器Digit(number:7)來實例化一個Digit。因此,通過擴展顯式聲明的初始化器並不會導致隱式初始化器的丟失,如果在原來的結構體聲明中就聲明了相同的初始化器,那麼就會出現這種情況。

4.10.2 擴展協議

在Swift 2.0中,你可以對協議進行擴展。在擴展協議時,你可以向其中添加方法與屬性,就像擴展對像類型那樣。與協議聲明不同的是,這些方法與屬性並不僅僅要被協議使用者所實現,它們還是要被協議使用者所繼承的實際方法與屬性!比如:


protocol Flier {
}
extension Flier {
    func fly {
        print(\"flap flap flap\")
    }
}
struct Bird : Flier {
}
  

現在,Bird可以使用Flier而無須實現fly方法。即便我們將func fly()作為一種要求添加到了Flier協議的聲明中,Bird依然可以使用Flier而無須實現fly方法。這是因為Flier協議擴展支持fly方法!這樣,Bird就繼承了fly的實現:


let b = Bird
b.fly // flap flap flap
  

使用者可以實現從協議擴展繼承下來的方法,因此也可以重寫這個方法:


struct Insect : Flier {
    func fly {
        print(\"whirr\")
    }
}
let i = Insect
i.fly // whirr
  

不過你要知道,這種繼承並不是多態。使用者的實現並非重寫;它只不過是另一個實現而已。內在一致性原則並不適用;重要的是引用類型到底是什麼:


let f : Flier = Insect
f.fly // flap flap flap
  

雖然f本質上是個Insect(這一點通過is運算符可以看到),但fly消息卻發送給了類型為Flier的對象引用,因此調用的是fly方法的Flier實現而非Insect實現。

要想實現多態繼承,我們需要在原始協議中將fly聲明為必須要實現的方法:


protocol Flier {
    func fly // *
}
extension Flier {
    func fly {
        print(\"flap flap flap\")
    }
}
struct Insect : Flier {
    func fly {
        print(\"whirr\")
    }
}
  

現在,Insect會維護其內在一致性:


let f : Flier = Insect
f.fly // whirr
  

這種差別有其現實意義,因為協議使用者並不會引入(也不能引入)動態分派的開銷。因此,編譯器要做出靜態的決定。如果方法在原始協議中聲明為必須要實現的方法,那麼我們就可以確保使用者會實現它,因此可以調用(也只能這麼調用)使用者的實現。但如果方法只存在於協議擴展中,那麼決定使用者是否重新實現了它就需要運行期的動態分派,這違背了協議的本質,因此編譯器會將消息發送給協議擴展。

協議擴展的主要好處在於可以將代碼移到合適的範圍中。如下示例來自於我開發的Zotz應用。我有4個枚舉,每個都表示Card的一個特性:Fill、Color、Shape和Number。它們都有一個Int原始值。我已經對每次通過其原始值初始化這些枚舉時都要調用rawValue:感到厭煩,因此為每個枚舉添加了一個沒有外部參數名的委託初始化器,它會調用內建的init(rawValue:)初始化器:


enum Fill : Int {
    case Empty = 1
    case Solid
    case Hazy
    init?(_ what:Int) {
        self.init(rawValue:what)
    }
}
enum Color : Int {
    case Color1 = 1
    case Color2
    case Color3
    init?(_ what:Int) {
        self.init(rawValue:what)
    }
}
// ... and so on ...
  

我不喜歡重複初始化器聲明,不過在Swift 1.2及之前的版本中只能這麼做。在Swift 2.0中,我可以將其聲明移到協議擴展中。帶有原始值的枚舉會自動使用內建的泛型RawRepresentable協議,其中的原始值類型是個名為RawValue的類型別名。因此,我可以將初始化器放到RawRepresentable協議中:


extension RawRepresentable {
    init?(_ what:RawValue) {
        self.init(rawValue:what)
    }
}
enum Fill : Int {
    case Empty = 1
    case Solid
    case Hazy
}
enum Color : Int {
    case Color1 = 1
    case Color2
    case Color3
}
// ... and so on ...
  

在Swift標準庫中,協議擴展使得很多全局函數都可以轉換為方法。比如,在Swift 1.2及之前的版本中,enumerate(參見第3章)是個全局函數:


func enumerate<Seq:SequenceType>(base:Seq) -> EnumerateSequence<Seq>
  

enumerate是個全局函數,因為只能如此。該函數只能應用於序列,即SequenceType協議的使用者。在Swift 2.0之前該如何表示這一點呢?enumerate方法被聲明為SequenceType協議中必須要實現的方法,不過這僅僅意味著SequenceType的每個使用者都要實現它;協議本身無法提供實現。要想做到這一點,唯一的辦法就是全局函數,將序列作為參數,使用泛型約束來做好把控,因此實參只能是序列。

不過在Swift 2.0中,enumerate是個方法,聲明在SequenceType協議的擴展中:


extension SequenceType {
    func enumerate -> EnumerateSequence<Self>
}
  

現在沒必要再使用泛型約束了。沒必要使用泛型了。也沒必要使用參數了!它已經成為SequenceType中的方法;要枚舉的序列就是接收enumerate消息的那個序列。

以此類推,在Swift 2.0中,有大量Swift標準庫全局函數都變成了方法。這種轉變改變了語言的風格。

4.10.3 擴展泛型

在擴展泛型類型時,佔位符類型名對於擴展聲明來說是可見的。這很棒,因為你可能會用到它;不過,這會導致代碼變得令人困惑,因為你看起來在使用未定義的類型。添加註釋是個好主意,用來提醒自己要做的是什麼:


class Dog<T> {
    var name : T?
}
extension Dog {
    func sayYourName -> T? { // T is the type of self.name
        return self.name
    }
}
  

在Swift 2.0中,泛型類型擴展可以使用一個where子句。這與泛型約束的效果是一樣的:它會限制泛型的哪個解析者可以調用該擴展所注入的代碼,並向編譯器保證代碼對於這些解析者來說是合法的。

與協議擴展一樣,這意味著全局函數可以轉換為方法了。回憶一下本章之前的這個示例:


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

我為何要將其作為全局函數呢?因為在Swift 2.0之前,我只能這麼做。假設將其作為Array的一個方法。在Swift 1.2及之前的版本中,你可以擴展Array,擴展會引用到Array的泛型佔位符;不過,它無法對佔位符做進一步的約束。這樣,我們沒辦法在將方法注入Array的同時又確保佔位符是個Comparable,因此編譯器不允許對數組的元素使用<運算符。在Swift 2.0中,我可以進一步約束泛型佔位符,因此可以將其作為Array的一個方法:


extension Array where Element:Comparable { // Element is the element type
    func min -> Element {
        var minimum = self[0]
        for ix in 1..<self.count {
            if self[ix] < minimum {
                minimum = self[ix]
            }
        }
        return minimum
    }
}
  

該方法只能在Comparable元素的數組上調用;它不會注入其他類型的數組中,因此編譯器不允許下面這樣調用:


let m = [4,1,5,7,2].min // 1
let d = [Digit(12), Digit(42)].min // compile error
  

第2行代碼無法編譯通過,因為Digit結構體並未使用Comparable協議。

重申一次,Swift語言的這種變化導致Swift標準庫發生了大規模的重組,可以將全局函數移到結構體擴展與協議擴展中並作為方法。比如,Swift 1.2及之前版本中的全局函數find在Swift 2.0中成為CollectionType的indexOf方法;它是受約束的,這樣集合的元素就都是Equatable的,因為大海撈針是不可能的,除非你有辦法能找到針:


extension CollectionType where Generator.Element : Equatable {
    func indexOf(element: Self.Generator.Element) -> Self.Index?
}
  

這是個協議擴展,也是個帶有where子句約束的泛型擴展,這些特性都是Swift 2.0中新增的。