刘毅的技术博客

记录自己的学习生活点滴,也希望和大家交流分享!

Swifter读书笔记1

继续Swift的进阶学习,这一阶段使用的是王巍(onevcat)的Swifter一书,书中记录了100个Swift开发的Tips,本章包含了Tip1~Tip20。

1.柯里化(Currying)

Currying我们在之前的Swift by Tutorials中也学习过,是一种可以使函数分布调用的方式,也可以充当产生新方法的工厂方法,例如:

1
2
3
4
5
6
7
8
func greaterThan(comparor: Int)(Input: Int) -> Bool {
    return Input > comparor
}

let greaterThan10 = greaterThan(10)

greaterThan10(Input: 12)
greaterThan10(Input: 1)

作者举了一个实践的例子,我们之前也讲过Swift中的Selector只能通过String类型来赋值,从实用角度讲,是很好的与OC的Target/Action做了桥接,但是任然无法保证调用的安全性,用例就是借助Currying对selector调用做了改造,这篇博客做了详细说明。

首先通过以下的例子,我们可以得知,一个类的方法也是可以被获取的,这因为Swift中方法是第一类对象,例如最后depositor的调用,它的类型就是BankAccount –> (Double) –> (),其实也是currying调用,利用这一特性,博主自己对selector的调用进行了改造。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class BankAccount {
    var balance = 0.0

    func deposit(amount: Double) {
        balance += amount
    }
}

let account = BankAccount()
account.deposit(100)
print(account.balance)

let depositor = BankAccount.deposit
depositor(account)(100)
print(account.balance)

这样改变了selector的调用方式,现在开发者需要传入target和一个(T) –> () –> ()类型的action,而不采用之前的字符串输入,这当然保证了该方法一定会被响应,因为如果,该类型没有这个方法,在编译期间就会报错,而不会是在runtime中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
protocol TargetAction {
    func performAction()
}

struct TargetActionWrapper<T: AnyObject>: TargetAction {
    weak var target: T?
    let action: (T) -> () -> ()

    func performAction() -> () {
        if let t = target {
            action(t)()
        }
    }
}

enum ControlEvent {
    case TouchUpInside
    case ValueChanged
    //...
}

class Control {
    var actions = [ControlEvent: TargetAction]()

    func setTarget<T: AnyObject>(target: T, action: (T) -> () -> (), controlEvent: ControlEvent) {
        actions[controlEvent] = TargetActionWrapper(target: target, action: action)
    }

    func removeTargetForCotrolEvent(controlEvent: ControlEvent) {
        actions[controlEvent] = nil
    }

    func performActionForControlEvent(controlEvent: ControlEvent) {
        actions[controlEvent]?.performAction()
    }
}
1
2
3
4
5
6
7
8
9
10
11
class MyViewController {
    let button = Control()

    func viewDidLoad() {
        button.setTarget(self, action: MyViewController.onButtonTap, controlEvent: .TouchUpInside)
    }

    func onButtonTap() {
        println("Button was tapped")
    }
}

2.Struct Mutable的方法

这个问题,之前的Swift by Tutorials也提到过,结构体的实例方法是不能修改自己声明的变量的,因为Struct是数值型的,默认是不可变的,所以如果某个方法需要修改结构体的变量,就需要添加mutating关键字,使得编译器可以对结构体进行copy-on-write操作。

1
2
3
4
5
6
7
8
struct User {
    var age: Int
    var weight: Int
    var height: Int
    mutating func gainWeight(newWeight: Int) {
        weight += newWeight
    }
}

3.将protocol的方法声明为mutating

这个问题是承接上一个问题的,Swift中的protocol是可以被class、struct、enum实现的,所以如果protocol中一些方法正好会修改到自身变量,但没有加mutating关键字的话,struct和enum实现该方法时就无法编过,所以一定要在设计protocol时考虑这个问题。另外,我补充一点,如果这个protocol只是给class实现的,那么可以不加mutating,同时也可以使用protocol ClassOnlyProtocol: class这样的写法,告诉使用者,该协议只能实现于class。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import UIKit

protocol Vehicle {
    var numberOfWheels: Int {get}
    var color: UIColor {get set}

    mutating func changeColor()
}

