Zhihao's Studio.

《Swift必备Tips》读书笔记

Word count: 3,811 / Reading time: 15 min
2019/02/20 Share

写在前面

本文是对喵神编写的《Swift必备Tips(第四版)》的读书笔记,内容是我不太熟悉或觉得有用的Swift使用技巧,需要注意的是tips按照我的理解进行了一些合并和拆分

不断更新中~

将protocol的方法声明为mutating

mutating关键字是用来在方法中修改struct或enum变量的,若不声明会报错。

多元组Tuple

一个js中也有类似功能的东西,不适用额外空间完成交换的方式也和js中类似。

1
2
3
func swapMe2<T>( a: inout T, b: inout T) {
(a,b) = (b,a)
}

@autoclosure和??、@escaping关键字

@autoclosure

@autoclosure是把一句表达式自动封装成一个闭包,在语法上看起来会很漂亮。

1
2
3
4
5
6
7
func logIfTrue(_ predicate: @autoclosure () -> Bool) {
if predicate() {
print("True")
}
}
logIfTrue(2 > 1) // 2 > 1会被自动封装为一个闭包

??

??关键字可以快速地对nil进行条件判断,左侧值是不为nil的Optional值时返回其value,等于nil的时候返回右侧值。

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

??的两种实现都用到了@autoclosure关键字,主要是类似于短路表达式,左边不为nil时右边才有计算的需要,否则会造成浪费。从这里衍生出一条面试题,让你设计 || 或 && 的实现,也是右边设计成一个闭包,右侧有计算必要的时候再进行计算。

最后要注意,@autoclosure不支持带输入参数的写法,只有形如()->T的参数才能使用这个特性进行简化。

@escaping

@escaping是用来表明某个闭包是可以“逃逸”的。

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
37
38
39
40
41
func doWork(block: ()->()) {
block()
}
func doWorkAsync(block: @escaping ()->()) {
DispatchQueue.main.async {
block()
}
}
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
func method3() {
doWorkAsync {
[weak self] in
print(self?.foo ?? "nil")
}
foo = "bar"
}
S().method3() // nil

doWork不会”逃逸“,因此闭包的作用域不会超过函数本身,所以不需要担心闭包内持有self。而doWorkAsync则不同,由于需要确保闭包内成员有效性,如果在闭包内引用了self及其成员的话,需要强制明确的写出self。method3中,由于self已经被是否,因此输出nil。

static和class 关键字

异同点

相同点:static和class都是表示“类型范围作用域”这一概念的。
不同点:class是专门用在class类型的上下文中的,可以用来修饰类方法和类属性。
class中现在是不能出现在class的存储属性的。

1
2
3
class MyClass{
class var bar: Bar?
}

会得到一个编译错误,class variables not yet supported,改成static就可以通过编译了。

结论

任何时候使用static都是没问题的。

多类型和容器使用Any/AnyObject的技巧

Swift中常用的原生容器类型有Array/Dictionay/Set,他们都是范型的,也就是说放在一个集合中的类型要一致。

但是Any可以让我们在容器中放各种类型的元素,但也不可避免的带来了部分信息损失,从容器中取出后还需要进行一次类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
// Any 类型可以隐式转换
let mixed: [Any] = [1, "two", 3]
// 转换为 [NSObject]
let objectArray = [1 as NSObject, "two" as NSObject, 3 as NSObject]
//建议的使用方式
let mixed: [CustomStringConvertible] = [1, "two", 3]
for obj in mixed {
print(obj.description)
}

改善方法一

作者认为,把这些不同类型的元素放到一个容器中,肯定是由于他们有共性,也就是这些元素服从某个共同的协议,这样虽有一定损失,但相对于Any或AnyObject还是改善了不少。

改善方法二

