Swift Tips

转载:转自猫神100个Swift必备Tips,onevcat

Selector

@selector 是 Objective-C 时代的一个关键字,它可以将一个方法转换并赋值给一个 SEL 类型,它的表现很类似一个动态的函数指针。在 Objective-C 时 selector 非常常用,从设定 target-action,到自举询问是否响应某个方法,再到指定接受通知时需要调用的方法等等,都是由 selector 来负责的。在 Objective-C 里生成一个 selector 的方法一般是这个样子的:

-(void) callMe {
    //...
}

-(void) callMeWithParam:(id)obj {
    //...
}

SEL someMethod = @selector(callMe);
SEL anotherMethod = @selector(callMeWithParam:);

// 或者也可以使用 NSSelectorFromString
// SEL someMethod = NSSelectorFromString(@"callMe");
// SEL anotherMethod = NSSelectorFromString(@"callMeWithParam:");

一般为了方便,很多人会选择使用 @selector,但是如果要追求灵活的话,可能会更愿意使用 NSSelectorFromString 的版本 – 因为我们可以在运行时动态生成字符串,从而通过方法的名字来调用到对应的方法。

在 Swift 中没有 @selector 了,我们要生成一个 selector 的话现在只能使用字符串。Swift 里对应原来 SEL 的类型是一个叫做Selector 的结构体,它提供了一个接受字符串的初始化方法。像上面的两个例子在 Swift 中等效的写法是:

func callMe() {
    //...
}

func callMeWithParam(obj: AnyObject!) {
    //...
}

let someMethod = Selector("callMe")
let anotherMethod = Selector("callMeWithParam:")

和 Objective-C 时一样,记得在 callMeWithParam 后面加上冒号 (:),这才是完整的方法名字。多个参数的方法名也和原来类似,是这个样子:

func turnByAngle(theAngle: Int, speed: Float) {
    //...
}

let method = Selector("turnByAngle:speed:")

另外,因为 Selector 类型实现了 StringLiteralConvertible,因此我们甚至可以不使用它的初始化方法,而直接用一个字符串进行赋值,就可以完成创建了。

最后需要注意的是,selector 其实是 Objective-C runtime 的概念,如果你的 selector 对应的方法只在 Swift 中可见的话 (也就是说它是一个 Swift 中的 private 方法),在调用这个 selector 时你会遇到一个 unrecognized selector 错误:

这是错误代码

private func callMe() {
    //...
}

NSTimer.scheduledTimerWithTimeInterval(1, target: self, 
           selector:"callMe", userInfo: nil, repeats: true)

正确的做法是在 private 前面加上 @objc 关键字,这样运行时就能找到对应的方法了。

@objc private func callMe() {
    //...
}

NSTimer.scheduledTimerWithTimeInterval(1, target: self, 
             selector:"callMe", userInfo: nil, repeats: true)

另外,如果方法的第一个参数有外部变量的话,在通过字符串生成 Selector 时还有一个约定,那就是在方法名和第一个外部参数之间加上 with

func aMethod(external paramName: AnyObject!) { ... }

想获取对应的获取 Selector,应该这么写:

let s = Selector("aMethodWithExternal:")

Sequence

Swift 的 for...in 可以用在所有实现了 SequenceType 的类型上,而为了实现 SequenceType 你首先需要实现一个 GeneratorType。比如一个实现了反向的 generator 和 sequence 可以这么写:

// 先定义一个实现了 GeneratorType protocol 的类型
// GeneratorType 需要指定一个 typealias Element 
// 以及提供一个返回 Element? 的方法 next()
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--
    }
}

// 然后我们来定义 SequenceType
// 和 GeneratorType 很类似,不过换成指定一个 typealias Generator
// 以及提供一个返回 Generator? 的方法 generate()
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]