struct MyCar: Vehicle {
    let numberOfWheels = 4
    var color = UIColor.blueColor()

    mutating func changeColor() {
        color = UIColor.redColor()
    }
}

4.Sequence

在Swift中,只要实现了SequenceType,就可以使用for…in快速遍历,而需要先实现一个GeneratorType来控制遍历的顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class ReverseGenerator: GeneratorType {
    typealias Element = Int

    var counter: Element
    init<T>(array: [T]) {
        self.counter = array.count - 1
    }

    init(start: Int) {
        self.counter = start
    }

    func next() -> Element? {
        return self.counter < 0 ? nil : counter--
    }
}

struct ReverseSequence<T>: SequenceType {
    var array: [T]

    init (array: [T]) {
        self.array = array
    }

    typealias Generator = ReverseGenerator

    func generate() -> Generator {
        return ReverseGenerator(array: array)
    }
}

let arr = [0, 1, 2, 3, 4]

for i in ReverseSequence(array: arr) {
    print("Index \(i) is \(arr[i])")
}

for…in如何利用这两个协议,作者给出了一个解释。

1
2
3
4
var g = array.generate()
while let obj = g.next() {
  print(obj)
}

而且你也可以免费使用map,filter,reduce这些函数,因为SequenceType的几个extension实现了他们。关于这几个函数的使用,我们在Swift by Tutorials做了很多介绍。

5.多元组(Tuple)

Tuple我们之前也介绍过,主要用于简单场景的复合数据类型,作者举了一个CGRect的方法在OC和Swift中的实践。

CGRect有个函数CGRectDivide(),用来切割矩形区域,由于会返回两个新的CGRect,而OC又没有返回多个结果的机制,只能先声明两个CGRect,然后将指针传入函数,等待函数对其赋值。

1
2
3
4
5
//CGRectDivide(CGRect rect, CGRect *slice, CGRect *remainder, CGFloat amount, CGRectEdge edge)
CGRect rect = CGRectMake(0, 0, 100, 100);
CGRect small;
CGRect large;
CGRectDivide(rect, &small, &large, 20, CGRectMinXEdge);

而在Swift中由于有了元组,可以优雅的进行实现,同时,由于Swift中结构体可以添加方法,所以将原来的函数直接作为了CGRect的一个方法,使得实现更加简洁。

1
2
3
//func rectsByDividing(atDistance: CGFloat, fromEdge: CGRectEdge) -> (slice: CGRect, remainder: CGRect)
let rect = CGRectMake(0, 0, 100, 100)
let (small, large) = rect.rectsByDividing(20, fromEdge: .MinXEdge)

6.@autoclosure和??

@autoclosure是简化代码的一个关键字,之前说过方法是Swift中的第一类对象,所以会在Swift中出现大量的匿名方法,即闭包closure作为参数,如下例。

1
2
3
4
5
6
func logIfTrue(@autoclosure predicate:() -> Bool) {
    if predicate() {
        print("True")
    }
}
logIfTrue({return 2>1})

当然按照之前介绍的简化策略,上面的调用可以简化为:

1
logIfTrue{2>1}

然而我们可以在方法定义时加上@autoclosure,这样调用时,Swift会自动将输入的参数转化为closure,但需要注意的是使用@autoclosure关键字时,闭包的参数只能是(),如果包含参数时,不能使用该关键字。

1
2
3
4
5
6
func logIfTrue(@autoclosure predicate:() -> Bool) {
    if predicate() {
        print("True")
    }
}
logIfTrue(2>1)

??可以用来对Optional类型进行空值判断,并赋默认值,用法如下:

1
2
3
var level: Int?
var startLevel = 1
var currentLevel = level ?? startLevel

??的具体实现如下,两个版本的区别是默认值是否是optional类型,但是需要注意的是输入的第二个参数被封装成了一个闭包的类型,作者猜测了??方法的具体实现。

1
2
3
4
@warn_unused_result
public func ??<T>(optional: T?, @autoclosure defaultValue: () throws -> T?) rethrows -> T?
@warn_unused_result
public func ??<T>(optional: T?, @autoclosure defaultValue: () throws -> T) rethrows -> T
1
2
3
4
5
6
7
8
func ??<T>(optional: T?, @autoclosure defaultValue: () -> T) -> T {
    switch optional {
        case .Some(let value):
            return value
        case .None:
            return defaultValue()
    }
}

