AnyClass 元类 和 .self
1 | typealias AnyClass = AnyObject.Type |
通过 AnyObject.Type 这种方式所得到的是一个元类型(Meta)。在声明时我们总是在类型名称后面加上 .Type,比如 A.Type 代表 A 这个类型的类型。而在 A 中取出其类型时,我们需要使用到 .self:
1 | class A { |
Swift 中,.self 可以用在类型后面取得类型本身,也可以用在某个实例后面取得这个实例本身。前一种可以获取一个表示该类型的值。
AnyObject.Type,或者说 AnyClass 所表达的东西其实并没有什么奇怪,就是任意类本身。上面 A 的类型的取值,可以强制让它是一个 AnyClass:
1 | class A { |
这样,如果 A 中有一个类方法,可以通过 typeA 来调用
1 | class A { |
这样做有什么意义呢?我们可以直接使用 A.method()来调用。对于单个独立的类型来说我们完全没有必要关心它的元类型,但是元类型或者元编程的概念可以变的非常灵活,在编写一些框架性的代码时会非常方便。比如:
1 | // 下面的例子中,虽然我们是用代码声明的方式获取了 MusicViewController 和 AlbumViewController 的元类型 |
这么一来,我们完全可以大好框架,然后用 DSL 的方式进行配置,就可以在不触及 Swift 的编码的情况下,很简单地完成一系列复杂操作了。
在 Cocoa API 中我们也常遇到需要一个 AnyClass 的输入,这时候应该使用.self 的方式获取所需要的元类型。比如 注册 tableView 的 cell 的类型。
self.tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: “myCell”)
.Type 表示的是某个类型的元类型,在 Swift 中,除了 class,struct 和 enum 这三个类型外,我们还可以定义 protocol,对于 protocol 来说,有时候我们也会想去的接口的元类型,可以在某个 protocol 的名字后面使用 .Protocol 来获取,使用的方法和.Type 是类似的。
接口和类方法中的 Self
1 | protocol IntervalType { |
在声明协议的时候,不知道最后是什么样的类型来实现这个接口,如果我们希望在协议中使用的类型就是实现这个接口本身的类的话,就需要使用 self 进行指代
这种情况下,self 不仅指代类型本身,也指代其子类,self 定义:
1 | protocol Copyable{ |
1 | class MyClass: Copyable { |
这里不能返回 MyClass。因为类型其实是不能确定的,在对象类型中,提到过 dynamicType(动态类型)
1 | func copy() -> Self { |
想要构建一个 Self 类型的对象的话,需要有 required 关键字修饰的初始化方法,因为 Swift 必须保证当前类和其子类都能响应这个 init 方法。
1 | class MyClass: Copyable { |
对于 MyClass 的子类,copy()方法也能正确返回子类的经过拷贝的对象了。
另一个可以使用 Self 的地方是在类方法中,使用起来十分类似,核心就在于保证子类也能返回恰当的类型。
动态类型和多方法
在 Swift 里,我们可以重载同样名字的方法,只需要保证参数类型不同:
1 | class Pet {} |
对这些方法进行调用时,编译器将帮助我们找到最精准的匹配。
但如果函数需要一个父类参数,我们传入子类参数时,子类类型会被忽略掉。
1 | func printThem(pet: Pet, _ cat: Cat) { |
Swift 默认情况下是不采用动态派发的,因此方法调用只能在编译时决定。
想要绕过这个限制,我们需要进行通过对输入类型做出判断和转换:
1 | func printThem(pet: Pet, _ cat: Cat) { |
属性观察
Property Observers 属性观察这种特性,利用属性观察,我们可以在当前类型内监视对于属性的设定,并作出一些响应。
1 | class MyClass { |
同一个类型中,属性观察和计算属性是不能共存的,计算属性中,通过改写 set 中的内容可以达到和 willSet 和 didSet 一样的目的。如果无法改动这个类,又想通过属性观察做一些事情的话,需要子类化这个类,并重写它的属性。在子类的重载属性中,我们可以对父类的属性任意的添加属性观察
1 | class A { |
get 首先被调用了一次,这是因为我们实现了 didSet,didSet 会用到 oldValue 这个值需要在整个 set 动作之前进行获取并存储代用,否则无法确保正确性。
final
写 Swift 在什么情况下使用 final
权限控制,这段代码不会再被修改,没有进行继承或重写的必要
类或者方法的功能确实已经完备了
子类继承和修改是一件危险的事情
举个例子,在某个公司管理的系统中,员工按照一定规则进行编号,这样通过编号能迅速找到任何一个员工,加入在子类中重写了这个编号的方法,就导致基类中依赖的员工编号的方法失效
- 为了父类中某些代码一定会被执行
有时候,父类中一些关键代码在被继承和重写后 必须执行,否则将导致运行的时候的错误。在 Objective-C 时,可以通过指定 attribute((objc_requires_super)) 这样的属性让编译器在子类没有调用父类方法时抛出警告。
1 | class Parent { |
这样,无论我们如何使用 method,都可以保证需要的代码一定被运行过,同时又给子类继承和重写自定义具体实现的机会。
final 防止方法被重写。
- 性能考虑
使用 final 的另一个重要理由是可能带来的性能改善,编译器可以从 final 中获取额外信息,因此对类或者方法调用进行额外的优化处理。项目还有其他方面可以优化(一般来说会是算法或者图形相关的内容导致的瓶颈)的情况下,不建议使用将类或者方法转化为 final 的方式追求性能的提升。
lazy 修饰符和 lazy 方法
1 | class ClassA { |
Swift 的标准库中,还有一组 lazy 方法:
1 | func lazy<S : SequenceType>(s: S) -> LazySequence<S> |
这些方法可以配合像 map 或者 filter 这类接受闭包并运行运行的方法一起,让整个行为编程延时进行。
对于那些不需要完全运行,可能提前退出的情况,使用 lazy 来进行性能优化效果会非常有效。
Reflection 和 Mirror
熟悉 Java 的读者可能会知道反射(Reflection)。这是一种在运行时检测、访问或者修改类型的行为的特性。一般的静态语言类型的结构和方法的调用等都需要在编译时决定,开发者能做的很多时候只是 使用控制流 来决定作出怎样的设置 或者是调用哪些方法。 反射特性可以让我们有机会再运行的时候通过某些条件实时地决定调用的方法,是一种非常灵活和强大的语言特性。
Objective-C 中,我们不太会提及“反射”这样的词语,因为运行时 比 一般的反射还要灵活和强大。纯 Swift 中也存在有反射相关的一些内容,但功能要弱的多。
多重 Optional
1 | enum Optional<T> : _Reflectable, NilLiteralConvertible { |
1 | var string: String? = "string" |
anotherString 是 Optional<Optional
1 | var aNil: String? = nil |
anotherNil 和 literalNil 不是等效的~!,anotherNil 是盒子中包了一个盒子,literalNil 盒子中直接是空气。
1 | if let a = anotherNil { |
这样的代码只能输出 anotherNil
在 Playground 中运行时,如果在用 lldb 进行调试,使用 po 指令打印 Optional 值。
如果遇到了多重 Optional 的麻烦,可以使用 fr v -r 命令打印出变量的未加工过时的信息:
1 | (lldb) fr v -R anotherNil |
这样就能清晰的分辨出两者的区别了
Optional Map
Map :
1 | let arr = [1,2,3] |
还可以使用 Optional 的 map,注意定义:
1 | public enum Optional<T> : |
这个方法让我们很方便地对一个 Optional 值做变化和操作,不必进行手动的解包工作。如果有有值,进入 f 的闭包进行变换,并返回一个 U ? 如果输入是 nil, 则直接返回值为 nil 的 U?
1 | let num: Int? = 3 |
Protocol Extension
1 | protocol MyProtocol { |
protocol extension 为 protocol 中定义的方法提供了一个默认的实现。
有一种情况会让人迷惑,例子:
1 | protocol A1 { |
实现只有一个,无论我们将实例类型定义为 A1 还是 B1,输出都是 hello
但是如果在接口里值定义了一个方法,而在接口扩展中实现了额外的方法的话,:
1 | protocol A2 { |
扩展中除了实现接口定义的 method1之外还定义了一个接口中不存在的 method2:
1 | struct B2: A2 { |
虽然在 protocol extension 中已经实现了这两个方法,但是它们只是默认的实现,我们在具体实现接口的类型中可以对默认实现进行覆盖,如果稍作改变:
1 | let a2 = b2 as A2 |
a2和 b2是同一个对象,只是通过 as 告诉编译器我们这里需要的类型是 A2 , method1 在 protocol 中被定义了,所以对于遵循 protocol 的实例 a2来说,可以确定实例必然实现了 method1,因此会动态派发的方式使用最终实现。但是对 method2来说,只在接口扩展中进行了定义,没有任何规定说必须在最终的类型中被实现。使用时,编译器对 method2 唯一能确定的是指在接口扩展中有一个默认的实现,为了确保安全,就不会进行动态派发,转而是编译器就确定的默认实现。
整理一下:
1 | 如果类型推断得到的是实际类型,那么类型中的实现将被调用;如果类型中没有实现,接口扩展中的默认实现被调用 |
where 和模式匹配
作用的地方:
if let 、for
约束泛型
协议扩展
indirect 和 嵌套 enum
1 | class Node<T> { |
涉及到数据结构的经典理论和模型,链表,树和图。我们往往会用嵌套的类型,可以像上述方式定义一个单向链表。
这样的形式在表达空节点的时候不会十分理想,不得不借助 nil 表达空节点,实际上空节点和 nil 并不等价。如果要表达一个空链表,需要把 list 设置为 Optional,要么把 Node 里的 value 和 next 都设为 nil。这样会存在歧义。
使用嵌套的 enum:
1 | indirect enum LinkedList<Element: Comparable> { |
在 enum 的定义中嵌套自身对于 class 这样引用类型来说没有任何问题,但是对于像 struct 或者 enum 这样的值类型来说,普通的做法是不可行的。需要在定义前加上 indirect 来提示表一起不要在值类型中直接嵌套。使用 enum 表达链表的好处在于,我们可以清晰地表示出空节点这一定义,在 enum 中实现链表节点的删除方法:
1 | indirect enum LinkedList<Element: Comparable> { |
从 Objective-C 到 Swift
Selector
在 Swift 中没有@selector 了,我们要生成一个 selector 的话现在只能使用字符串了。Swift 里对应原来 SEL 的类型是一个叫 Selector 的结构体,
因为 Selector 实现了 StringLiteralConvertible,所以可以直接用字符串进行赋值,
selector 是 Objec Runtime 的概念,如果 Swift 中 selector 对应的方法是一个 private 方法,调用这个 selector 时,会遇到一个 unrecognized selector 错误。
需要在 private 前面加上 @objc 告诉编译器该方法参与动态派发。
实例方法的动态调用
在 Swift 中有一类很有意思的写法,可以让我们不直接使用实例来调用实例上的方法,而是通过类型取出这个类型的某个实例方法的签名,然后再通过传递实例来拿到实际需要调用的方法,:
1 | class MyClass { |
Swift 中可以直接用 Type.instanceMethod 的语法生成一个可以柯里化的方法,如果观察 f 的类型
f: (MyClass) -> (Int) -> Int
这种方法只适用于实例方法,对于属性的 getter 和 setter 是不能用类似的写法的,如果我们遇到有类型方法的名字冲突:
1 | class MyClass { |
这里如果不加改动,MyClass.method 将取到的是类型方法,如果我们想要取实例方法的话,可以显式地加上类型声明加以区别。
1 | let f1 = MyClass.method |
单粒
1 | @implementation MyManager |
1 | class MyManager { |
1.2之前,Swift 不支持存储类型的类属性,我们需要一个 struct 来存储类型变量。
Swift 里有一个更简单的保证线程安全的方法,就是 let。
1 | class MyManager { |
还有一种写法:
1 | private let sharedInstance = MyManager() |
private 关键字表示这个变量只能在当前文件夹中被访问。
1.2 以后:
1 | class MyManager { |
条件编译
C 中的#if #ifdef
Swift 中没有宏定义的概念,Swift 为我们提供了几种简单的的机制来根据需求定制编译内容
首先是#if 这套编译标记存在:
1 | #if <condition> |
当然,#elseif 和 #else 是可选的
几个表达式里的 condition 并不是任意的。Swift 内建了几种平台和架构组合,帮助我们为不同的平台编译不同的代码:
方法: os() 可选参数 OSX、iOS
arch() x86_64,arm,arm64,i386
这些方法和参数都是大小写敏感,如果我们统一在 iOS 平台和 Mac 平台的关于颜色的 API 的话,一个种可能的方法就是配合 typealias 条件编译:
1 | #if os(OSX) |
arch() 的参数 arm 和 arm64两项分别对应32位 CPU 和64位 CPU 的真机情况,对于模拟器,i386和 x86_64分别对应32位模拟器 和 64位设备模拟器
另一种方式是对自定义的符号进行条件编译,比如我们需要使用同一个 target 完成同一个 app 的收费版和免费版两个版本,并且希望在点击某个按钮时收费版本执行功能,而免费版本弹出提示的话,可以使用类似下面的方法:
1 | func someButtonPressed(sender: AnyObject!) { |
在这里我们用 FREE_VERSION 这个编译符号来表示免费版本。
为了使之有效,在项目的编译选项中进行设置,
Build Settings -> Swift Compiler - Custom Flags, 并在其中的 Other Swift Flags 加上 -D FREE_VERSION 就可以了。
编译标记
在 Objective-C 中,#param 这个符号 // MARK: 这样标记。
1 | // MARK: - |
Swift 另外几种标记:
// TODO: 和 // FIXME: 和 MARK 不同的是, 另外两个标记在导航栏中不仅会显示后面跟着的名字或者说明,而且它们本身也会被显示出来,用来提示还未完成的工作或者需要修正的地方。这样在阅读源代码时首先看一看导航栏中的标记
以前 OC 中的 #warning 黄色警告。Swift 暂时没有类似的标记
@UIApplicationMain
C 系统程序入口都是 main 函数
1 | int main(int argc, char * argv[]) |
调用了 UIKit 的 UIApplicationMain 方法,这个方法将根据第三个参数初始化一个 UIApplication 或其子类的对象并开始接收事件。传入 nil 意味着使用默认的 UIApplication
它用来接收类似 didFinishLaunching 或者 didEnterBackground 这样的与应用生命周期相关的委托方法。虽然这个方法标明返回一个 int,但是不会真正返回,知道用户或者系统将其强制终止。
新建一个 Swift 的 iOS app 项目,只发现一个 @UIApplicationMain 的标签
这个标签做的事情就是将被标注的类作为委托,创建一个 UIApplication 并启动整个程序。在编译的时候,编译器将寻找这个标记的类,自动插入像 main 函数这样的模板代码
一般情况下不需要做任何修改,但是当我们如果想要使用 UIApplication 的子类而不是它本身的话,我们就需要对这部分内容“做点手脚”了。
Swift 的 app 也需要 main 函数,如果我们删除@UIApplication 后,添加一个 main.swift 文件:
1 | UIApplicationMain(Process.argc, Process.unsafeArgv, nil, NSStringFromClass(AppDelegate)) |
可以通过将第三个参数换成自己的 UIApplication 子类,可以轻易地做一些控制整个应用行为的事情:
1 | import UIKit |
这样每次发送事件(比如点击按钮),就可以监听到这个事件了。
@objc 和 dynamic
在最初的版本中,Swift 不得不考虑与 OC 的兼容
Apple 采取的做法是允许我们在同一个项目中使用 Swift 和 OC 来进行开发,一个项目中的 OC 文件和 Swift 文件是处于两个不同的世界中的,为了能让它们相互联通,我们需要添加一些桥梁。
通过添加{product-module-name}-Bridging-Header.h 文件,填写想要使用的头文件名称,就可以在 Swift 中使用 Objective-C 代码了。Xcode 为了简化操作,在 Swift 项目中第一次导入 Objective-C 文件时会主动弹框进行询问,非常方便。
OC 和 Swift 底层使用的是两套完全不同的机制,Cocoa 中的 OC 对象是基于运行时的,它从骨子里遵循 KVC 以及动态派发(Dynamic Dispatch),Swift 为了追求性能,如果没有特殊需要的话,不会再运行时决定这些。就是说 Swift 类型的成员或者方法在编译时就已经决定,运行时不需要经过一次查找,可以直接使用。