另一种做法是用enum可以带有值的特点,将类型信息封装到enum中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum IntOrString {
case IntValue(Int)
case StringValue(String)
}
let mixed = [IntOrString.IntValue(1),
IntOrString.StringValue("two"),
IntOrString.IntValue(3)]
for value in mixed {
switch value {
case let .IntValue(i):
print(i * 2)
case let .StringValue(s):
print(s.capitalized)
}
}

AnyClass

除了上面提到的Any和AnyObject,还有一个表示任意这个概念的东西AnyClass。在Swift中的定义方式:

typealias AnyClass = AnyObject.Type

得到的是一个元类型(Meta),存储的是一个类的类型本身。除了可以用元类型来调用类方法或类变量,还可以用在传递类型的时候,不需要不断地改动代码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let usingVCTypes: [AnyClass] = [MusicViewController.self,
AlbumViewController.self]
func setupViewControllers(_ vcTypes: [AnyClass]) {
for vcType in vcTypes {
if vcType is UIViewController.Type {
let vc = (vcType as! UIViewController.Type).init()
print(vc)
}
}
}
setupViewControllers(usingVCTypes)

在编框架时,搭好框架后,用DSL的方式进行配置,就可以在不触及Swift编码的情况下,完成一系列复杂操作了。另外,Cocoa API中,也经常需要一个AnyClass的输入,如:self.tableView.registerClass(
UITableViewCell.self, forCellReuseIdentifier: “myCell”)

Self

首字母大写的Self,通常用在协议内,指代实现协议的类型和子类。实现代理方法有点技巧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protocol Copyable {
func copy() -> Self
}
class MyClass: Copyable {
var num = 1
func copy() -> Self {
let result = type(of: self).init()
result.num = num
return result
}
required init() {
}
}

直接使用MyClass()进行初始化是错误的,无法编译,因为该方法要求返回的是一个抽象的、表示当前类型的Self,而不是真实类型MyClass。type(of:)在js中也有,保证了方法与当前类型上下文无关,无论是MyClass还是它的子类,都可以正确地返回合适的类型,满足Self的要求。

注意需要有required关键字修饰init方法,保证当前类和子类都能响应init方法,否则type(of: self).init()可能会出错。另一个解决方法是申明class为final,保证不会有其他子类来继承这个类型,本质上也是保证type(of: self).init()不会出错。

动态类型和多方法 & protocol extension

Swift默认是不采用动态派发的,方法调用在编译时决定。如果想绕开这个限制,需要手动对输入类型做判断与转换。如果没有判断,即使printThem的函数第一个参数传入Dog(),也是派发的Pet的printPet()方法。

1
2
3
4
5
6
7
8
func printThem(_ pet: Pet, _ cat: Cat) {
if let aCat = pet as? Cat {
printPet(aCat)
} else if let aDog = pet as? Dog {
printPet(aDog)
}
printPet(cat)
}

关于调用什么方法,在protocol extension这个tips里也有这个问题。

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
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"
}
}
let a2 = b2 as A2
a2.method1() // hello
a2.method2() // hi

作者认为可以这样来理解:对于 method1,因为它在 protocol 中被定义了,因此对于一个被声明为遵守协议的类型的实例 (也就是对于 a2) 来说,可以确定实例必然实现了 method1,我们可以放心大胆地用动态派发的方式使用最终的实现 (不论它是在类型中的具体实现,还是在协议扩展中的默认实现);但是对于 method2 来说,我们只是在协议扩展中进行了定义,没有任何规定说它必须在最终的类型中被实现。在使用时,因为 a2 只是一个符合 A2 协议的实例,编译器对 method2 唯一能确定的只是在协议扩展中有一个默认实现,因此在调用时,无法确定安全,也就不会去进行动态派发,而是转而编译期间就确定的默认实现。

final 关键字

上面提到了final,作者对final关键字的态度是:虽然final能告诉编译器这段代码不会被更改,但是提升的性能非常有限,建议先优化算法和图像相关的内容。

