Swift - 基础之extension

swift-extension.jpg

在swift中,extension与Objective-C的category有点类似,但是extension比起category来说更加强大和灵活,它不仅可以扩展某种类型或结构体的方法,同时它还可以与protocol等结合使用,编写出更加灵活和强大的代码。

0. 概述 - extension summary

在swift中,swift可以为特定的class, strut, enum或者protocol添加新的特性。当你没有权限对源代码进行改造的时候,此时可以通过extension来对类型进行扩展。extension有点类似于OC的类别 -- category,但稍微不同的是category有名字,而extension没有名字。

swift的extension可以做如下几件事,

  • 添加计算属性 - computed properties
  • 添加方法 - methods
  • 添加初始化方法 - initializers
  • 添加附属脚本 - subscripts
  • 添加并使用嵌套类型 - nested types
  • 遵循并实现某一协议 - conform protocol

在swift中,你甚至可以对一个协议protocol进行扩展,实现其协议方法或添加额外的功能,以便于实现该协议的类型可以使用,在swift中,这叫做协议扩展 - protocol extension,后面的内容会举例说明。

注意:extension可以为类型添加新的特性,但是它不能覆盖已有的特性。例如Animal已经有eat的方法,我们不能使用extension覆盖Animal的eat方法。

1. 语法 - extension syntax

定义extension的语法非常简单,只需要使用extension关键字,如下代码,

extension SomeType {
    // new functionality to add to SomeType goes here
}

extension可以让一个特定的类型实现一个或多个协议,也就是说无论对于class, structure或enum等类型而言,都可以实现一个或多个协议,如下代码所示,

extension SomeType: SomeProtocol, AnotherProtocol {
    // implementation of protocol requirements goes here
}

上面的代码表示了简单的功能,即SomeType服从了SomeProtocol和AnotherProtocol协议,并实现两个协议中的方法,我们可以对上面的代码进行简单的拆分,让代码更加简洁易读,如下代码,

extension SomeType: SomeProtocol {
    // implentations of SomeProtocol
}

extension SomeType: AnotherProtocol {
    // implentations of AnotherProtocol
}

2. 添加多种特性

在前面已经列举了extension可以为类型添加诸多特性,下面逐一做简单解释,并举例说明。

2.1 添加计算属性 - computed properties

extension可以为已经存在的类型添加计算属性(computed properties),下面的demo为swift内置的Double类型添加了5个计算属性,分别是km, m, cm, mm, ft,用来提供基础的计算距离的功能,如下代码所示,

extension Double {
    var km: Double { return self * 1_000.0 }
    var m: Double { return self }
    var cm: Double { return self / 100.0 }
    var mm: Double { return self / 1_000.0 }
    var ft: Double { return self / 3.28084 }
}

// usage of Double extension
let oneInch = 25.4.mm
print("One inch is \(oneInch) meters")
// Prints "One inch is 0.0254 meters"
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// Prints "Three feet is 0.914399970739201 meters"

什么是计算属性呢?这大概算是swift语言的特性吧,在swift中属性有两种类型,一种是存储属性,另一种是计算属性。存储属性就是存储在特定的class, struct中的一个常量或变量,可以在定义存储属性的时候指定默认值,也可以在构造过程中设置或修改存储属性的值,需要注意的是enum中并不能定义存储属性;而计算属性不直接存储值,而是提供一个getter, setter来间接获取和设置其他属性和变量的值。

归纳一下,swift中存储属性和计算属性的区别如下表,

存储属性 计算属性
存储常量或变量属性 用来计算数值,不是存储数值
定义在class, struct中 定义在class, struct, enum中

在上面的demo中,这些计算属性表示一个Double值应该被当做一个特定的长度单位,即千米、米、厘米、毫米等。虽然它们被当做计算属性来实现,但可以将这些属性名称通过.操作符放在浮点值的后面。Double类型的浮点值1.0表示“一米”,这也是为什么.m计算属性返回了该Double值本身。同样,1米约等于3.28084寸(feet),所以ft计算属性转换成"米"时候需要乘以3.28084。

这些属性都是只读(read-only)的计算属性,所以为了方便,在定义它们的时候只需要使用get关键字,不过为了简洁和直观,这个demo直接省略了get关键字。这些计算属性的返回值是Double类型,所以可以用在任何Double类型适用的地方进行数学计算,例如下面的代码,

let aMarathon = 42.km + 195.m
print("A marathon is \(aMarathon) meters long")
// Prints "A marathon is 42195.0 meters long"

注意:extension可以添加计算属性,但是不能添加存储属性,也不能为当前属性添加观察者。

2.2 添加构造器 - initializers

extension可以为已经存在的类(class)添加便捷初始化方法。这样你就可以对其他的类型进行扩展,以便该类型在初始化时可以接受你自定义的类型作为初始化的参数,这样就为该类型添加了额外的初始化方法,在创建该类型的对象时提供了更多的选择。

下面的例子定义了一个结构体类型Rect,它表示一个长方形几何图形;同样我们定义了另外两个结构体类型Size和Point,两者的所有属性都设置了0.0默认值。如下代码所示,

struct Size {
    var width = 0.0, height = 0.0
}
struct Point {
    var x = 0.0, y = 0.0
}
struct Rect {
    var origin = Point()
    var size = Size()
}

因为结构体Rect为它所有属性都提供了默认值,所以它自动拥有一个默认初始化方法和一个成员逐一(memberwise)初始化方法,我们可以使用这两个初始化方法来创建Rect结构体的实例,如下代码,

let defaultRect = Rect()
let memberwiseRect = Rect(origin: Point(x: 2.0, y: 2.0), size: Size(width: 5.0, height: 5.0))