// 对 SequenceType 可以使用 for...in 来循环访问
for i in ReverseSequence(array: arr) {
    println("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 g = array.generate()
while let obj = g.next() {
    println(obj)
}

顺便你可以免费得到的收益是你可以使用像 map , filter 和 reduce 这些方法,因为它们都有对应 SequenceType 的版本:

func map<S : SequenceType, T>(source: S, 
      transform: (S.Generator.Element) -> T) -> [T]

func filter<S : SequenceType>(source: S, 
      includeElement: (S.Generator.Element) -> Bool) -> [S.Generator.Element]

func reduce<S : SequenceType, U>(sequence: S, 
      initial: U, combine: (U, S.Generator.Element) -> U) -> U

@autoclosure 和 ??

Apple 为了推广和介绍 Swift,破天荒地为这门语言开设了一个博客(当然我觉着是因为 Swift 坑太多需要一个地方来集中解释)。其中有一篇提到了一个叫做 @autoclosure 的关键词。

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

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

func logIfTrue(predicate: () -> Bool) {
    if predicate() {
        println("True")
    }
}

在调用的时候,我们需要写这样的代码

logIfTrue({return 2 > 1})

当然,在 Swift 中对闭包的用法可以进行一些简化,在这种情况下我们可以省略掉 return,写成:

logIfTrue({2 > 1})

还可以更近一步,因为这个闭包是最后一个参数,所以可以使用尾随闭包 (trailing closure) 的方式把大括号拿出来,然后省略括号,变成:

logIfTrue{2 > 1}

但是不管那种方式,要么是书写起来十分麻烦,要么是表达上不太清晰,看起来都让人生气。于是 @autoclosure 登场了。我们可以改换方法参数,在参数名前面加上 @autoclosure 关键字:

func logIfTrue(@autoclosure predicate: () -> Bool) {
    if predicate() {
        println("True")
    }
}

这时候我们就可以直接写:

logIfTrue(2 > 1)

来进行调用了,Swift 将会吧 2 > 1 这个表达式自动转换为 () -> Bool。这样我们就得到了一个写法简单,表意清楚的式子。

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

var level : Int?
var startLevel = 1

var currentLevel = level ?? startLevel

在这个例子中我们没有设置过 level,因此最后 startLevel 被赋值给了 currentLevel。如果我们充满好奇心地点进 ?? 的定义,可以看到?? 有两种版本:

func ??<T>(optional: T?, @autoclosure defaultValue: () -> T?) -> T?
func ??<T>(optional: T?, @autoclosure defaultValue: () -> T) -> T

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

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

可能你会有疑问,为什么这里要使用 autoclosure,直接接受 T 作为参数并返回不行么?这正是 autoclosure 的一个最值得称赞的地方。如果我们直接使用 T,那么就意味着在 ?? 操作符真正取值之前,我们就必须准备好一个默认值,这个默认值的准备和计算是会消耗性能的。但是其实要是 optional 不是 nil 的话,我们是完全不需要这个默认值,而会直接返回 optional 解包后的值。这样一来,默认值就白白准备了,这样的开销是完全可以避免的,方法就是将默认值的计算推迟到 optional 判定为 nil 之后。

就这样,我们可以巧妙地绕过条件判断和强制转换,以很优雅的写法处理对 Optional 及默认值的取值了。最后要提一句的是,@autoclosure 并不支持带有输入参数的写法,也就是说只有形如 () -> T 的参数才能使用这个特性进行简化。另外因为调用者往往很容易忽视 @autoclosure 这个特性,所以在写接受 @autoclosure 的方法时还请特别小心,如果在容易产生歧义或者误解的时候,还是使用完整的闭包写法会比较好。

在 Swift 1.2 中,@autoclosure 的位置发生了变化。现在 @autoclosure 需要像本文中这样,写在参数名的前面作为参数修饰,而不是在类型前面作为类型修饰。但是现在标准库中的方法签名还是写在了接受的类型前面,这应该是标准库中的疏漏。在我们自己实现一个 autoclosure 时,在类型前修饰的写法在 Swift 1.2 中已经无法编译了。

练习

在 Swift 中,其实 && 和 || 这两个操作符里也用到了 @autoclosure。作为练习,不妨打开 Playground,试试看怎么实现这两个操作符吧?

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?
}

在实际使用中,我们想要知道小明的宠物的玩具的名字的时候,可以通过下面的 Optional Chaining 拿到:

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()}

这样调用的返回将是一个 ()? (或者写成 Void? 会更清楚一些),虽然看起来挺奇怪的,但这就是事实。使用的时候我们可以通过 Optional Binding 来判定方法是否调用成功:

if let result: () = playClosure(xiaoming) {
    println("好开心~")
} else {
    println("没有玩具可以玩 :(")
}

func 的参数修饰

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

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

这个方法接受一个 Int 的输入,然后通过将这个输入加 1,返回一个新的比输入大 1 的 Int。嘛,就是一个简单的 +1器

有些同学在大学的 C 程序设计里可能学过像 ++ 这样的“自增”运算符,再加上做了不少关于“判断一个数被各种前置 ++ 和后置 ++折磨后的输出是什么”的考试题,所以之后写代码时也会不自觉地喜欢带上这种风格。于是同样的功能可能会写出类似这样的方法:

这是错误代码

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

残念..编译错误。为什么在 Swift 里这样都不行呢?答案是因为 Swift 其实是一门讨厌变化的语言。所有有可能的地方,都被默认认为是不可变的,也就是用 let 进行声明的。这样不仅可以确保安全,也能在编译器的性能优化上更有作为。在方法的参数上也是如此,我们不写修饰符的话,默认情况下所有参数都是 let 的,上面的代码等效为:

func incrementor(let variable: Int) -> Int {
    return ++variable
}

let 的参数,不能重新赋值这是理所当然的。要让这个方法正确编译,我们需要做的改动是将 let 改为 var

func incrementor(var variable: Int) -> Int {
    return ++variable
}

现在我们的 +1器 又可以正确工作了:

var luckyNumber = 7
let newNumber = incrementor(luckyNumber)
// newNumber = 8

println(luckyNumber)
// luckyNumber 还是 7

正如上面的例子,我们将参数写作 var 后,通过调用返回的值是正确的,而 luckyNumber 还是保持了原来的值。这说明 var 只是在方法内部作用,而不直接影响输入的值。有些时候我们会希望在方法内部直接修改输入的值,这时候我们可以使用 inout 来对参数进行修饰:

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

因为在函数内部就更改了值,所以也不需要返回了。调用也要改变为相应的形式,在前面加上 & 符号:

var luckyNumber = 7
incrementor(&luckyNumber)

println(luckyNumber)
// luckyNumber = 8

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

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

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

Selector

@selector 是 Objective-C 时代的一个关键字,它可以将一个方法转换并赋值给一个 SEL 类型,它的表现很类似一个动态的函数指针。在 Objective-C 时 selector 非常常用,从设定 target-action,到自举询问是否响应某个方法,再到指定接受通知时需要调用的方法等等,都是由 selector 来负责的。在 Objective-C 里生成一个 selector 的方法一般是这个样子的:

-(void) callMe {
    //...
}

-(void) callMeWithParam:(id)obj {
    //...
}

SEL someMethod = @selector(callMe);
SEL anotherMethod = @selector(callMeWithParam:);

// 或者也可以使用 NSSelectorFromString
// SEL someMethod = NSSelectorFromString(@"callMe");
// SEL anotherMethod = NSSelectorFromString(@"callMeWithParam:");

一般为了方便,很多人会选择使用 @selector,但是如果要追求灵活的话,可能会更愿意使用 NSSelectorFromString 的版本 – 因为我们可以在运行时动态生成字符串,从而通过方法的名字来调用到对应的方法。

在 Swift 中没有 @selector 了,我们要生成一个 selector 的话现在只能使用字符串。Swift 里对应原来 SEL 的类型是一个叫做Selector 的结构体,它提供了一个接受字符串的初始化方法。像上面的两个例子在 Swift 中等效的写法是:

func callMe() {
    //...
}

func callMeWithParam(obj: AnyObject!) {
    //...
}

let someMethod = Selector("callMe")
let anotherMethod = Selector("callMeWithParam:")

和 Objective-C 时一样,记得在 callMeWithParam 后面加上冒号 (:),这才是完整的方法名字。多个参数的方法名也和原来类似,是这个样子:

func turnByAngle(theAngle: Int, speed: Float) {
    //...
}

let method = Selector("turnByAngle:speed:")

另外,因为 Selector 类型实现了 StringLiteralConvertible,因此我们甚至可以不使用它的初始化方法,而直接用一个字符串进行赋值,就可以完成创建了。