final真正适用的几个场景:

  • 类方法或功能以及确实完备了,比如很难会重写计算字符串MD5或AES加密解密的工具类。
  • 子类继承和修改是一件危险的事情,比如在某个公司管理的系统中我们对员工按照一定规则进行编号,这样通过编号我们能迅速找到任一员工。而假如我们在子类中重写了这个编号方法,很可能就导致基类中的依赖员工编号的方法失效。
  • 为了父类中某些代码一定会被执行。比如有时候父类中有一些关键代码是在被继承重写后必须执行的 (比如状态配置,认证等等),否则将导致运行时候的错误。

lazy 关键字

在构建和生成新的对象时,内存分配会在运行时耗费不少时间,如果有一些对象的属性和内容非常复杂,耗时不可忽略;另外,有些情况下,我们不会立即使用到一个对象的所有属性,默认初始化会将全部变量初始化,包括特定环境下的存储属性,这是一种浪费,懒加载就是为此而生。

lazy除了可以修饰属性,也有其他用法:

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 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>
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("操作完毕")

用来配合像map和filter这类接受闭包并进行运行的方法一起,让整个行为变成延时的。有lazy和无lazy的输出是完全不同的。对于那些不需要完全运行,可能提前退出的情况,使用lazy是进行性能优化的一种有效手段。

weak和unowned

这两个关键字和内存管理关系紧密,主要是用来解决计数机制中的循环引用问题的。一般来说,我们希望“被动”的乙方不要去持有“主动”的一方。举一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class A: NSObject {
let b: B
override init() {
b = B()
super.init()
b.a = self
}
deinit {
print("A deinit")
}
}
class B: NSObject {
(weak/unowned) var a: A? = nil
deinit {
print("B deinit")
}
}

在class B对A的声明前加上weak/unowned关键字,就可以保证内存正确的释放。weak和unowned的区别是:unowned设置以后,即使它原来引用的内容以及被释放了,它仍然会保持对已经释放对象的一个“无效的”引用,它不能Optional值,也不会指向nil。但此时如果继续调用,程序就会崩溃。相比之下,weak就友好不少,在应用的内容释放后,标记为weak的成员会自动地变为nil。Apple的建议是如果能够确定在访问时不会已经被释放的话,尽量使用unowned,试过存在被释放的可能性,那就用weak。

两个实际经常使用的场景:

  1. 设置delegate。作者举了一个异步网络请求的例子,这种情况下无法保证在拿到返回时作为delegate的对象一定还存活,所以需要用weak关键字。
  2. 在self属性存储为闭包时,其中拥有对self的引用。闭包的例子非常经典,因为闭包中对任何其他元素的引用都是会被闭包自动持有的。如果我们在闭包中写了self,其实闭包内也持有了当前对象,如果当前实例直接或间接的有引用,就形成了self->闭包->self的循环引用。这种情况下使用哪个关键字的策略同上。

delegate

协议-委托(protocol-delegate)模式贯穿于整个Cocoa框架中,为代码之间的关系清理和解耦合做出了贡献。

上面已经提到了,在ARC中,对于一般的delegate,我们会在声明中将其指定为weak,这样实际对象被释放时,会被重置为nil。一种常见的错误是:

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 MyClassDelegate {
func method()
}
class MyClass {
weak var delegate: MyClassDelegate?
}
class ViewController: UIViewController, MyClassDelegate {
// ...
var someInstance: MyClass!
override func viewDidLoad() {
super.viewDidLoad()
someInstance = MyClass()
someInstance.delegate = self
}
func method() {
print("Do something")
}
//...
}

原因是Swift的Protocol不仅可以被class所遵守,也可以被struct和enum遵守,本身就不通过引用计数(ARC)来管理内存,所以也不可能用weak这样的ARC概念进行修饰。

在Swift中使用weak delegate,有两种解决方式,主要思路都是将protocol限制在class内。

  1. 在MyClassDelegate后面增加:class,使得只允许class遵守它;
  2. 在protocol前加上@objc,OC中只有类能实现protocol。

String和NSString

