阅读 796

【iOS】Swift Tips - (一)

文章来自对 objccn.io/ 相关书籍的整理笔记。“感谢 objc.io 及其撰稿作者们无私地将他们的知识分享给全世界”。

Swift 新元素

1. 柯里化

Swift 可以将方法进行柯里化 (Currying),把接受多个参数的方法进行一些变形使其更加灵活。

// 将输入的数字加 1:
 func addOne(num: Int) -> Int {
    return num + 1
}

// 这个函数所表达的内容非常有限
// 可以定义一个通用的函数,它将接受需要与输入数字相加的数,并返回一个函数
// 返回的函数 将接受输入数字本身,然后进行操作

 func addTo(_ addNum: Int) -> (Int) -> Int {
    return {
        num in
        return num + addNum
    }
}
var add10 = addTo(10)
print(add10(5)) // 15
复制代码
// 一个大小比较的例子
func greatThan(_ target: Int) -> (Int) -> Bool {
    return {
        num in
        return num > target
    }
}

var greatThan10 = greatThan(10)
print(greatThan10(5)) // false
print(greatThan10(15)) // true
复制代码

柯里化是一种量产相似方法的办法,可以通过柯里化一个方法模板来避免写出很多重复代码,同时方便了维护。

Swift 中 Selector 只能使用字符串在生成。这会导致难以重构,并且无法在编译期间进行检查。但 target-action 又是 Cocoa 中重要的设计模式,一种可能的解决方式就是利用方法的柯里化。

 protocol TargetAction {
    func performAction()
}

struct TargetActionWrapper<T: AnyObject>: TargetAction {
    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: @escaping (T) -> () -> (),
                           controlEvent: ControlEvent) {
        actions[controlEvent] = TargetActionWrapper(target: target, action: action)
    }
    func removeTargetForControlEvent(controlEvent: ControlEvent) {
        actions[controlEvent] = nil
    }
    func performActionForControlEvent(controlEvent: ControlEvent) {
        actions[controlEvent]?.performAction()
    } 
}
复制代码

2. 将 protocol 的方法声明为 mutating

Swift 的 protocol 可以被 class、struct 和 enum 实现。在写协议时需要考虑是否使用 mutating 来修饰方法,比如定义为 mutating func myMethod() 。Swift 的 mutating 关键字修饰方法是为了能在该方法中修改 struct 或是 enum 的变量,如果没在协议方法里写 mutating 的话,用 struct 或者 enum 来实现这个协议就不能在方法里改变自己的变量了。

 protocol Vehicle
{
    var numberOfWheels: Int {get}
    var color: UIColor {get set}
    mutating func changeColor()
}
struct MyCar: Vehicle {
    let numberOfWheels = 4
    var color = UIColor.blue
    mutating func changeColor() {
        color = .red
    } 
}
复制代码

如果把 protocol 定义中的 mutating 去掉的话, MyCar 就编译不过了。

在使用 class 来实现带有 mutating 的方法的协议时,具体实现的前面是不需要加 mutating 修饰的,因为 class 可以随意更改自己的成员变量。在协议里用 mutating 修饰方法,对于 class 的实现是完全透明,可以当作不存在。

3. Sequence

Swift 的 for...in 可以用在所有实现了 Sequence 的类型上,而为了实现 Sequence 首先需要实 现一个 IteratorProtocol。一个实现了反向的 iterator 和 sequence 可以这么写:

// 先定义一个实现了 IteratorProtocol 协议的类型
// IteratorProtocol 需要指定一个 typealias Element 
// 以及提供一个返回 Element? 的方法 next()
class ReverseIterator<T>: IteratorProtocol {
    typealias Element = T
    
    var array: [Element]
    var currentIndex = 0
    
    init(array: [Element]) {
        self.array = array
        currentIndex = array.count - 1
    }
    
    func next() -> Element? {
        if currentIndex < 0{
            return nil 
        } else {
            let element = array[currentIndex]
            currentIndex -= 1
            return element
        } 
    }
}

