Currying 柯里化
Swift 里可以将方法进行柯里化,把接受多个参数的方法转变成接受第一个参数的方法,并且返回 一个接受其余参数并且返回结果的新方法。
curry 是 Swift 中一个很灵活的特性,curry 就是一个用方法生成方法的机制。
1 | func add(a: Int, b:Int) -> Int { |
向上面的代码那样,给 add 函数传入不同参数,它就会依照这个参数值生成一个新的函数,这个就会对它接受的参数与之前 add 指定的的增量进行加法操作。
1 | func add(a: Int)(b: Int) -> Int { |
上面我们对自定义方法进行 curry 处理,那么如果系统内置的方法,或者别的第三方库中定义的方法,如何对他们应用 curry
例子:
1 | // 自定义方法进行 curry 处理,那么如果系统内置的方法,或者别的第三方库中定义的方法,如果使用对他们应用 curry |
Struct Mutable 的方法
在 Swift 中我们基本都是用 struct 去定义一个纯数据类型。
1 | struct User { |
在变量里添加一些简单的改变变量里内容的方法
1 | // Struct 出来的变量是 Immutable 的,不可改变的。想要用一个方法去改变变量里面的值,就需要加上一个关键字 mutating,User 的 Weight 是 Immutable 的,所以 += 无法再这两个 Int 上使用 |
将 protocol 的方法声明为 mutating
Swift 的 protocol 不仅可以被 class 类型实现,也适用于 struct 和 enum。
在写给别人用的接口时,需要考虑是否使用 mutating 来修饰方法,Swift 的 mutating 关键字修饰方法,是为了能在该方法中修改 struct 或者 enum 的变量。比如下面代码:
1 | protocol Vehicle { |
在使用 class 来实现带有 mutating 的方法的接口时,具体实现的前面是不需要加 mutating 修饰的,因为 class 可以随意更改自己的成员变量。所以在接口里用 mutating 修饰方法,对于 class 的实现是完全透明,可以当作不存在的。
Sequence … 关联
Swift 的 for…in 可以用在所有实现了 SequenceType 的类型上。
首先需要实现 GeneratorType。比如一个实现了反向的 generator 和 sequence 可以这么写:
1 | // 首先定义一个实现了 GeneratorType protocol 的类型 |
多元祖 Tuple
使用多元组,会让程序轻松不少,比如交换输入:
1 | func swapMe<T>(inout a: T, inout b: T){ |
使用多元祖:
1 | func swapMe<T>(inout a: T, inout b: T){ |
1 | // OC 中 CGRect 有一个辅助方法叫做 CGRectDivide 用来将一个 CGRect 在一定位置切分成两个区域。 |
@autoclosure 和 ??
@autoclosure 做的事情就是把一句表达式自动地封装成一个闭包。
例如:
1 | func logIfTrue(predicate: () -> Bool) { |
Swift 会把 2 > 1这个表达式自动转换为 () -> Bool。
在 Swift 中,有一个非常有用的操作符,可以快速的对 nil 进行判断,??
这个操作符可以判断输入并在当左侧的值是非 nil 的 Optional 值时返回其 value,当左侧是 nil 时返回右侧的值,比如:
1 | var level: Int? |
因此最后 一个 startLevel 被赋值给了 currentLevel ,??的定义:
1 | func ??<T>(optional: T?, @autoclosure defaultValue:() -> T?) -> T? |
这里我们输入满足的是后者,虽然表面上看,startLevel 只是一个 Int,其实它被自动封装成了一个() -> Int,实现:
1 |
|
如果我们直接使用 T,那么久以为这 在 ?? 操作符真正取值之前,我们就必须准备好一个默认值。这个默认值的准备和计算是会消耗性能的。但是其实要是 optional 不是 nil 的话,我们是完全不需要这个默认值的,而会直接返回 optional 解包后的值。这样一来,默认值就拜拜准备了。这样的开销是完全可以避免的,方法就是讲默认值的计算推迟到了 optional 判定为 nil 之后。
@autoclosure 并不支持带有输入参数的写法,只有形如() -> T 的参数才能使用这个特性进行简化,另外因为调用者往往很容易忽视 @autoclosure 这个特性,需要小心使用,还是写完整的闭包写法比较好。
练习:其实 && 和 || 这两个操作符里也用到了 @autoclosure,试试看如何实现这两个操作符
1 | // false && true -> false true |
Optional Chaining(可选链)
使用 Optional Chaining 可以让我们摆脱很多不必要的判断和取值,但是需要注意一些陷阱
因为 Optional Chaining 是随时都可能体检返回 nil 的,所以使用 Optional Chaining 所得到的东西其实都是 Optional 的,比如有下面的一段代码:
1 | class Toy { |
如果我们想通过小明,知道小明的宠物的玩具的名字,则:
1 | let xiaoming = Child() |
在这个 Optional Chaining 中,我们在任何一个 ?.时,都可能遇到 nil 而提前返回。
实际使用中,大多情况下我们更希望使用可选绑定来直接取值:
1 | if let toyname = xiaoming.pet?.toy?.name{ |
接下来,稍微和其他特性结合,事情就会变得复杂起来:
如果为 Toy 定义一个扩展,增加一个玩玩具的 play()方法
1 | extension Toy { |
这时,处于对代码的扩展性,也许会有其他人有一个宠物,宠物会有玩具,玩具有 play()方法,则做一个闭包方便调用
1 | let playClosure = {(child: Child) -> () in child.pet?.toy?.play()} |
这样的代码没有意义,在对于 play()的调用上,定义时我们没有写 play()的返回,表示该方法返回 Void,经过 Optional Chaining 我们得到的是一个 Optional 的结果,就是说,最后应该是这样一个 closure:
1 | let playClosure = {(child: Child) -> ()? in child.pet?.toy?.play()} |
这样调用的返回将是一个() ? 这时候就可以通过可选绑定来判断方法是否被调用成功了:
1 | if let result: () = playClosure(xiaoming) { |
操作符
与 Objective-C 不同,Swift 支持重载操作符这样的特性,比如:
1 | struct Vector2D { |
上面定义的加号,减号,负号等都是存在于 Swift 中的运算符了,我们所做的只是变换它参数进行重载。
如果我们要定义一个全新的运算符,需要做的事情会多一件,根据定义选取+*运算符:
1 | func +*(left: Vector2D, right: Vector2D) -> Double { |
但是编译器会报出错误:Operator implementation without matching operator declaration
这时因为,我们没有对这个操作符进行申明。之前可以直接重载 + - * % 是因为 Swift 中早已经有定义了。
这时,我们需要告诉编译器这个符号是一个操作符:
1 | infix operator +* { |
infix
表示要定义一个中位操作符,即前后都是输入,其他的修饰符还包括,prefix 和 postfix
associativity
定义了结合律,即如果多个同类的操作符顺序出现的计算顺序,常见的+ - 都是 left,多个加法同时出现,按照从左往右顺序计算。点乘的结果是一个 Double 不会再和其他点乘结合使用,所以这里写 none
precedence(优先权)
运算的优先级,越高则优先进行计算。 乘除150 加减140 这里我们定义的是160
1 | let result = v1 +* v2 |
注意点:Swift 的操作符是不能定义在局部域中的,因为至少会希望在全局范围使用你的操作符,否则操作符就失去了意义。不同 module 的操作符是有可能冲突的,这对于库开发者来说是需要特别注意的地方。 如果库中的操作符冲突的话,使用者是无法像解决类型名冲突那样通过指定库名字来进行调用的。因此在重载或者自定义操作符时,应当尽量将其作为其他某个方法的“简便写法”,我们不应该滥用这个特性。
func 的参数修饰
1 | func incrementor(variable: Int) -> Int { |
最后,要注意参数的修饰是具有传递限制的,对于跨越层级的调用,需要保证同一参数的修饰是统一的。例子:
1 | func makeIncrementor(addNumber: Int) -> ((inout Int) -> ()) { |
外层的 makeIncrementor 的返回里也需要在参数类型前面明确指出修饰词,以符合内部的定义,否则将无法编译通过
字面转换量
Swift 提供了一组非常有意思的接口,用来将字面量转换为特定类型
开发中,经常可能用到的有:
ArrayLiteralConvertible
BooleanLiteralConvertible
DictionaryLiteralConvertible
FloatLiteralConvertible
NilLiteralConvertible
IntegerLiteralConvertible
StringLiteralConvertible
所有的字面量转换接口都定义了一个 typealias 和对应的 init 方法。拿 BooleanLiteralConvertible 举个例子
1 | protocol BooleanLiteralConvertible { |
于是在我们需要自己实现一个字面量转换的时候,可以简单地只实现定义的 init 方法就行了。举个不太有实际意义的例子,我们想实现自己的 Bool 类型,可以这么做
1 | enum MyBool: Int { |
BooleanLiteralType 大概是最简单的形式,如果我们深入一点,就会发现像是 StringLiteralConvertible 这样的接口会复杂一些,这个接口不仅类似上面布尔的情况,定义了 StringLiteralType 及接受其的初始化方法,这个接口还要求实现:
1 | // 如果通过 String 赋值生成 Person 对象的话,可以改写这个类: |
在所有的接口定义的 init 前面都加上了 required 关键字,这个类的子类都需要保证能够做类似的字面量转换,确保类型安全,但是会有很多重复代码,需要做一个修改加入,convenience
1 | class Person2: StringLiteralConvertible { |
在上面的 Person 的例子中,我们没有像 MyBool 中做的那样,使用一个 extension 的方式来扩展类,使其可以使用字面量赋值,这是因为,在 extension 中,我们是不能定义 required 的初始化方法。 我们无法为现有的非 final 的 class 添加字面量转换