最后需要注意的是,selector 其实是 Objective-C runtime 的概念,如果你的 selector 对应的方法只在 Swift 中可见的话 (也就是说它是一个 Swift 中的 private 方法),在调用这个 selector 时你会遇到一个 unrecognized selector 错误:

这是错误代码

private func callMe() {
    //...
}

NSTimer.scheduledTimerWithTimeInterval(1, target: self, 
           selector:"callMe", userInfo: nil, repeats: true)

正确的做法是在 private 前面加上 @objc 关键字,这样运行时就能找到对应的方法了。

@objc private func callMe() {
    //...
}

NSTimer.scheduledTimerWithTimeInterval(1, target: self, 
             selector:"callMe", userInfo: nil, repeats: true)

另外,如果方法的第一个参数有外部变量的话,在通过字符串生成 Selector 时还有一个约定,那就是在方法名和第一个外部参数之间加上 with

func aMethod(external paramName: AnyObject!) { ... }

想获取对应的获取 Selector,应该这么写:

let s = Selector("aMethodWithExternal:")

Sequence

Swift 的 for...in 可以用在所有实现了 SequenceType 的类型上,而为了实现 SequenceType 你首先需要实现一个 GeneratorType。比如一个实现了反向的 generator 和 sequence 可以这么写:

// 先定义一个实现了 GeneratorType protocol 的类型
// GeneratorType 需要指定一个 typealias Element 
// 以及提供一个返回 Element? 的方法 next()
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--
    }
}

// 然后我们来定义 SequenceType
// 和 GeneratorType 很类似,不过换成指定一个 typealias Generator
// 以及提供一个返回 Generator? 的方法 generate()
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]

// 对 SequenceType 可以使用 for...in 来循环访问
for i in ReverseSequence(array: arr) {
    println("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 g = array.generate()
while let obj = g.next() {
    println(obj)
}

顺便你可以免费得到的收益是你可以使用像 map , filter 和 reduce 这些方法,因为它们都有对应 SequenceType 的版本:

func map<S : SequenceType, T>(source: S, 
      transform: (S.Generator.Element) -> T) -> [T]

func filter<S : SequenceType>(source: S, 
      includeElement: (S.Generator.Element) -> Bool) -> [S.Generator.Element]

func reduce<S : SequenceType, U>(sequence: S, 
      initial: U, combine: (U, S.Generator.Element) -> U) -> U

@autoclosure 和 ??

Apple 为了推广和介绍 Swift,破天荒地为这门语言开设了一个博客(当然我觉着是因为 Swift 坑太多需要一个地方来集中解释)。其中有一篇提到了一个叫做 @autoclosure 的关键词。

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

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

func logIfTrue(predicate: () -> Bool) {
    if predicate() {
        println("True")
    }
}

在调用的时候,我们需要写这样的代码

logIfTrue({return 2 > 1})

当然,在 Swift 中对闭包的用法可以进行一些简化,在这种情况下我们可以省略掉 return,写成:

logIfTrue({2 > 1})

还可以更近一步,因为这个闭包是最后一个参数,所以可以使用尾随闭包 (trailing closure) 的方式把大括号拿出来,然后省略括号,变成:

logIfTrue{2 > 1}

但是不管那种方式,要么是书写起来十分麻烦,要么是表达上不太清晰,看起来都让人生气。于是 @autoclosure 登场了。我们可以改换方法参数,在参数名前面加上 @autoclosure 关键字:

func logIfTrue(@autoclosure predicate: () -> Bool) {
    if predicate() {
        println("True")
    }
}

这时候我们就可以直接写:

logIfTrue(2 > 1)

来进行调用了,Swift 将会吧 2 > 1 这个表达式自动转换为 () -> Bool。这样我们就得到了一个写法简单,表意清楚的式子。

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

var level : Int?
var startLevel = 1

var currentLevel = level ?? startLevel

在这个例子中我们没有设置过 level,因此最后 startLevel 被赋值给了 currentLevel。如果我们充满好奇心地点进 ?? 的定义,可以看到?? 有两种版本:

func ??<T>(optional: T?, @autoclosure defaultValue: () -> T?) -> T?
func ??<T>(optional: T?, @autoclosure defaultValue: () -> T) -> T

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

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

可能你会有疑问,为什么这里要使用 autoclosure,直接接受 T 作为参数并返回不行么?这正是 autoclosure 的一个最值得称赞的地方。如果我们直接使用 T,那么就意味着在 ?? 操作符真正取值之前,我们就必须准备好一个默认值,这个默认值的准备和计算是会消耗性能的。但是其实要是 optional 不是 nil 的话,我们是完全不需要这个默认值,而会直接返回 optional 解包后的值。这样一来,默认值就白白准备了,这样的开销是完全可以避免的,方法就是将默认值的计算推迟到 optional 判定为 nil 之后。

就这样,我们可以巧妙地绕过条件判断和强制转换,以很优雅的写法处理对 Optional 及默认值的取值了。最后要提一句的是,@autoclosure 并不支持带有输入参数的写法,也就是说只有形如 () -> T 的参数才能使用这个特性进行简化。另外因为调用者往往很容易忽视 @autoclosure 这个特性,所以在写接受 @autoclosure 的方法时还请特别小心,如果在容易产生歧义或者误解的时候,还是使用完整的闭包写法会比较好。

在 Swift 1.2 中,@autoclosure 的位置发生了变化。现在 @autoclosure 需要像本文中这样,写在参数名的前面作为参数修饰,而不是在类型前面作为类型修饰。但是现在标准库中的方法签名还是写在了接受的类型前面,这应该是标准库中的疏漏。在我们自己实现一个 autoclosure 时,在类型前修饰的写法在 Swift 1.2 中已经无法编译了。

练习

在 Swift 中,其实 && 和 || 这两个操作符里也用到了 @autoclosure。作为练习,不妨打开 Playground,试试看怎么实现这两个操作符吧?

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?
}

在实际使用中,我们想要知道小明的宠物的玩具的名字的时候,可以通过下面的 Optional Chaining 拿到:

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()}