首先他们是可以无缝转换的,作者推荐尽可能使用String,原因有三:

  1. 现在Cocoa所有的API都接受和返回String类型
  2. String是struct,NSString是Object,String更符合字符串“不变”这一特性,在多线程编程时非常重要。另外,在不触及NSString特有操作和动态特性时,使用String的性能也更好。
  3. String实现了Collection协议,而NSString没有,所以有些Swift语法特性只有String可以用。比如for…in枚举。String和Range配合没有NSString和NSRange配合方便。

GCD和延时调用、取消的封装

GCD是一种非常方便的使用多线程的方式,在“复杂必死”的多线程编程中,保持简单就是避免错误的金科玉律。

作者给出的延时调用策略有两种,第一种不太推荐,是创建一个selector,因为它并不安全(需要通过字符串创建,改动代码比较危险)。第二种是使用asyncAfter

另外作者将其进行了封装,并提供了取消的功能,调用起来非常方便。

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
37
38
39
40
41
42
43
44
45
typealias Task = (_ cancel : Bool) -> Void
func delay(_ time: TimeInterval, task: @escaping ()->()) -> Task? {
func dispatch_later(block: @escaping ()->()) {
let t = DispatchTime.now() + time
DispatchQueue.main.asyncAfter(deadline: t, execute: block)
}
var closure: (()->Void)? = task
var result: Task?
let delayedClosure: Task = {
cancel in
if let internalClosure = closure {
if (cancel == false) {
DispatchQueue.main.async(execute: internalClosure)
}
}
closure = nil
result = nil
}
result = delayedClosure
dispatch_later {
if let delayedClosure = result {
delayedClosure(false)
}
}
return result;
}
func cancel(_ task: Task?) {
task?(true)
}
// 使用方法
let task = delay(2) {print("2秒后输出")}
cancel(task)

Lock的封装

OC中的@synchronized关键字在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
25
26
27
func synchronized(_ lock: AnyObject, closure: () -> ()) {
objc_sync_enter(lock)
closure()
objc_sync_exit(lock)
}
func myMethodLocked(anObj: AnyObject!) {
synchronized(anObj) {
// 在括号内持有 anObj 锁
}
}
// 一个实际的线程安全的 setter 例子
class Obj {
var _str = "123"
var str: String {
get {
return _str
}
set {
synchronized(self) {
_str = newValue
}
}
// 下略
}
}

结合swift尾随闭包的语言特性,使用起来就跟OC很像了。

属性访问控制

Swift中由低至高提供了private/fileprivate/internal/public/open五种访问控制权限。默认值是internal。
private让代码只能在当前作用域或者同一文件中同一类型的作用域中被使用,fileprivate表示代码可以在当前文件中被访问,而不做类型限定

作者认为:如果想让同意module或者target中的其他代码访问的话,保持默认的internal。如果在为其他开发者开发库的话,可能会希望用public甚至open,方便在target外调用。public和open的区别是,只有open的内容才能在别的框架中被继承或重写。因此,如果你只希望别人使用而不希望他们修改,就设为public,否则open。

CATALOG
  1. 1. 写在前面
  2. 2. 将protocol的方法声明为mutating
  3. 3. 多元组Tuple
  4. 4. @autoclosure和??、@escaping关键字
    1. 4.1. @autoclosure
    2. 4.2. ??
    3. 4.3. @escaping
  5. 5. static和class 关键字
    1. 5.1. 异同点
    2. 5.2. 结论
  6. 6. 多类型和容器使用Any/AnyObject的技巧
    1. 6.1. 改善方法一
    2. 6.2. 改善方法二
  7. 7. AnyClass
  8. 8. Self
  9. 9. 动态类型和多方法 & protocol extension
  10. 10. final 关键字
  11. 11. lazy 关键字
  12. 12. weak和unowned
  13. 13. delegate
  14. 14. String和NSString
  15. 15. GCD和延时调用、取消的封装
  16. 16. Lock的封装
  17. 17. 属性访问控制