这是我参与更文挑战的第8天,活动详情查看: 更文挑战
文章来自对 objccn.io/ 相关书籍的整理笔记。“感谢 objc.io 及其撰稿作者们无私地将他们的知识分享给全世界”。
31. lazy 修饰符和 lazy 方法
延时加载(延时初始化)是常用的优化方法,在构建和生成新的对象的时候,内存分配会在运行时耗费不少时间,如果有一些对象的属性和内容非常复杂,这个时间更是不可忽略。另外,有些情况下并不会立即用到一个对象的所有属性,而默认情况下初始化时,那些在特定环境下不被使用的存储属性,也一样要被初始化和赋值,也是一种浪费。
在其他语言 (包括 Objective-C) 中延时加载的情况是很常⻅的。第一次访问某个属性时,判断这个属性背后的存储是否已经存在,如果存在则直接返回,如果不存在则说明是首次访问,那么就进行初始化并存储后再返回。这样可以把这个属性的初始化时刻推迟,与包含它的对象的初始化时刻分开,以达到提升性能的目的。
// ClassA.h
@property (nonatomic, copy) NSString *testString;
// ClassA.m
- (NSString *)testString {
if (!_testString) {
_testString = @"Hello"; NSLog(@"只在首次访问输出");
}
return _testString;
}
复制代码
在 Swift 中使用在变量属性前加 lazy 关键字的方式来简单地指定延时加载。
class ClassA {
lazy var str: String = {
let str = "Hello" print("只在首次访问输出")
return str
}()
}
复制代码
在使用 lazy 作为属性修饰符时,只能声明属性是变量。另外需要显式地指定属性类型,并使用一个可以对这个属性进行赋值的语句来在首次访问属性时运行。如果多次访问这个实例的 str 属性的话有一次输出。
为了简化,如果不需要做什么额外工作的话,也可以对这个 lazy 的属性直接写赋值语句:
lazy var str: String = "Hello"
复制代码
另外一个不太引起注意的是,在 Swift 的标准库中,还有一组 lazy 方法,它们的定义是这样的:
func lazy<S : SequenceType>(s: S) -> LazySequence<S>
func lazy<S : CollectionType where S.Index : RandomAccessIndexType>(s: S)
-> LazyRandomAccessCollection<S>
func lazy<S : CollectionType where S.Index : BidirectionalIndexType>(s: S)
-> LazyBidirectionalCollection<S>
func lazy<S : CollectionType where S.Index : ForwardIndexType>(s: S)
-> LazyForwardCollection<S>
复制代码
这些方法可以配合像 map 或是 filter 这类接受闭包并进行运行的方法一起,让整个行为变成延时进行的。在某些情况下这么做也对性能会有不小的帮助。例如,直接使用 map 时:
let data = 1...3
let result = data.map {
(i: Int) -> Int in print("正在处理 \(i)") return i * 2
}
print("准备访问结果")
for i in result { print("操作后结果为 \(i)") }
print("操作完毕")
// 正在处理 1
// 正在处理 2
// 正在处理 3
// 准备访问结果
// 操作后结果为 2
// 操作后结果为 4
// 操作后结果为 6
// 操作完毕
复制代码
而如果先进行一次 lazy 操作的话,就能得到延时运行版本的容器:
let data = 1...3
let result = data.lazy.map {
(i: Int) -> Int in
print("正在处理 \(i)")
return i * 2
}
print("准备访问结果")
for i in result { print("操作后结果为 \(i)") }
print("操作完毕")
// 准备访问结果
// 正在处理 1
// 操作后结果为 2
// 正在处理 2
// 操作后结果为 4
// 正在处理 3
// 操作后结果为 6
// 操作完毕
复制代码
对于那些不需要完全运行,可能提前退出的情况,使用 lazy 来进行性能优化效果会非常有效。
32. Reflection 和 Mirror
反射 (Reflection)是一种在运行时检测、访问或者修改类型的行为的特性。一般的静态语言类型的结构和方法的调用等都需要在编译时决定,开发者能做的很多时候只是使用控制流 (比如 if 或者 switch) 来决定做出怎样的设置或是调用哪个方法。而反射特性可以有机会在运行的时候通过某些条件实时地决定调用的方法,或者甚至向某个类型动态地设置甚至加入属性及方法,是一种非常灵活和强大的语言特性。
Objective-C 中不太会经常提及到 “反射” 这样的词语,因为 Objective-C 的运行时比一般的反射还要灵活和强大。而在 Swift 中其实就算抛开 Objective- C 的运行时的部分,在纯 Swift 范畴内也存在有反射相关的一些内容,只不过相对来说功能要弱得多。
因为这部分内容并没有公开的文档说明,所以随时可能发生变动,或者甚至存在今后被从 Swift 的可调用标准库中去掉的可能。在实际的项目中,也不建议使用这种没有文档说明的 API,不过有时候如果能稍微知道 Swift 中也存在这样的可能性的话也许会有帮助。
Swift 中所有的类型都实现了 _Reflectable
,这是一个内部协议,可以通过 _reflect
来获取任意对象的一个镜像,这个镜像对象包含类型的基本信息,在 Swift 2.0 之前,这是对某个类型的象进行探索的一种方法。在 Swift 2.0 中,这些方法已经从公开的标准库中移除了,取而代之可以使用 Mirror 类型来做类似的事情:
struct Person {
let name: String
let age: Int
}
let xiaoMing = Person(name: "XiaoMing", age: 16)
let r = Mirror(reflecting: xiaoMing) // r 是 MirrorType
print("xiaoMing 是 \(r.displayStyle!)")
print("属性个数:\(r.children.count)") for child in r.children {
print("属性名:\(String(describing: child.label)),值:\(child.value)") }
// 输出:
// xiaoMing 是 Struct
// 属性个数:2
// 属性名:name,值:XiaoMing // 属性名:age,值:16
复制代码
通过 Mirror 初始化得到的结果中包含的元素的描述都被集合在 children 属性下,实际上是一个 Child 的集合,而 Child 则是一对键值的多元组:
public typealias Child = (label: String?, value: Any)
public typealias Children = AnyCollection<Mirror.Type.Child>
复制代码
AnyForwardCollection 是遵守 CollectionType 协议的,因此可以简单地使用 count 来获取元素的个数,而对于具体的代表属性的多元组,则使用下标进行访问。在例子中,每个 Child 都是具有两个元素的多元组,其中第一个是属性名,第二个是这个属性所存储的值。这个值有可能是多个元素组成嵌套的形式 (例如属性值是数组或者字典)。
也可以简单地使用 dump 方法来通过获取一个对象的镜像并进行标准输出的方式将其输出:
dump(xiaoMing)
// 输出:
// ▿ Person
// - name: XiaoMing
// -age:16
复制代码
对于一个从对象反射出来的 Mirror ,它所包含的信息是完备的。可以在运行时通过 Mirror 的手段了解一个 Swift 类型的实例的属性信息。该特性最容易想到的应用的特性就是为任意 model 对象生成对应的 JSON 描述。可以对等待处理的对象的 Mirror 值进行深度优先的访问,并按照属性的 valueType 将它们归类对应到不同的格式化中。
另一个常⻅的应用场景是类似对 Swift 类型的对象做像 Objective-C 中 KVC 那样的 valueForKey: 的取值。通过比较取到的属性的名字和我们想要取得的 key 值就行了,非常简单:
func valueFrom(_ object: Any, key: String) -> Any? {
let mirror = Mirror(reflecting: object)
for child in mirror.children {
let (targetKey, targetMirror) = (child.label, child.value)
if key == targetKey {
return targetMirror
}
}
return nil }
// 接上面的 xiaoMing
if let name = valueFrom(xiaoMing, key: "name") as? String {
print("通过 key 得到值: \(name)") }
// 输出:
// 通过 key 得到值: XiaoMing
复制代码
Swift 的反射特性并不是非常强大,只能对属性进行读取,还不能对其设定。虽然理论上将反射特性应用在实际的 app 制作中是可行的,但是这一套机制设计的最初目的是用于 REPL 环境和 Playground 中进行输出的。最好遵守 Apple 的这一设定,只在 REPL 和 Playground 中用它来对一个对象进行深层次的探索,而避免将它用在 app 制作中。
33. 隐式解包 Optional
相对于普通的 Optional 值,在 Swift 中还有一种特殊的 Optional,在对它的成员或者方法进行访问时,编译器会帮助自动进行解包,这就是 ImplicitlyUnwrappedOptional 。在声明的时候,通过在类型后加上一个感叹号 ( ! ) 这个语法糖来告诉编译器需要一个可以隐式解包的 Optional 值:
var maybeObject: MyClass!
复制代码
隐式解包的 Optional 本质上与普通的 Optional 值并没有任何不同,只是在对这类变量的成员或方法进行访问的时候,编译器会自动在后面插入解包符号 ! ,也就是说,对于一个隐式解包的下面的两种写法是等效的:
var maybeObject: MyClass! = MyClass()
maybeObject!.foo()
maybeObject.foo()
复制代码
如果 maybeObject 是 nil 的话那么这两种不加检查的写法的调用都会导致程序崩溃。 而如果 maybeObject 是普通的 Optional 的话,就只能使用第一种显式地加感叹号的写法,这能提醒应该使用 if let 的 Optional Binding 的形式来处理。而对隐式解包来说,后一种写法看起来就像操作的 maybeObject 确实是 MyClass 类的实例,不需要对其检查就可以使用。为什么以安全著称的 Swift 中会存在隐式解包这种危险写法呢?
因为 Objective-C 中 Cocoa 的所有类型变量都可以指向 nil 的,有一部分 Cocoa 的 API 中在参数或者返回时即使被声明为具体的类型,但是还是有可能在某些特定情况下是 nil ,而同时也有另一部分 API 永远不会接收或者返回 nil 。在 Objective-C 时,这两种情况并没有被加以区别,因为 Objective-C 里向 nil 发送消息并不会有什么不良影响。在将 Cocoa API 从 Objective-C 转为 Swift 的 module 声明的自动化工具里,是无法判定是否存在 nil 的可能的,因此也无法决定哪些类型应该是实际的类型,而哪些类型应该声明为 Optional。
在这种自动化转换中,最简单粗暴的应对方式是全部转为 Optional,然后让使用者通过 Optional Binding 来判断并使用。虽然这是最安全的方式,但对使用者来说是一件非常麻烦的事情。隐式解包的 Optional 就作为一个妥协方案出现了。使用隐式解包 Optional 的最大好处直接进行属性访问和方法调用。但隐式解包不意味着 “这个变量不会是 nil ,可以放心使用” 这种暗示,只能说 Swift 通过这个特性给了一种简便但是危险的使用方式罢了。
在 Swift 的正式 版本中,已经没有太多的隐式解包的 API 了。 Objective-C 中又加入了像是 nonnull 和nullable 这样的修饰符,那些真正有可能为 nil 的返回可以被明确定义为普通的Optional 值,而那些不会是 Optional 的值,也根据情况转换为了确定的类型。现在比较常⻅的隐
式解包的 Optional 就只有使用 Interface Builder 时建立的 IBOutlet 了:
@IBOutlet weak var button: UIButton!
复制代码
如果没有连接 IB 的话,对 button 的直接访问会导致应用崩溃,这种情况和错误在调试应用时是很容易被发现的问题。在代码的其他部分,还是少用这样的隐式解包,很多时候多写一个 Optional Binding 就可以规避掉不少应用崩溃的⻛险。
34. 多重 Optional
Optional 一个很容易让人迷惑的概念——多重的 Optional。
enum Optional<T> : _Reflectable, NilLiteralConvertible {
case None
case Some(T)
//...
复制代码
在这个定义中,对 T 没有任何限制,可以在 Optional 中装入任意东⻄的,甚至也包括 Optional 对象自身。
对于下面这种形式的写法:
var string: String? = "string"
var anotherString: String?? = string
复制代码
可以知道 anotherString 是 Optinal<Optional<String>>
。但是除开将一个 Optional 值 赋给多重 Optional 以外,也可以将直接的字面量值赋给它:
var literalOptional: String?? = "string"
复制代码
根据类型推断只能将 Optional<String>
放入到 literalOptional 中,所以可以猜测它与上面提到的 anotherString 是等效的。但是如果是将 nil 赋值给它的话,情况就有所不同了。考虑下面的代码:
var aNil: String? = nil
var anotherNil: String?? = aNil
var literalNil: String?? = nil
复制代码
anotherNil 和 literalNil 是不是等效的呢?答案是否定的。 anotherNil 是盒子中包了一个盒子;但是 literalNil 是盒子中直接是 nil。使用中一个最显著的区别在于:
if anotherNil != nil {
print("anotherNil")
}
if literalNil != nil {
print("literalNil")
}
// 只能输出 anotherNil
复制代码
另一个值得注意的地方时在Playground 中运行时,或者在用 lldb 进行调试时,直接使用 po 指令 打印 Optional 值的话,为了看起来方便,lldb 会将要打印的 Optional 进行展开。如果直接打 印上面的 anotherNil 和 literalNil ,得到的结果都是 nil。
如果遇到了多重 Optional 的麻烦的时候,可以使用 fr v -R 命令来打印出变量的未加工过时的信息:
(lldb) fr v -R anotherNil
(Swift.Optional<Swift.Optional<Swift.String>>)
anotherNil = Some {
... 中略 }
(lldb) fr v -R literalNil
(Swift.Optional<Swift.Optional<Swift.String>>)
literalNil = None {
... 中略 }
复制代码
35. Optional Map
经常会对 Array 类型使用 map 方法,这个方法能对数组中的所有元素应用某个规则,然后返回一个新的数组。可以在 CollectionType 的 extension 中找到这个方法的定义:
extension CollectionType {
public func map<T>(@noescape transform:
(Self.Generator.Element) -> T) -> [T]
//...
}
et arr = [1,2,3]
let doubled = arr.map{ $0 * 2 }
print(doubled) // 输出:
// [2,4,6]
复制代码
假设要将某个 Int? 乘 2。一个合理的策略是如果这个 Int? 有值的话,就取出值进行乘 2 的操作,如果是 nil 的话就直接将 nil 赋给结果:
let num: Int? = 3
var result: Int?
if let realNum = num {
result = realNum * 2
} else {
result = nil
}
复制代码
有更优雅简洁的方式,使用 Optional 的 map 。Optional 的声明也有一个 map 方法:
public enum Optional<T> :
_Reflectable, NilLiteralConvertible {
//...
/// If `self == nil`, returns `nil`. Otherwise, returns `f(self!)`.
public func map<U>(@noescape f: (T) -> U) -> U?
//...
}
复制代码
这个方法能很方便地对一个 Optional 值做变化和操作,而不必进行手动的解包工作。输入会被自动用类似 Optinal Binding 的方式进行判断,如果有值,则进入 f 的闭包进行变换,并返回一个 U? ;如果输入就是 nil 的话,则直接返回值为 nil 的 U? 。
let num: Int? = 3
let result = num.map {
$0 * 2 }
// result 为 {Some 6}
复制代码
。函子指的是可以被某个 函数作用,并映射为另一组结果,而这组结果也是函子的值。例子中, Array 的 map 和 Optional 的 map 都满足这个概念,它们分别将 [Self.Generator.Element] 映射为 [T] 以及 T? 映 射为 U? 。
36. Protocol Extension
Swift 2 中引入了一个非常重要的特性,那就是 protocol extension。在 Swift 1.x 中,extension 仅只能作用在实际的类型上 (也就是 class , struct 等等),而不能扩展一个 protocol。在 Swift 中, 标准库的功能基本都是基于 protocol 来实现的,Array 就是遵守了 CollectionType 这个 protocol 的。 CollectionType 可以说是 Swift 中非常重要的协议,除了 Array 以外,像是 Dictionary 和 Set 也都实现了这个协议所定义的内容。
在 protocol 不能被扩展的时候,想要为实现了某个协议的所有类型添加一些另外的共通的功能时,会非常麻烦。Swift 1.x 时像是 map 或者 filter 这样的函数。大体来说有两种思路进行添加:第一种方式是在协议中定义这个方法,然后在所有实现了这个协议的类型中都去实现一遍。每有一个这样的类型,就需要写一份类似甚至相同的方法,麻烦而且完全没有可维护性。另一种方法是在全局范围实现一个接受该 protocol 的实例的方法,相比于前一种方式,只需要维护一份代码,显然要好不少,但是缺点在于在全局作用域中引入了只和特定 protocol 有关的东⻄,不符合代码设计。Apple 在 Swift 1.x 中采用的是后一种,也就是全局方法。
在 Swift 2 中这个问题被彻底解决了。可以对一个已有的 protocol 进行扩展,而扩展中实现的方法将作为实现扩展的类型的默认实现。也就是说,假设我们有下面的 protocol 声明,以及一个对该协议的扩展:
protocol MyProtocol {
func method()
}
extension MyProtocol {
func method() {
print("Called")
}
}
复制代码
在具体的实现这个协议的类型中,即使不写,也可以编译通过。进行调用的话,会直接使用 extension 中的实现:
struct MyStruct: MyProtocol { }
MyStruct().method()
// 输出:
// Called in extension
复制代码
需要在类型中进行其他实现的话,可以像以前那样在具体类型中添加这个方法:
struct MyStruct: MyProtocol {
func method() {
print("Called in struct")
}
}
MyStruct().method()
// 输出:
// Called in struct
复制代码
protocol extension 为 protocol 中定义的方法提供了一个默认的实现。有了这个特性以后,之前被放在全局环境中的接受 CollectionType 的 map 方法,就可以被移动到 CollectionType 的协议扩展中去了:
extension CollectionType {
public func map<T>(@noescape transform: (Self.Generator.Element) -> T) -> [T]
//...
}
复制代码
在日常开发中,另一个可以用到 protocol extension 的地方是 optional 的协议方法。通过提供 protocol 的 extension,为 protocol 提供了默认实现,这相当于变相将 protocol 中的方法设定为了 optional。
对于 protocol extension 来说,有一种会非常让人迷惑的情况,就是在协议的扩展中实现了协议里 没有定义的方法时的情况。举个例子,比如定义了这样的一个协议和它的一个扩展:
protocol A1 {
func method1() -> String
}
struct B1: A1 {
func method1() -> String {
return "hello"
}
}
// 在使用的时候,无论将实例的类型为 A1 还是 B1,
// 调用方法时的输出都是 “hello”:
let b1 = B1() // b1 is B1
b1.method1()
// hello
let a1: A1 = B1()
// a1 is A1
a1.method1()
// hello
复制代码
但是如果在协议里只定义了一个方法,而在协议扩展中实现了额外的方法的话,考虑下面这组协议和它的扩展:
protocol A2 {
func method1() -> String
}
extension A2 {
func method1() -> String {
return "hi"
}
func method2() -> String {
return "hi"
}
}
struct B2: A2 {
func method1() -> String {
return "hello"
}
func method2() -> String {
return "hello"
}
}
// B2 中实现了 method1 和 method2
// 初始化一个 B2 对象,然后对这两个方法进行调用:
let b2 = B2()
b2.method1() // hello
b2.method2() // hello
复制代码
虽然在 protocol extension 中已经实现了这两个方法,但是它们只是默认的实现,在具体实现协议的类型中可以对默认实现进行覆盖,这非常合理。但是如果稍作改变,在上面的代码后面继续添加:
let a2 = b2 as A2
a2.method1() // hello
a2.method2() // hi
复制代码
a2 和 b2 是同一个对象,只不过通过 as 告诉编译器我们在这里需要的类型是 A2 。但是这时候在这个同样的对象上调用同样的方法调用却得到了不同的结果。
对 a2 调用 method2 实际上是协议扩展中的方法被调用了,而不是 a2 实例中的方法被调用。对于 method1 ,因为它在 protocol 中被定义了,因此对于一个被声明为遵守协议的类型的实例 (也就是对于 a2 ) 来说,可以确定实例必然实现了 method1 ,可以放心大胆地用动态派发的方式使用最终的实现 但是对于 method2 来说,只是在协议扩展中进行了定义,没有任何规定说它必须在最终的类型中被实现。在使用时,因为 a2 只是一个符合 A2 协议的实例,编译器对 method2 唯一能确定的只是在协议扩展中有一个默认实现,因此在调用时,无法确定安全,也就不会去进行动态派发,而是转而编译期间就确定的默认实现。
实际中估计并不会有人将一个已知类型实例转回协议类型。但是要考虑到一些泛型 API 中有类似的直接拿到一个协议类型的结果的时候,调用它的扩展方法时就需要特别小心了:一般来说,如果有这样的需求话,可以考虑将这个协议类型再转回实际的类型,然后进行调用。
- 如果类型推断得到的是实际的类型
那么类型中的实现将被调用;如果类型中没有实现的话,那么协议扩展中的默认实现将被使用 - 如果类型推断得到的是协议,而不是实际类型
并且方法在协议中进行了定义,那么类型中的实现将被调用;如果类型中没有实现,那么协议扩展中的默认实现被使用。否则 (也就是方法没有在协议中定义),扩展中的默认实现将被调用。
37. where 和模式匹配
where 关键字在 Swift 中非常强大。
首先是 switch 语句中,可以使用 where 来限定某些条件 case:
let name = ["王小二","张三","李四","王二小"]
name.forEach {
switch $0 {
case let x where x.hasPrefix("王"): print("\(x)是笔者本家")
default: print("你好,\($0)")
}
}
// 输出:
// 王小二是笔者本家 // 你好,张三
// 你好,李四
// 王二小是笔者本家
复制代码
是模式匹配的标准用法,对 case 条件进行限定可以让我们更灵活地使用 switch 语句。
在 for 中也可以使用 where 来做类似的条件限定:
let num: [Int?] = [48, 99, nil]
let n = num.flatMap {$0}
for score in n where score > 60 {
print("及格啦 - \(score)") }
// 输出:
// 及格啦 - Optional(99)
复制代码
和 for 循环中类似,也可以对可选绑定进行条件限定。不过在 Swift 3 中, if let 和 guard let 的条件不再使用 where 语句,而是直接和普通的条件判断一样,用逗号写在 if 或者 guard 的后面:
num.forEach {
if let score = $0, score > 60 {
print("及格啦 - \(score)") }
else {
print(":(")
}
}
// 输出:
// :(
// 及格啦 - 99
// :(
复制代码
这两种使用的方式都可以用额外的 if 来替代,这里只不过是让代码更加易读了。也有一些场合是只有使用 where 才能准确表达的,比如在泛型中想要对方法的类型进行限定的时候。比如在标准库里对 RawRepresentable 协议定义 != 运算符定义时:
/// Returns `true` iff `lhs.rawValue != rhs.rawValue`.
public func !=<T : RawRepresentable
where T.RawValue : Equatable>(lhs: T, rhs: T) -> Bool
复制代码
这里限定了 T.RawValue 必须要遵守 Equatable 协议,这样才能通过对比 lhs 和 rhs 的 rawValue 是否相等,进而判断这两个 RawRepresentable 值是否相等。如果没有 where 的保证的话,下面的代码就无法编译。同时限定了那些 RawValue 无法判等的 RawRepresentable 类型不能进行判等。
/// Returns `true` iff `lhs.rawValue != rhs.rawValue`.
public func !=<T : RawRepresentable
where T.RawValue : Equatable>(lhs: T, rhs: T) -> Bool {
return lhs.rawValue != rhs.rawValue
}
复制代码
在 Swift 2.0 中,引入了 protocol extension。在有些时候会希望一个协议扩展的默认实现只在某些特定的条件下适用,这时我们就可以用 where 关键字来进行限定。标准库中的协议扩展大量使用了这个技术来进行限定,比如 Sequence 的 sorted 方法就被定义在这样一个类型限制的协议扩展中:
extension Sequence where Self.Iterator.Element : Comparable {
public func sorted() -> [Self.Iterator.Element]
}
复制代码
如果 Sequence (比如一个 Array) 中的元素是不可比较的,那么 sort 方法自然也就不能适用了:
let sortableArray: [Int] = [3,1,2,4,5]
let unsortableArray: [Any?] = ["Hello", 4, nil]
sortableArray.sortd()
// [1,2,3,4,5]
unsortableArray.sortd()
// 无法编译
// note: expected an argument list of type
// '(Self.Iterator.Element, Self.Iterator.Element) -> Bool'
// 这意味着 Swift 尝试使用带有闭包的 `sorted` 方法,并期望你输入一种排序方式
复制代码
38. indirect 和嵌套 enum
在涉及到一些数据结构的经典理论和模型 (没错,就是链表,树和图) 时,往往会用到嵌套的类型。比如链表,在 Swift 中,可以这样来定义一个单向链表:
class Node<T> {
let value: T?
let next: Node<T>?
init(value: T?, next: Node<T>?) {
self.value = value
self.next = next
}
}
let list = Node(value: 1,
next: Node(value: 2,
next: Node(value: 3,
next: Node(value: 4, next: nil))))
// 单向链表:1->2->3->4
复制代码
看起来还不错,但是这样的形式在表达空节点的时候并不十分理想。不得不借助于 nil 来表达空节点,但是事实上空节点和 nil 并不是等价的。另外,如果想表达一个空链表的话,要么需要把 list 设置为 Optional,要么把 Node 里的 value 以及 next 都设为 nil ,这导致描述中存在歧义,不得不去做一些人为的约定来表述这样的情况,这在算法描述中是十分致命的。
在 Swift 2 中,可以使用嵌套的 enum 了 — 而这在 Swift 1.x 里是编译器所不允许的。用 enum 来重新定义链表结构的话,会是下面这个样子:
indirect enum LinkedList<Element: Comparable> {
case empty
case node(Element, LinkedList<Element>)
}
let linkedList = LinkedList.node(1, .node(2, .node(3, .node(4, .empty))))
// 单项链表:1->2->3->4
复制代码
在 enum 的定义中嵌套自身对于 class 这样的引用类型来说没有任何问题,但是对于像 struct 或者 enum 这样的值类型来说,普通的做法是不可行的。需要在定义前面加上 indirect 来提示编译器不要直接在值类型中直接嵌套。用 enum 表达链表的好处在于,可以清晰地表示出空节点这一定义,这在像 Swift 这样类型十分严格的语言中是很有帮助的。实现链表节点的删除方法,在 enum 中添加:
func removing(_ element: Element) -> LinkedList<Element> {
guard case let .node(value, next) = self else {
return .empty
}
return value == element ?
next : LinkedList.node(value, next.removing(element))
}
let result = linkedList.removing(2)
print(result)
// 1 -> 3 -> 4
复制代码
2. 从 Objective-C/C 到 Swift
39. Selector
@selector 是 Objective-C 时代的一个关键字,可以将一个方法转换并赋值给一个 SEL 类型, 它的表现很类似一个动态的函数指针。在 Objective-C 时 selector 非常常用,从设定 target-action,到自举询问是否响应某个方法,再到指定接受通知时需要调用的方法等等,都是由 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 了,取而代之,从 Swift 2.2 开始使用 #selector 来从暴露给 Objective-C 的代码中获取一个 selector。在 Swift 里对应原来 SEL 的类型是一个叫做
Selector 的结构体。像上面的两个例子在 Swift 中等效的写法是:
@objc func callMe() {
//...
}
@objc func callMeWithParam(obj: AnyObject!) {
//...
}
let someMethod = #selector(callMe)
let anotherMethod = #selector(callMeWithParam(obj:))
复制代码
和 Objective-C 时一样,在 callMeWithParam 后面加上冒号和参数名 (:),这才是完整的方法名 字。多个参数的方法名也和原来类似,是这个样子:
@objc func turn(by angle: Int, speed: Float) {
//...
}
let method = #selector(turn(by:speed:))
复制代码
最后需要注意的是,selector 其实是 Objective-C runtime 的概念。在 Swift 4 中,默认情况下所有的 Swift 方法在 Objective-C 中都是不可⻅的,所以需要在这类方法前面加上 @objc 关键字, 将这个方法暴露给 Objective-C,才能进行使用。
在 Swift 3 和之前的版本中,Apple 为了更好的 Objective-C 兼容性,会自动对 NSObject 的子类的非私有方法进行推断并为在幕后为它们自动加上 @objc 。但是这需要每次 Swift 代码变动时都重新生成 Objective-C 所使用的头文件,这将造成 Swift 与 Objective-C 混编时速度大幅恶化。 另外, 即使在 Swift 3 中,私有方法也只在 Swift 中可⻅,在调用这个 selector 时会遇到一个 unrecognized selector 错误:
// 这是错误代码
// In Swift 3
private func callMe() {
//...
}
NSTimer.scheduledTimerWithTimeInterval(1, target: self,
selector:#selector(callMe), userInfo: nil, repeats: true)
复制代码
正确的做法是在 private 前面加上 @objc 关键字,这样运行时就能找到对应的方法了。
@objc private func callMe() {
//...
}
NSTimer.scheduledTimerWithTimeInterval(1, target: self,
selector:#selector(callMe), userInfo: nil, repeats: true)
复制代码
同理,现在如果想要 Objective-C 能使用 Swift 的类型或者方法的话,也需要进行相应的标记。对于单个方法,在前面添加 @objc 。如果想让整个类型在 Objective-C 可用,可以 在类型前添加 @objcMembers 。
如果方法名字在方法所在域内是唯一的话,可以简单地只是用方法的名字来作为 #selector 的内容。相比于前面带有冒号的完整的形式来说,这么写起来会方便一些:
let someMethod = #selector(callMe)
let anotherMethod = #selector(callMeWithParam)
let method = #selector(turn)
复制代码
但是,如果在同一个作用域中存在同样名字的两个方法,即使它们的函数签名不相同,Swift 编译器也不允许编译通过:
@objc func commonFunc() {
}
@objc func commonFunc(input: Int) -> Int {
return input
}
let method = #selector(commonFunc)
// 编译错误,`commonFunc` 有歧义
复制代码
可以通过将方法进行强制转换来使用:
let method1 = #selector(commonFunc as ()->())
let method2 = #selector(commonFunc as (Int)->Int)
复制代码
40. 实例方法的动态调用
在 Swift 中有一类很有意思的写法,可以不直接使用实例来调用这个实例上的方法,而是通过类型取出这个类型的某个实例方法的签名,然后再通过传递实例来拿到实际需要调用的方法。
class MyClass {
func method(number: Int) -> Int {
return number + 1
}
}
// 想要调用 method 方法的话,最普通的使用方式是生成 MyClass 的实例,
// 用 .method 来调用 它:
let object = MyClass()
let result = object.method(number: 1)
// result = 2
复制代码
这就限定了只能够在编译的时候就决定 object 实例和对应的方法调用。其实还可以使用刚才说到的方法,将上面的例子改写为:
let f = MyClass.method
let object = MyClass()
let result = f(object)(1)
复制代码
这种语法看起来会比较奇怪,但是实际上并不复杂。Swift 中可以直接用 Type.instanceMethod 的语法来生成一个可以柯里化的方法。如果观察 f 的类型 (Alt + 单击),可以知道它是:
f: MyClass -> (Int) -> Int
复制代码
其实对于 Type.instanceMethod 这样的取值语句,实际上刚才 let f = MyClass.method
做的事情是类似于下面这样的字面量转换:
let f = { (obj: MyClass) in obj.method }
复制代码
这种方法只适用于实例方法,对于属性的 getter 或者 setter 是不能用类似的写法的。另外,如果 遇到有类型方法的名字冲突时:
class MyClass {
func method(number: Int) -> Int {
return number + 1
}
class func method(number: Int) -> Int {
return number
}
}
复制代码
如果不加改动,MyClass.method 将取到的是类型方法,如果想要取实例方法的话,可以显式地加上类型声明加以区别。这种方式不仅在这里有效,在其他大多数名字有歧义的情况下,都能很好地解决问题:
let f1 = MyClass.method // class func method 的版本
let f2: (Int) -> Int = MyClass.method // 和 f1 相同
let f3: (MyClass) -> (Int) -> Int = MyClass.method // func method 的柯里化版本
复制代码