在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,代码很简单,不多做解释。