这里没有直接用T类型作为默认值的参数,而是封转了一个方法,这是一种优化手段。这样,系统在开始判断optional是否有值之前就不需要开辟默认值的内存,只有在optional为nil时,才去准备默认值。但是在开发者调用??方法时又不需要输入一个闭包参数,而仍是一个数值,这就是巧妙的利用了@autoclosure关键字。

作者留出了关于Swift的||和&&的实现,其实他们的情况也和??一样,需要分布判断,下面是我给出的答案。

1
2
3
4
5
6
7
8
func ||<T : BooleanType, U : BooleanType>(lhs: T, @autoclosure rhs: () -> U) -> Bool {
    switch lhs.boolValue {
    case true:
        return true
    case false:
        return rhs().boolValue
    }
}
1
2
3
4
5
6
7
8
func &&<T : BooleanType, U : BooleanType>(lhs: T, @autoclosure rhs: () -> U) -> Bool {
    switch lhs.boolValue {
    case false:
        return false
    case true:
        return rhs().boolValue
    }
}

7.Optional Chaining

Optional Chaining在之前也介绍过,可以摆脱Swift中不必要的有值判断,但使用时也需注意其返回值和执行方法的不确定性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Toy {
    let name: String
    init(name: String) {
        self.name = name
    }
}

class Pet {
    var toy: Toy?
}

class Child {
    var pet: Pet?
}

let xiaoming = Child()
let toyName = xiaoming.pet?.toy?.name

如上,虽然name定义为toy的String类型,非Optional,但由于Optional Chaining的使用,是可能提前返回nil的,所以最终toyName的类型,还是String?。

实际开发中,一般还是用Optional Binding+Optional Chaining的方式来取值。

1
2
3
if let toyname = xiaoming.pet?.toy?.name {
    print(toyname)
}

如果,我们给Toy再加一个扩展:

1
2
3
4
5
extension Toy {
    func play() {
        //...
    }
}

如果我们想将play()封装成一个以Child为参数的closure,那么就要注意closure的返回类型了。以下列出了错误和正确的写法。

1
2
3
4
//wrong
//let playClosure = {(child: Child) -> () in child.pet?.toy?.play()}
//right
let playClosure = {(child: Child) -> ()? in child.pet?.toy?.play()}

由于toy之前的值的不确定性,所以play()不一定可以正常执行,所以应该是()?类型,或者Void?类型,最后列出我们实际使用的场景,依然使用Optional Binding。

1
2
3
if let result: () = playClosure(xiaoming) {
    //...
}

8.操作符

Swift与OC的一大区别是支持操作符的重载,比如我们需要定义以下结构体的相加运算,我们可以重载加号操作符。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Vector2D {
    var x = 0.0
    var y = 0.0
}

func +(left: Vector2D, right: Vector2D) -> Vector2D {
    return Vector2D(x: left.x+right.x, y: left.y+right.y)
}

let v1 = Vector2D(x: 2.0, y: 3.0)
let v2 = Vector2D(x: 1.0, y: 4.0)
let v3 = v1+v2
print(v3)

类似的,我们也可以重载减号(-),负号(-)这样的运算符。

但是如果我们要定义一个全新的运算符,比如点积运算,单纯重载是无效的,因为该方法并不存在于Swift之中,需要我们先声明,如下:

1
2
3
4
infix operator +* {
    associativity none
    precedence 160
}

infix表示这是一个中位操作符,即前后都是输入,其他修饰子还包括prefix(一个输入,并操作符作用于输入之前,类似++i,!i)和postfix(一个输入,并操作符作用于输入之后,类似i++,i—)。

associativity定义的事结合律,就是计算顺序,比如常见的加法和减法就是left,我们这里定义的点积的结果是Double,不会再和其他点积结合,所以就是none。

precedence是运算的优先级,越高的优先级优先进行运算,Swift中乘除法是150,加减法是140,这里的点积是160,是优先于乘除法的。

之后就可以定义点积运算了。

1
2
3
4
5
func +* (left: Vector2D, right: Vector2D) -> Double {
    return left.x*right.x+left.y*right.y
}

let result = v1 +* v2