这样调用的返回将是一个 ()? (或者写成 Void? 会更清楚一些),虽然看起来挺奇怪的,但这就是事实。使用的时候我们可以通过 Optional Binding 来判定方法是否调用成功:

if let result: () = playClosure(xiaoming) {
    println("好开心~")
} else {
    println("没有玩具可以玩 :(")
}

func 的参数修饰

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

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

这个方法接受一个 Int 的输入,然后通过将这个输入加 1,返回一个新的比输入大 1 的 Int。嘛,就是一个简单的 +1器

有些同学在大学的 C 程序设计里可能学过像 ++ 这样的“自增”运算符,再加上做了不少关于“判断一个数被各种前置 ++ 和后置 ++折磨后的输出是什么”的考试题,所以之后写代码时也会不自觉地喜欢带上这种风格。于是同样的功能可能会写出类似这样的方法:

这是错误代码

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

残念..编译错误。为什么在 Swift 里这样都不行呢?答案是因为 Swift 其实是一门讨厌变化的语言。所有有可能的地方,都被默认认为是不可变的,也就是用 let 进行声明的。这样不仅可以确保安全,也能在编译器的性能优化上更有作为。在方法的参数上也是如此,我们不写修饰符的话,默认情况下所有参数都是 let 的,上面的代码等效为:

func incrementor(let variable: Int) -> Int {
    return ++variable
}

let 的参数,不能重新赋值这是理所当然的。要让这个方法正确编译,我们需要做的改动是将 let 改为 var

func incrementor(var variable: Int) -> Int {
    return ++variable
}

现在我们的 +1器 又可以正确工作了:

var luckyNumber = 7
let newNumber = incrementor(luckyNumber)
// newNumber = 8

println(luckyNumber)
// luckyNumber 还是 7

正如上面的例子,我们将参数写作 var 后,通过调用返回的值是正确的,而 luckyNumber 还是保持了原来的值。这说明 var 只是在方法内部作用,而不直接影响输入的值。有些时候我们会希望在方法内部直接修改输入的值,这时候我们可以使用 inout 来对参数进行修饰:

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

因为在函数内部就更改了值,所以也不需要返回了。调用也要改变为相应的形式,在前面加上 & 符号:

var luckyNumber = 7
incrementor(&luckyNumber)

println(luckyNumber)
// luckyNumber = 8

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

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值