首页 » iOS编程基础:Swift、Xcode和Cocoa入门指南 » iOS编程基础:Swift、Xcode和Cocoa入门指南全文在线阅读

《iOS编程基础:Swift、Xcode和Cocoa入门指南》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 = Birdb.fly // flap flap flap  

使用者可以实现从协议扩展继承下来的方法,因此也可以重写这个方法:


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

不过你要知道,这种继承并不是多态。使用者的实现并非重写;它只不过是另一个实现而已。内在一致性原则并不适用;重要的是引用类型到底是什么:


let f : Flier = Insectf.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 = Insectf.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 // 1let 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中新增的。