// 然后我们来定义 Sequence
// 和 IteratorProtocol 很类似,不过换成指定一个 typealias Iterator 
// 以及提供一个返回 Iterator? 的方法 makeIterator()
struct ReverseSequence<T>: Sequence {
    var array: [T]
    
    init (array: [T]) {
        self.array = array
    }
    
    typealias Iterator = ReverseIterator<T>
    
    func makeIterator() -> ReverseIterator<T> {
        return ReverseIterator(array: self.array)
    }
}

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

// 对 Sequence 可以使用 for...in 来循环访问
for i in ReverseSequence(array: arr) {
    print("Index \(i) is \(arr[i])")
}
// Index 4 is 4
// Index 3 is 3
// Index 2 is 2
// Index 1 is 1
// Index 0 is 0
复制代码

深究 for...in 方法做了什么,将其展开:

var iterator = arr.makeIterator()
while let obj = iterator.next() {
    print(obj) 
}
复制代码

同时可以得到的收益是可以使用像 map,filter 和 reduce 这些方法,因为 Sequence 协议扩展 (protocol extension) 已经实现了:

 extension Sequence {
    func map<T>(_ transform: @noescape (Self.Iterator.Element) throws -> T) rethrows -> [T]
    func filter(_ isIncluded: @noescape (Self.Iterator.Element) throws -> Bool) rethrows -> [Self.Iterator.Element]
    func reduce<Result>(_ initialResult: Result, _ nextPartialResult: @noescape (partialResult: Result, Self.Iterator.Element) throws -> Result) rethrows -> Result
    // ...
}
复制代码

4. 多元组

// 交换输入,普通方法可能都是这么写的:
 func swapMe1<T>( a: inout T, b: inout T) {
    let temp = a
    a=b
    b = temp 
}

// 使用多元组:
 func swapMe2<T>( a: inout T, b: inout T) {
    (a,b) = (b,a)
}
复制代码

在 Objective-C 版本的 Cocoa API 中有不少需要传递指针来获取值的地方,这一般是由于在 Objective-C 中返回值只能有一个所造成的妥协。比如 CGRect 有一个辅助方法叫做 CGRectDivide ,它用来将一个 CGRect 在一定位置切分成两个区域。具体定义和用法如下:

/*
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);
复制代码

上面的代码将 {0,0,100,100} 的 rect 分割为两部分,分别是 {0,0,20,100} 的 small 和 {20,0,80,100} 的 large 。由于 C 系语言的单一返回,通过传入指针的方式让方法来填充需要的部分。

而现在在 Swift 中,使用了多元组的方式来同时返回被分割的部分和剩余部分:

 extension CGRect {
    //...
    func divided(atDistance: CGFloat, from fromEdge: CGRectEdge)
                    -> (slice: CGRect, remainder: CGRect)
    //...
}

let rect = CGRect(x: 0, y: 0, width: 100, height: 100)
let (small, large) = rect.divided(atDistance: 20, from: .minXEdge)
复制代码

5. @autoclosure 和 ??

@autoclosure 是 Apple 的一个非常神奇的创造,更多地是像在 “hack” 这⻔语言。 @autoclosure 做的事情就是把一句表达式自动地封装成一个闭包(closure)。有时候语法上看起来会非常漂亮。

一个方法接受一个闭包,当闭包执行的结果为 true 的时候进行打印:

func logIfTrue(_ block: () -> Bool) {
    if block() {
        print(true)
    }
}
logIfTrue({ return 1 < 2 })  // true
logIfTrue({ 1 < 2 }) // true
logIfTrue{ 1 < 2 } // true
复制代码

可以改换方法参数,在参数名前面加上 @autoclosure 关键字:

func logIfTrue(_ block: @autoclosure () -> Bool) {
    if block() {
        print(true)
    }
}
logIfTrue(1 < 2)  // true
复制代码

Swift 将会把 1 < 2 这个表达式自动转换为 () -> Bool 。这样就得到了一个写法简单,表意清楚的式子。

在 Swift 中,?? 是非常有用的操作符可以用来快速地对 nil 进行条件判断。这个操作符可以判断输入并在当左侧的值是非 nil 的 Optional 值时返回其 value,当左侧是 nil 时返回右侧的值,比如:

var level: Int?
var startLevel = 1
var currentLevel = level ?? startLevel
复制代码

?? 有两种版本:

func ??<T>(optional: T?, defaultValue: @autoclosure () -> T?) -> T?
func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T
复制代码

在这里的输入满足的是后者,虽然表面上看 startLevel 只是一个 Int ,但是其实在使用时它被自动封装成了一个 () -> Int ,猜测一下 ?? 的实现:

func ??<T>(optional: T?, defaultValue: @autoclosure () -> T) -> T {
    switch optional {
        case .Some(let value):
            return value
        case .None:
            return defaultValue()
    } 
}
复制代码

为什么要使用 autoclosure ?如果直接使用 T ,那么就意味着在 ?? 操作符真正取值之前,必须准备好一个默认值传入到这个方法中,如果这个默认值是通过一系列复杂计算得到的话,可能会成为浪费 -- 因为其实如果 optional 不是 nil 的话,实际上是完全没有用到这个默认值,而会直接返回 optional 解包后的值的。这样的开销是完全可以避免的,方法就是将默认值的计算推迟到 optional 判定为 nil 之后。可以巧妙地绕过条件判断和强制转换,优雅的写法处理对 Optional 及默认值的取值。

@autoclosure 并不支持带有输入参数的写法,也就是说只有形如 () -> T 的参数才能使用这个特性进行简化。因为调用者这个特性,所以在写接受 @autoclosure 的方法时要小心,如果容易产生歧义或者误解还是使用完整的闭包写法。

6. @escaping

Swift 的语法非常适合函数式编程的使用,而闭包 (closure) 正是函数式编程的核心概念之一。 Swift 中可以定义一个接受函数作为参数的函数,而在调用时,使用闭包的方式来传递这个参数是常⻅手段:

 func doWork(block: ()->()) {
    block()
}
doWork {
    print("work")
}
复制代码

这种闭包默认隐藏了一个假设,参数中 block 的内容会在 doWork 返回前就完成。对于 block 的调用是同步行为。将 block 放 到一个 Dispatch 中去,让它在 doWork 返回后被调用的话,就需要在 block 的类型前加上 @escaping 标记来表明这个闭包是会“逃逸”出该方法的:

 func doWorkAsync(block: @escaping ()->()) {
    DispatchQueue.main.async {
    block() }
}
复制代码

在使用闭包调用这个两个方法时,也会有一些行为的不同。闭包是可以捕获其中的变量的。对于 doWork 参数里这样的没有逃逸行为的闭包,因为闭包的作用域不会超过函数本身,所以不需要担心在闭包内持有 self 等。而接受 @escaping 的 doWorkAsync 则有所不同。由于需要确保闭包内的成员依然有效,如果在闭包内引用了 self 及其成员的话,Swift 将强制明确地写出 self 。对比下面的两个用例的不同之处:

class S {
    var foo = "foo"
    func method1() {
        doWork {
            print(foo) 
        }
        foo = "bar" 
    }
    func method2() {
        doWorkAsync {
            print(self.foo)
        }
        foo = "bar" 
    }
}
S().method1() // foo
S().method2() // bar
复制代码

method1 中调用者不需要考虑 self.foo 的持有情况,使用起来相当直接。对 foo 的打印输出的是原始值 foo 。而 method2 中由于闭包可逃逸,Swift 强制写明 self ,以起到提醒作用,需要考虑 self 的持有情况。这个例子中,让闭包持有了 self ,打印的值是 method2 最后对 foo 赋值后的内容 bar 。如果不希望在闭包中持有 self ,可以使用 [weak self] 的方式来表达:

 func method3() {
    doWorkAsync {
        [weak self] in
        print(self?.foo ?? "nil")
    }
foo = "bar" }
S().method3() // nil
复制代码

如果你在协议或者父类中定义了一个接受 @escaping 为参数方法,那么在实现协议和类型或者是这个父类的子类中,对应的方法也必须被声明为 @escaping ,否则两个方法会被认为拥有不同的函数签名:

 protocol P {
    func work(b: @escaping ()->())
}
// 可以编译通过
class C: P {
    func work(b: @escaping () -> ()) {
        DispatchQueue.main.async {
            print("in C")
        b() 
    }
// 无法编译通过
class C1: P {
    func work(b: () -> ()) {
        // ...
    } 
}
复制代码

7. Optional Chaining

使用 Optional Chaining 可以摆脱很多不必要的判断和取值,但是在使用的时候需要小心陷阱。Optional Chaining 是随时都可能提前返回 nil 的,所以使用 Optional Chaining 所得到的东 ⻄其实都是 Optional 的。

 class Toy {
    let name: String
    init(name: String) {
        self.name = name
    }
}
class Pet {
    var toy: Toy?
}
class Child {
    var pet: Pet?
}

// ...
let toyName = xiaoming.pet?.toy?.name
复制代码

最后访问的是 name ,并且在 Toy 的定义中 name 是被定义为一个确定的 String 而非 String? 的,但是拿到的 toyName 其实还是一个 String? 的类型。这是由于在 Optional Chaining 中在任意一个 ?. 的时候都可能遇到 nil 而提前返回,这个时候当然就只能拿到 nil 了。

在实际的使用中,大多数情况下可能更希望使用 Optional Binding 来直接取值的这样的代码:

if let toyName = xiaoming.pet?.toy?.name {
    // 太好了,小明既有宠物,而且宠物还正好有个玩具
}
复制代码
extension Toy {
     func play() {
         //...
     } 
 }
// 为 Toy 定义了一个扩展,以及一个玩玩具的 play() 方法
// 要是有玩具的话就玩玩具
xiaoming.pet?.toy?.play()
复制代码

除了小明也许还有小红小李小张等等..在这种时候要把这一串调用抽象出来,做一个闭包方便使用。传入一个 Child 对象,如果有宠物并且宠物有玩具的话就去玩:

 // 这是错误代码
 let playClosure = {(child: Child) -> () in child.pet?.toy?.play()}
复制代码

这样的代码是没有意义的!问题在于对于 play() 的调用上。定义的时候没有写 play() 的返回,这表示这个方法返回 Void (或者写作一对小括号 () ,它们是等价的)。经过 Optional Chaining 以后我们得到的是一个 Optional 的结果。也就是说最后得到的应该是这样一个 closure

let playClosure = {(child: Child) -> ()? in child.pet?.toy?.play()}
复制代码

使用的时候可以通过 Optional Binding 来判定方法是否调用成功:

if let result: () = playClosure(xiaoming) { 
    print("好开心")
} else { 
    print("没有玩具可以玩")
}
复制代码

8. 操作符

Swift 支持重载操作符这样的特性,最常⻅的使用方式可能就是定义一些简便的计算。比如需要一个表示二维向量的数据结构:

 struct Vector2D {
    var x = 0.0
    var y = 0.0
}

// 两个 Vector2D 相加:
let v1 = Vector2D(x: 2.0, y: 3.0)
let v2 = Vector2D(x: 1.0, y: 4.0)
let v3 = Vector2D(x: v1.x + v2.x, y: v1.y + v2.y)

// 重载加号操作符:
 func +(left: Vector2D, right: Vector2D) -> Vector2D {
    return Vector2D(x: left.x + right.x, y: left.y + right.y)
}
let v4 = v1 + v2
//v4 为 {x3.0,y7.0}
复制代码

上面定义的加号,减号和负号都是已经存在于 Swift 中的运算符了,所做的只是变换参数进行重载。如果想要定义一个全新的运算符的话,要做的事情会多一件。

比如点积运算就是一个在矢量运算中很常用的运算符,它表示两个向量对应坐标的乘积的和。根据定义,以及参考重载运算符的方法,选取 +* 来表示这个运算:

 func +* (left: Vector2D, right: Vector2D) -> Double {
    return left.x * right.x + left.y * right.y
}
复制代码

但是编译器会给我们一个错误:Operator implementation without matching operator declaration

这是因为没有对这个操作符进行声明。可以直接重载像 + , - , * 这样的操作符,是因为 Swift 中已经有定义了,如果要新加操作符的话,需要先对其进行声明,告诉编译器这个符号其实是一个操作符。添加如下代码:

 precedencegroup DotProductPrecedence {
    associativity: none
    higherThan: MultiplicationPrecedence
}
infix operator +*: DotProductPrecedence
复制代码
  • precedencegroup

定义了一个操作符优先级别。操作符优先级的定义和类型声明有些相似,一个操作符比需要属于某个特定的优先级。Swift 标准库中已经定义了一些常用的运算优先级组,比如加法优先级 ( AdditionPrecedence ) 和乘法优先级 ( MultiplicationPrecedence ) 等。如果没有适合的运算符的优先级组,就需要自己指定结合律方式和优先级顺序了。

  • associativity

定义了结合律,即如果多个同类的操作符顺序出现的计算顺序。比如常⻅的加法和减法都是 left ,就是说多个加法同时出现时按照从左往右的顺序计算 (因为加法满足交换律,所以这个顺序无所谓,但是减法的话计算顺序就很重要了)。点乘的结果是一个 Double ,不再会和其他点乘结合使用,所以这里是 none。

  • higherThan

运算的优先级,点积运算是优先于乘法运算的。除了 higherThan ,也支持使用 lowerThan 来指定优先级低于某个其他组。

  • infix

表示要定义的是一个中位操作符,即前后都是输入;其他的修饰子还包括 prefix 和 postfix。

有了这些之后,就可以很简单地进行向量的点积运算了:

 let result = v1 +* v2 // 输出为 14.0
复制代码

Swift 的操作符是不能定义在局部域中的,因为至少会希望在能在全局范围使用你的操作符,否则操作符也就失去意义了。来自不同 module 的操作符是有可能冲突的,这对于库开发者来说是需要特别注意的地方。如果库中的操作符冲突的话,使用者是无法像解决类型名冲突那样通过指定库名字来进行调用的。因此在重载或者自定义操作符时,应当尽量将其作为其他某个方法的 "简便写法",而避免在其中实现大量逻辑或者提供独一无二的功能。这样即使出现了冲突,使用者也还可以通过方法名调用的方式使用库。运算符的命名也应当尽量明了,避免歧义和可能的误解。因为一个不被公认的操作符是存在冲突⻛险和理解难度的,所以不应该滥用这个特性。

9. func 的参数修饰

在声明一个 Swift 的方法的时候,一般不去指定参数前面的修饰符,而是直接声明参数:

 func incrementor(variable: Int) -> Int {
    return variable + 1
}
复制代码

如果想对增加后的变量做点什么,又不想引入一个新的变量的话:

// 错误代码
func incrementor(variable: Int) -> Int {
    variable += 1
    print(variable)
    return variable
}
复制代码

Swift 其实是一⻔讨厌变化的语言。所有有可能的地方,都被默认认为是不可变的,也就是用 let 进行声明的。这样不仅可以确保安全,也能在编译器的性能优化上更有作为。在方法的参数上也是如此,不写修饰符的话,默认情况下所有参数都是 let 的。

 func incrementor2(variable: Int) -> Int {
    var num = variable
    num += 1
    return num
}
复制代码

有些时候希望在方法内部直接修改输入的值,可以使用 inout 来对参数进行修饰:

func incrementor(variable: inout Int) {
    variable += 1
}

var luckyNumber = 7
incrementor(variable: &luckyNumber)
print(luckyNumber)
// luckyNumber = 8
复制代码

Int 其实是一个值类型,并不能直接修改它的地址来让它指向新的值。对于值类型来说,inout 相当于在函数内部创建了一个新的值,然后在函数返回时将这个值赋给 & 修饰的变量,这与引用类型的行为是不同的。

要注意的是参数的修饰是具有传递限制的,就是说对于跨越层级的调用,需要保证同一参数的修饰是统一的。比如想扩展一下上面的方法,实现一个可以累加任意数字的+N器的话,可以写成这样:

 func makeIncrementor(addNumber: Int) -> ((inout Int) -> ()) {
    func incrementor(variable: inout Int) -> () {
        variable += addNumber;
    }
    return incrementor;
}
复制代码

外层的 makeIncrementor 的返回里也需要在参数的类型前面明确指出修饰词,以符合内部的定义,否则将无法编译通过。

10. 字面量表达

字面量就是指像特定的数字,字符串或者是布尔值这样,能够直截了当地指出自己的类型并为变量进行赋值的值。

// // 3 、 Hello 、 true 称为字面量
let aNumber = 3
let aString = "Hello"
let aBool = true

// Array 和 Dictionary 在使用简单的描述赋值的时候,使用的也是字面量
let anArray = [1,2,3]
let aDictionary = ["key1": "value1", "key2": "value2"]
复制代码

Swift 提供了一组协议,使用字面量来表达特定的类型。对于那些实现了字面量表达协议的类型,在提供字面量赋值的时候,就可以简单地按照协议方法中定义的规则“无缝对应”地通过赋值的方式将值表达为对应类型。这些协议包括了各个原生的字面量,在实际开发中经常可能用到的有:

      ExpressibleByArrayLiteral
      ExpressibleByBooleanLiteral
      ExpressibleByDictionaryLiteral
      ExpressibleByFloatLiteral
      ExpressibleByNilLiteral
      ExpressibleByIntegerLiteral
      ExpressibleByStringLiteral
复制代码

所有的字面量表达协议都定义了一个 typealias 和对应的 init 方法。拿 ExpressibleByBooleanLiteral 举个例子:

protocol ExpressibleByBooleanLiteral {
    typealias BooleanLiteralType
    init(booleanLiteral value: BooleanLiteralType)
}
   
// 在这个协议中, BooleanLiteralType 在 Swift 标准库中已经有定义了:
typealias BooleanLiteralType = Bool
复制代码

需要自己实现一个字面量表达的时候,可以简单地只实现定义的 init 方法。比如实现一个自己的 Bool 类型,可以这么做:

enum MyBool: Int {
    case myTrue, myFalse
}
extension MyBool: ExpressibleByBooleanLiteral {
    init(booleanLiteral value: Bool) {
        self = value ? .myTrue : .myFalse
    }
}

let myTrue: MyBool = true
let myFalse: MyBool = false
myTrue.rawValue    // 0
myFalse.rawValue   // 1
复制代码

BooleanLiteralType 是最简单的形式,像是 ExpressibleByStringLiteral 这样的协议要复杂一些。这个协议不仅类似于上面布尔的情况,定义了 StringLiteralType 及接受其的初始化方法,这个协议本身还要求实现下面两个协议:

ExpressibleByExtendedGraphemeClusterLiteral
ExpressibleByUnicodeScalarLiteral
复制代码

这两个协议对应字符簇和字符的字面量表达。形式上还是一致的,只不过在实现 ExpressibleByStringLiteral 时需要将这三个init 方法都进行实现。

class Person {
    let name: String
    init(name value: String) {
        self.name = value
    }
}

class Person: ExpressibleByStringLiteral {
    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 赋值的代码。一个改善的方式是在这些初始化方法中去调用原来的 init(name value: String) ,需要在这些初始化方法前加上 convenience :

class Person: ExpressibleByStringLiteral {
    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)
// 输出:
// xiaoMing
复制代码

上面的 Person 的例子中,没有像 MyBool 中做的那样,使用一个 extension 的方式来扩展类使其可以用字面量赋值,这是因为在 extension 中不能定义 required 的初始化方法的。 无法为现有的非 final 的 class 添加字面量表达。

文章分类
iOS
文章标签