需要注意的是,重载的操作符都是函数,所以是全局的,也就是说多次重载会导致冲突,尤其是多个module同时重载时。所以应该尽量将其作为其他某个方法的简便写法,避免实现大量逻辑或提供独一无二的功能,因为这样即使出现冲突,使用者还可以通过方法名调用的方式来使用,总之,重载操作符的特性不要滥用,除非确实存在大量并全局的需求。

9.func的参数修饰

这一问题在之前也说过,一般我们声明Swift函数时都不会添加参数修饰符,如:

1
2
3
func incrementor(variable: Int) -> Int {
    return variable + 1
}

而很多人可能不会这么写,因为有++这样的运算符,一般都会这么写:

1
2
3
func incrementor(variable: Int) -> Int {
    return ++variable
}

然而直接报错,原因是Swift默认参数是let型的,所以++对自己会重新赋值,所以会报错,修改也很简单。修改参数为var就好。

1
2
3
4
5
6
7
8
func incrementor(var variable: Int) -> Int {
    return ++variable
}

var putIn = 3
let resultOut = incrementor(putIn)
print(resultOut)
print(putIn)

但在我们使用时会发现,返回值没错,但是作为输入的putIn并没有改变,这一原因是即使我们将参数声明为var,但Swift在使用之前还是会将其copy一份,然后对其copy修改,所以实际输入的变量并不会改变。

如果我们想对输入参数在方法内修改,就要使用inout关键字,这样我们就将参数的指针输入了方法,所以在调用时也需要传入参数的指针,其实这与OC中的block使用外部变量是一样的,默认就是值拷贝,无法修改原值,如需修改,在原值加_block关键字。

1
2
3
4
5
6
7
func incrementor(inout variable: Int) {
    ++variable
}

var putIn = 3
incrementor(&putIn)
print(putIn)

作者补充的一点是,参数修饰符是有传递限制性的,入股统一参数要跨层调用,需要保证同一参数的修饰统一,如下例:

1
2
3
4
5
6
func makeIncrementor(addNumber: Int) -> ((inout variable: Int) -> ()) {
    func incrementor(inout variable: Int) -> () {
        variable + addNumber
    }
    return incrementor
}

10.字面量转换

字面量就是指特定的数字,字符串或布尔值这样可以直接表明类型,并直接赋值的值。如下:

1
2
3
4
5
let aNumber = 3
let aString = "Hello"
let aBool = true
let anArray = [1, 2, 3]
let aDictionary = ["key1": "value1", "key2": "value2"]

Swift提供了一组协议,用来将字面量转换为特定类型,对于实现了字面量转换接口的类型,在字面量赋值时,可以直接通过赋值的方式,将其转化为对应类型。这些协议有:

  • ArrayLiteralConvertible
  • BooleanLiteralConvertible
  • DictionaryLiteralConvertible
  • FloatLiteralConvertible
  • NilLiteralConvertible
  • IntegerLiteralConvertible
  • StringLiteralConvertible

下面举例BooleanLiteralConvertible的实现,BooleanLiteralConvertible的定义是:

1
2
3
4
5
protocol BooleanLiteralConvertible {
  typealias BooleanLiteralConvertible
  
  init(booleanLiteral value: BooleanLiteralType)
}

BooleanLiteralConvertible在Swift中已有定义:

1
typealias BooleanLiteralType = Bool

那么我们自己实现字面量转换时,就需要实现init方法就好了,这样我们就可以直接使用true和false对MyBool类型赋值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum MyBool: Int {
    case myTrue, myFalse
}

extension MyBool: BooleanLiteralConvertible {
    init(booleanLiteral value: Self.BooleanLiteralType) {
        self = value ? myTrue : myFalse
    }
}

let myTrue: MyBool = true
let myFalse: MyBool = false

myTrue.rawValue
myFalse.rawValue

BooleanLiteralConvertible是这些协议中最简单的了,如果是StringLiteralConvertible协议,还要实现另外两个方法,这两个方法我们一般开发中不会遇到,是对应的字符簇和字符的字面量转换。

  • ExtendedGraphemeClusterLiteralConvertible
  • UnicodeScalarLiteralConvertible