现在我们可以通过extension为结构体Rect添加一个额外的初始化方法,该初始化方法接受一个point和size作为参数,如下代码,

extension Rect {
    init(center: Point, size: Size) {
        let originX = center.x - (size.width / 2)
        let originY = center.y - (size.height / 2)
        self.init(origin: Point(x: originX, y: originY), size: size)
    }
}

这个自定义的初始化方法实现的逻辑并不复杂,首先它通过传入的Point参数center和Size参数size计算出(originX, originY),然后调用该结构体的逐一成员初始化方法,即init(origin:size:),该初始化方法将origin和size参数存储在对应的属性中,如下代码所示,

let centerRect = Rect(center: Point(x: 4.0, y: 4.0), size: Size(width: 3.0, height: 3.0))
// centerRect's origin is (2.5, 2.5) and its size is (3.0, 3.0)

注意:如果你使用extension来添加初始化方法,你同样需要保证该类型的初始化方法结束时,它的每一个属性被完全的初始化了。

2.3.1 添加方法 - methods

跟OC的category类似,通过extension,可以为某一类型添加实例方法(instance method)和类型方法(type method),如下代码为Int类型添加了一个名为repetitions的实例方法,

extension Int {
    func repetitions(task: () -> Void) {
        for _ in 0..<self {
            task()
        }
    }
}

新增的repetitions(task:)方法接受一个闭包()->Void作为参数,该闭包作为一个函数,并且该函数没有入参和返回值。

如下代码所示,我们来进行一个简单的测试,

3.repetitions {
    print("Hello!")
}
// Hello!
// Hello!
// Hello!

2.3.2 添加突变方法 - mutating method

通过extension添加的实例方法同样可以修改(modify)或突变(mutate)该实例本身,如果结构体和枚举定义的方法想要改变自身或自身的属性,那么该实例方法必须被标记为突变(mutating)的。

下面的例子为Int类型添加了一个名为square的突变方法,它的作用是计算原始值的平方,如下代码所示,

extension Int {
    mutating func square() {
        self = self * self
    }
}

var someInt = 3
someInt.square()
// someInt is now 9

这里针对mutating关键字再啰嗦一句,如果我们把mutating关键字删除,则编译器会报错,只有mutating修饰的方法才能更改实例属性和实例本身,mutating关键字与extension, protocol结合使用,可以用更简洁的代码实现更复杂的功能。笔者建议读者搜索资料,写写demo,加深印象和理解。

2.4 添加附属脚本 - subscripts

extension可以为某一个特定类型添加附属脚本subscript。那么什么是附属脚本呢?附属脚本可以定义在class, struct, enum中,可以认为是访问对象,集合或序列的快捷方式,不需要在调用实例的特定的赋值方法和访问方法。举例来说,用附属脚本访问一个Array实例中的元素可以写为someArray[index],访问Dictionary实例中的元素可以写为someDictionary[key],读者可能已经注意到,通过这种快捷方式对siwft中Array和Dictionary元素进行访问我们经常使用,所以可以推断,swift已经默认帮开发者实现了附属脚本的特性。

下面的例子为Int类型添加了整数下标的附属脚本,该附属脚本[n]返回该数字对应的十进制(decimal)的第n位的数字,当然计数方式从最右侧开始,例如,123456789[0]返回9,而123456789[1]返回8,该方法的具体实现如下代码所示,

extension Int {
    subscript(digitIndex: Int) -> Int {
        var decimalBase = 1
        for _ in 0..<digitIndex {
            decimalBase *= 10
        }
        return (self / decimalBase) % 10
    }
}

746381295[0]
// returns 5
746381295[1]
// returns 9
746381295[2]
// returns 2
746381295[8]
// returns 7

仔细分析一下subscript方法,实现的逻辑其实就是小学生的练习题,这里不做赘述。

如果一个Int数字没有足够多的位数,那么它会在最左边添加0来补全,然后返回0给调用方,如下代码,

746381295[9]
// returns 0, as if you had requested:
0746381295[9]

2.5 添加嵌套类型 - nested types

extension可以为类(class)、结构体(structure)和枚举(enumation)添加嵌套类型,如下代码,

extension Int {
    enum Kind {
        case negative, zero, positive
    }
    var kind: Kind {
        switch self {
        case 0:
            return .zero
        case let x where x > 0:
            return .positive
        default:
            return .negative
        }
    }
}

上面的demo为Int添加了嵌套的枚举类型,这个枚举名为Kind,用来表示一个数字是正数、复数还是0,之所以说是嵌套,是因为该枚举定义在Int的extension内部。(这里,我可能对嵌套的理解有误,这段理解暂时保留,欢迎读者指正。)

这个demo还为Int添加了一个计算属性(computed property),名为kind,针对数字的值不同,分别返回.zero, .positive或.negative。

现在该嵌套的枚举类型可以在任意的Int值中使用,如下代码所示,

func printIntegerKinds(_ numbers: [Int]) {
    for number in numbers {
        switch number.kind {
        case .negative:
            print("- ", terminator: "")
        case .zero:
            print("0 ", terminator: "")
        case .positive:
            print("+ ", terminator: "")
        }
    }
    print("")
}
printIntegerKinds([3, 19, -27, 0, -6, 0, 7])
// Prints "+ + - 0 - 0 + "

上面的代码简单易懂,printIntegerKinds(_:)接受一个Int类型的数组,然后遍历数组并判断每个元素的kind来判断数组元素的正、负还是0,代码很简单,不多做解释。

3. 参考链接

推荐阅读更多精彩内容