继续举例比如,我们声明一个Person类,想通过String赋值,来生成Person对象,那么可以这样实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person: StringLiteralConvertible {
    let name: String
    init(name value: String) {
        self.name = value
    }

    required init(stringLiteral value: String) {
        self.name = value
    }

    required init(extendedGraphemeClusterLiteral value: String) {
        self.name = value
    }

    required init(unicodeScalarLiteral value: String) {
        self.name = value
    }
}

下面三个init方法都加上了required关键字,这是由初始化方法的完备性需求所决定的,该类的子类都需要保证能实现类似的字面量转换,以确保类型安全。

上面多次调用了self.name = value,其实是不必要的,我们可以在之后的每个init中调用最初的init方法,当然这需要在其他init方法前加convenience关键字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person: StringLiteralConvertible {
    let name: String
    init(name value: String) {
        self.name = value
    }

    required convenience init(stringLiteral value: String) {
        self.init(name: value)
    }

    required convenience init(extendedGraphemeClusterLiteral value: String) {
        self.init(name: value)
    }

    required convenience init(unicodeScalarLiteral value: String) {
        self.init(name: value)
    }
}

let p: Person = "xiaoming"
print(p.name)

Person例子中,没有像MyBool一样,使用extension来扩展,因为在extension中,我们无法定义required的初始化方法,也就是说我们无法为非final的class添加字面量转换。

综上,字面量转换是一个强大的功能,使用得当对代码简洁度有很大提高,但是对于使用者来讲,如果没有详细的文档支持,会造成很多困惑,所以它的最佳实践还有待开发者们继续挖掘。

11.下标

Swift中的Array和Dictionary是支持下标读写的,这一点与新版OC是一致的。

1
2
3
4
5
6
7
var arr = [1, 2, 3]
arr[2]
arr[2] = 4

var dict = ["cat":"meow", "goat":"mie"]
dict["cat"]
dict["cat"] = "miao"

而Swift比较先进的是,Swift是允许自定义下标的,比如作者给出的,使用subscript关键字为Array添加支持以[Int]为index来读取的extension。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
extension Array {
    subscript(input: [Int]) -> ArraySlice<Element> {
        get {
            var result = ArraySlice<Element>()
            for i in input {
                assert(i < self.count, "index out of range")
                result.append(self[i])
            }
            return result
        }
        set {
            for (index, i) in input.enumerate() {
                assert(i < self.count, "index out of range")
                self[i] = newValue[index]
            }
        }
    }
}

var arr = [1, 2, 3, 4, 5]
arr[[0, 2, 3]]
arr[[0, 2, 3]] = [-1, -3, -4]
arr

12.方法嵌套

方法在Swift中成为了第一对象,可以作为参数,可以作为结果返回,而且可以在方法中定义子方法,这对内容过长的方法进行重构提供了新的思路。如果子方法只会被调用一次,那么它更适合被声明到父方法内部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func appendQuery(var url: String, key: String, value: AnyObject) -> String {
    func appendQueryDictionary(var url: String, key: String, value: [String: AnyObject]) {
        //...
        return result
    }

    func appendQueryArray(var url: String, key: String, value: [AnyObject]) {
        //...
        return result
    }

    func appendQuerySingle(var url: String, key: String, value: AnyObject) {
        //...
        return result
    }

    if let dictionary = value as? [String: AnyObject] {
        return appendQueryDictionary(url, key: key, value: dictionary)
    } else if let array = value as? [AnyObject] {
        return appendQueryArray(url, key: key, value: array)
    } else {
        return appendQuerySingle(url, key: key, value: value)
    }
}

还有在方法模板中(就是在Swift By Tutorials中提到的Partial application),开发者一方面希望提供一个模板来让使用者定制方法,而同时不希望暴露太多的的实现细节。

1
2
3
4
5
6
func makeIncrementor(addNumber: Int) -> ((inout Int) -> Void {
    func incrementor(inout variable: Int) -> Void {
        variable += addNumber;
    }
    return incrementor;
}

13.命名空间

OC是没有命名空间的,开发时只能通过自己添加前缀来区分,但是还是有可能出现类文件相同的情况,而另一种情况是,一些第三方库本身引用了其他第三方库,而开发者同时使用这两个库时,就会出现冲突。

Swift在一定程度上解决了这一问题,目前Swift支持基于module的命名空间,也就是说根据不同的Target,可以有相同的类型名称,而一个target中还是不能有相同的类型。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//MyClass.swift
//该文件在app的主Target中
class MyClass {
    class func hello() {
        print("hello from app")
    }
}
//MyClass.swift
//该文件存在于NameSpaceFramework中
public class MyClass {
    public class func hello() {
      print("hello from framework")
    }
}

使用时如果出现冲突,可以使用Target名称作为前缀,如:

1
2
MyClass.hello()
NameSpaceFramework.MyClass.hello()

另一种方法是通过类型嵌套来指定访问范围,将名字重复的类型定义到不同的struct中,从而达到不使用多个module,也可以重复定义类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct MyClassContainer1 {
       class MyClass {
           class func hello() {
               print("hello from MyClassContainer1")
           }
       }
}
struct MyClassContainer2 {
       class MyClass {
           class func hello() {
               print("hello from MyClassContainer2")
       }
   }
}

使用时,

1
2
MyClassContainer1.MyClass.hello()
MyClassContainer2.MyClass.hello()

14.Any和AnyObject

Any和AnyObject是Swift的两个妥协的产物,因为OC是动态语言,不对类型进行严格的检查,所以会有id这种类型存在,并大量使用。而Swift是一种对类型严格检验的语言,只是为了与Cocoa协作,将原来的id用AnyObject来代替(实际上应该是AnyObject?类型,因为id可以为nil)。

AnyObject可以代表任何class类型的实例

Any可以代表任意类型,甚至包括方法(func)类型

但使用时,作者还是推荐我们对AnyObject?类型的实例,进行常规的拆解,再使用,确保安全性,例如:

1
2
3
4
5
6
7
8
9
10
func someMethod() -> AnyObject {
    //...
    return result
}

let anyObject : AnyObject? = SomeClass.someMethod()
if let someInstance = anyObject as? SomeRealClass {
    //...
    someInstance.funcOfSomeRealClass()
}

实际上AnyObject是一个protocol,所有的class都隐式实现了这一接口,这也是AnyObject只适用于class的原因,而Swift中的Array和Dictionary是struct类型,是不能用AnyObject来表示的。所以产生了Any这个类型,Any可以表示所有类型,包括class、struct、enum等。

使用Swift原生类型是可以提高性能的,所以代码中大量出现AnyObject和Any时,有必要重新检查代码,因为Swift中强调类型安全,这意味着代码结构和设计出现了问题。

15.typealias和泛型接口

typealias最常见的使用在于对于一些类型重新命名来增加可读性,如下例,使用typealias对CGPoint和Double重新命名后,使得代码更符合其代表的意义,如果在大型工程中对代码可读性的提高是很明显的。

1
2
3
4
5
6
7
8
func distanceBetweenPoint(point: CGPoint, toPoint: CGPoint) -> Double {
    let dx = Double(toPoint.x - point.x)
    let dy = Double(toPoint.y - point.y)
    return sqrt(dx * dx + dy * dy)
}
let origin: CGPoint = CGPoint(x: 0, y: 0)
let point: CGPoint = CGPoint(x: 1, y: 1)
let distance: Double =  distanceBetweenPoint(origin, toPoint: point)
1
2
3
4
5
6
7
8
9
10
11
typealias Location = CGPoint
typealias Distance = Double
func distanceBetweenPoint(location: Location,
    toLocation: Location) -> Distance {
        let dx = Distance(location.x - toLocation.x)
        let dy = Distance(location.y - toLocation.y)
        return sqrt(dx * dx + dy * dy)
}
let origin: Location = Location(x: 0, y: 0)
let point: Location = Location(x: 1, y: 1)
let distance: Distance =  distanceBetweenPoint(origin, toLocation: point)

需要注意的是,对于泛型使用typealias时,如果泛型类型的确定性没有保证时,是不可以使用的,如下:

1
2
3
4
5
//错误的例子,不可使用typealias
class Person<T> {}
typealias Worker = Person
typealias Worker = Person<T>
typealias Worker<T> = Person<T>
1
2
3
class Person<T> {}
typealias WorkId = String
typealias Worker = Person<WorkId>

最后一点,Swift是没有泛型接口的,但使用typealias可以模拟一个泛型接口,在protocol中声明一个必须实现的typealias,要求遵从该protocol的class或struct实现该typealias,比如在GeneratorType和SequenceType中就使用了这一技巧。

1
2
3
4
5
6
7
8
9
protocol GeneratorType {
    typealias Element
    mutating func next() -> Self.Element?
}

protocol SequenceType {
    typealias Generator : GeneratorType
    func generate() -> Self.Generator
}

16.可变参数函数

可变参数函数就是可以接受任意多个参数的函数,例如NSString的-stringWithFormat:的方法。但是在OC中,自己实现一个可变参数函数是非常困难的。而在Swift中,这变得非常容易,如下声明一个累加的函数:

1
2
3
4
func sum(input: Int...) -> Int {
    return input.reduce(0, combine: +)
}
print(sum(1, 2, 3, 4, 5))

这个结合了reduce方法,但是需要注意的是可变参数只能作为方法中最后一个参数,而且一个方法中最多只能有一组可变参数,同时可变参数必须是同一个类型的。如果想像-stringWithFormat:这样传入不同类型的参数的话,就需要作出一些变化。

一种解决方法是使用Any来作为参数类型,然后对接收到的首个元素单独处理;另一个解决方法是利用使用_作为参数外部标签,调用时不再需要加上参数名字的特性,先指定一个参数作为字符串,然后跟一个匿名参数列表,这样看起来就像跟了一个参数列表,Swift中的-stringWithFormat:实际上就是这样实现的。

1
2
3
extension NSString {
    public convenience init(format: NSString, _ args: CVarArgType...)
}

17.初始化方法顺序

与OC不同,Swift的初始化方法需要保证类型的所有属性都被初始化,在子类的初始化方法中,我们需要保证子类实例的成员初始化完成后,再调用父类的初始化方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Cat {
    var name: String
    init() {
        name = "cat"
    }
}

class Tiger: Cat {
    let power: Int
    override init() {
        power = 10
        super.init()
        name = "tiger"
    }
}

如果子类不需要对父类的成员变量作出修改,那么不存在第三步的情况下,super.init()也不需要调用,Swift在初始化最后会自动调用一次super.init()。

1
2
3
4
5
6
class Lion: Cat {
    let power: Int
    override init() {
        power = 10
    }
}

18.Designated,Convenience和Required

Swift强调安全性,在初始化方法中也不例外。OC中的init方法是不安全的,init不能保证只被调一次,也没有保证在初始化方法中实现各个成员变量的初始化。

而在Swift中强化了designated初始化方法的地位,Swift中不加修饰的init方法中药保证所有的非Optional的成员变量被初始化,而且在子类的designated初始化中也要强制调用父类的designated初始化。所以在Swift中无论何种路径,被初始化对象是一定可以完整初始化的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ClassA {
    let numA: Int
    init(num: Int) {
        numA = num
    }
}

class ClassB: ClassA {
    let numB: Int

    override init(num: Int) {
        numB = num + 1
        super.init(num: num)
    }
}

与designated初始化方法对应的是convenience初始化方法,作为补充初始化方法,所有的convenience初始化必须调用同一类中的designated初始化来完成。另外,convenience初始化时不能被子类重写或者从子类中以super的方式调用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ClassA {
    let numA: Int
    init(num: Int) {
        numA = num
    }

    convenience init(bigNum: Bool) {
        self.init(num: bigNum ? 10000 : 1)
    }
}

class ClassB: ClassA {
    let numB: Int

    override init(num: Int) {
        numB = num + 1
        super.init(num: num)
    }
}

只要在子类中重写了父类convenience方法所需的init方法,我们在子类中也可以使用父类的convenience的初始化方法了,比如:

1
let aClassBInstance = ClassB(bigNum: true)

作者提出两个原则:

  1. 初始化路径必须保证对象完全初始化,这可以通过调用本类型的designated初始化方法来得到保证;
  2. 子类的designated初始化方法必须调用父类的designated方法,以保证父类也完成初始化。

某些情况下我们希望子类一定实现designated初始化方法,我们可以添加required关键字来限制,强制子类对这个方法重写。这样的好处是,保证某些依赖designated初始化方法的convenience初始化方法始终可用,如下,将ClassA的designated方法设置为required时,ClassB始终可以成功调用init(bigNum: Bool)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class ClassA {
    let numA: Int
    required init(num: Int) {
        numA = num
    }

    convenience init(bigNum: Bool) {
        self.init(num: bigNum ? 10000 : 1)
    }
}

class ClassB: ClassA {
    let numB: Int

    required init(num: Int) {
        numB = num + 1
        super.init(num: num)
    }
}

最后说明的一点是designated初始化方法也可以添加required,确保子类对其实现,这在子类必须重写该方法是很有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ClassA {
    let numA: Int
    required init(num: Int) {
        numA = num
    }

    required convenience init(bigNum: Bool) {
        self.init(num: bigNum ? 10000 : 1)
    }
}

class ClassB: ClassA {
    let numB: Int

    required init(num: Int) {
        numB = num + 1
        super.init(num: num)
    }

    required convenience init(bigNum: Bool) {
        self.init(num: bigNum ? 20000 : 1)
    }
}

19.初始化返回nil

在OC中init方法是可以返回nil的,用以表示该次初始化失败,但在Swift默认下是不能return的,所以也就没有机会返回一个Optional值,例如NSURL的初始化方法-initWithString:,如果输入为不规格的string,是会返回nil的。

1
2
3
NSURL *url = [[NSURL alloc] initWithString:@"ht tp://swifter tips"];
NSLog(@"%@",url);
//输出(null)

而在Swift中,-initWithString:对应的是一个convenience方法,convenience init?(string URLString: String)。为了解决init不能返回nil的问题,Swift1.1之后Apple添加了可以返回nil的能力,需要在init声明后加上一个?或者!,

1
2
3
4
5
public convenience init?(string URLString: String)

let url = NSURL(string: "ht tp://swifter tips")
print(url)
//输出(null)

20.protocol组合

Swift中以Any来表示任意类型,而Any在Swift中其实是protocol<>的一个同名类型,其实这就是protocol组合的应用。

标准的protocol组合的写法是:

1
protocol<ProtocolA, ProtocolB, ProtocolC>

这是一个同时需要实现这三个协议的一个新的匿名协议类型,等同于新申明一个协议类型。

1
2
 protocol ProtocolD: ProtocolA, ProtocolB, ProtocolC {
 }

Any定义时的protocol<>类型是需要实现空协议的协议,也就是任意类型的意思。

protocol组合相比新建一个协议的最大区别在于其匿名性,可以使代码更简洁,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
protocol KittenLike {
    func meow() -> String
}

protocol DogLike {
    func bark() -> String
}

protocol TigerLike {
    func aou() -> String
}

class MysteryAnimal: KittenLike, DogLike, TigerLike {
    func meow() -> String {
        return "meow"
    }

    func bark() -> String {
        return "bark"
    }

    func aou() -> String {
        return "aou"
    }
}

如果需要单独检查宠物类或者猫科类的叫声时,我们可能会选择新建两个新的协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
protocol PetLike: KittenLike, DogLike {

}

protocol CatLike: KittenLike, TigerLike {

}

struct soundChecker {
    static func checkPetTalking(pet: PetLike) {
        //...
    }

    static func checkCatTalking(cat: CatLike) {
        //...
    }
}

我们可以使用typealias来重命名这两个协议,而避免引入新的类型。

1
2
typealias PetLike = protocol<KittenLike, DogLike>
typealias CatLike = protocol<KittenLike, TigerLike>

如果这两个协议只是临时使用一次,那么直接使用protocol组合也是可以的。

1
2
3
4
5
6
7
8
9
struct soundChecker {
    static func checkPetTalking(pet: protocol<KittenLike, DogLike>) {
        //...
    }

    static func checkCatTalking(cat: protocol<KittenLike, TigerLike>) {
        //...
    }
}

最后作者提供了一个解决,同时实现多个协议,且出现方法冲突的解决方法。就是在使用前先进行类型转化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
protocol A {
    func bar() -> Int
}

protocol B {
    func bar() -> String
}

class Class: A, B {
    func bar() -> Int {
        return 1
    }

    func bar() -> String {
        return "Hi"
    }
}

let instance = Class()

let num = (instance as A).bar()
let str = (instance as B).bar()
6vvqnj09Z6