类型
Swift 一个重要的概念是类型。类型是 Swift 中实现面向对象编程(object-oriented programming)、面向协议编程(protocol-oriented programming)以及泛型编程(generic programming)的基础。
在 Swift 中,所有变量的类型要么属于:
class
struct
enum
protocol
(一定条件下)
要么属于:
tuple
- 函数
我们分别介绍上面的几种类型,在其中会讨论它们之间的联系与区别。
类 class
¶
Swift 中的 class
与 C++ 类似。
1 2 3 4 5 6 7 8 9 10 11 12 |
|
成员函数、成员变量及其访问控制¶
Swift 中成员函数和成员变量的概念与 C++ 基本一致,一个类对象拥有独立的成员变量,成员函数可以访问所有的成员变量。
class
的访问控制通过在成员声明前加上修饰符实现,包括:
public
:默认,外界可访问,可写private
:外界不可访问private(set)
(成员变量):外界可访问,不可写
在类中通过 self
引用自身,如果不会产生歧义,可以省略 self
。
在类中时常会用到一种特殊的变量:计算变量。计算变量相当于一个没有参数的函数,每次获取其值的时候都要通过调用这个函数来计算:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
计算变量是只读的,而且不会占用储存空间。如果计算的时间复杂度超过 O(1),那么应该使用普通的函数,而不是计算变量。
构造函数和析构函数¶
构造函数使用关键字 init
声明。由于 Swift 没有零值初始化,所有成员变量都要有初值,要么直接在变量后显式指出,要么在构造函数中设定。构造函数结束时,所有成员变量都要有初值,否则产生编译错误。
1 2 3 4 5 6 7 8 9 |
|
如果类的每个成员变量都有默认值,那么可以使用默认构造函数:
1 2 3 4 5 |
|
析构函数使用关键字 deinit
声明,不过在一般的开发中很少用到:
1 2 3 |
|
静态变量、静态方法¶
使用 static
定义静态变量和方法:
1 2 3 4 5 6 7 |
|
类是引用类型¶
Swift 中的类是引用类型,传递引用而非副本,相当于指针。比如:
1 2 3 4 5 6 7 8 9 |
|
Swift 中对类的实例的管理使用类似于智能指针的方法,当引用计数为 0 时,实例被销毁。另外,可以在 var
前加上 weak
声明不参与计数的 weak 变量。
继承¶
Swift 支持类的继承,但不支持多重继承。与 C++ 不同,Swift 的继承没有 private
、protected
和 public
之分,所有继承都相当于 C++ 中的 public
。
另外,Swift 中所有的函数都相当于 C++ 中的虚函数,在派生类中使用 override
即可重写基类的方法;使用 super
调用基类的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
使用 as
向下进行类型转换,使用 as?
向上进行动态类型转换,使用 as!
进行强制类型转换:
1 2 3 4 5 6 |
|
结构体 struct
¶
与 C++ 不同,Swift 中的 struct
和 class
有共同之处,但并不能等同。
struct
与 class
类似,可以定义成员变量、成员函数、静态成员、构造函数,访问控制与类相同。
它们之间的不同点有:
struct
是值类型。也就是说,传递的是原有实例的副本,而非引用struct
没有继承struct
不能定义析构函数
与 class
进行对照
使用上面的例子:
1 2 3 4 5 6 7 8 9 |
|
struct
由于没有继承,因此对成员的构造函数(英语:Memberwise initializer)能够自动生成,大多数情况下无需手动写构造函数:
1 2 3 4 5 6 |
|
另外,struct
的成员函数默认不可更改成员变量,如果需要更改,则需要在函数前加上 mutating
:
1 2 3 4 5 6 |
|
关于 struct
和 class
的使用
看上去 struct
比 class
功能弱了不少,但 struct
通过遵循协议(英语:protocol,在下面介绍)可以实现继承的效果,同时由于传递的不是引用,因此大大降低了意料之外的对某个实例更改的概率。而且,struct
采用 copy-on-write 的策略,只有在更改时才会真正进行复制,因此性能上并没有太多的损失。
实际上,现代的 Swift 框架,如 SwiftUI、Combine,已经几乎全部使用 struct
进行面向对象/面向协议的编程。官方的建议是:除非只能用 class
,比如要在多个地方引用同一个实例,否则优先使用 struct
。
枚举 enum
¶
Swift 的 enum
提供了一种类型安全的枚举方法。
1 2 3 4 5 |
|
enum
可以使用 switch
语句来匹配:
1 2 3 4 5 6 7 8 9 10 11 |
|
或者用 ==
判断:
1 2 3 |
|
类型省略写法
上面 directionToHead
的类型已经明确,因此可以将 CompassPoint.south
缩写为 .south
。这种写法对于 class
和 struct
的静态成员和方法同样适用。如:
1 2 3 4 5 |
|
迭代¶
可以在 enum
后面加上 CaseIterable
使得可以按照声明顺序枚举所有的类型:
1 2 3 4 5 6 7 |
|
原始值¶
enum
可以有一个原始值(raw value)类型,可以是 Int
,String
等等,可以用来初始化 enum
类型。如果不指定值,编译期会自动设置对应的值,如 Int
是从 0 开始赋值并依次递增,而 String
对应各个 case
的名称。
1 2 3 4 5 6 7 8 9 |
|
关联值¶
enum
的每个 case 可以储存一个或多个关联值(associated value)。需要注意,一旦储存了关联值,就不能够指定 raw value,也不能够通过 ==
判断是否相等:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
使用关联值处理互斥情况
enum
关联值的一个重要的作用是处理若干互斥情况。
比如,Optional
类型实际上就是一个带有 associated value 的 enum
(泛型稍后介绍):
1 2 3 4 |
|
再如,包装结果和错误的 Result
:
1 2 3 4 |
|
一个请求要么成功(此时返回有效的结果),要么失败(此时返回错误)。使用 enum
就可以在不使用 Optional
的情况下以类型安全的方法返回一个唯一的结果:
1 2 3 4 5 6 |
|
函数、构造函数和变量¶
enum
是 Swift 的一等类型,它可以有成员函数、构造函数、计算变量、静态函数、静态变量,但不能储存值,即不能拥有成员变量。
能够给 enum
添加函数等给编程带来了极大的便利:
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 |
|
另外,一般使用 enum
加上静态变量来限定命名空间:
1 2 3 4 5 6 |
|
协议 protocol
¶
Swift 中的 protocol
(协议)与 C++ 的抽象类概念对应,其作用是定义一组需要实现的接口。比如,Swift 中的 Equatable
,其作用是一个判等器:
1 2 3 |
|
满足协议要求的类型称为遵循(conform to)此协议。如果一个类型想要遵循 Equatable
,需要:
- 在类型名称后面加上
Equatable
- 实现
==
方法
比如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
在这里,Array
本身并不知道如何判断它其中是否有元素与提供的 Rectangle
相等,但由于 Rectangle
遵循了 Equatable
协议,使得 Array
可以调用 Equatable
规定的判等接口来实现判断。
Note
值得注意的是,这种接口的规定是严格的。Array
的 contains
方法当且仅当其元素类型 Element
为 Equatable
时才存在:
1 2 3 |
|
有关泛型和 extension
,我们在后面介绍。
接口的规定方法¶
protocol
可以规定几类接口:
- 成员函数
- 成员变量
- 静态函数
- 静态变量
- 构造函数
1 2 3 4 5 6 7 8 9 |
|
其中,规定变量的接口时,只能用 var
,而且需要在类型名后加上 { get }
或 { get set }
以指定需要满足的最小权限要求。比如,某个类型遵循 SomeProtocol
时,它的 foo
变量需要可读写,而 bar
变量至少可以读,但不能限制其是否可写。
协议作为类型¶
当没有 associated type(稍后介绍)时,协议可以作为类型,并且其真正的类型可以改变,如 Swift 中内置的 Encodable
:
1 2 |
|
协议方法的默认实现¶
在规定了协议的接口后,可以在 extension
中对除了 init
以外的接口进行默认实现,但不能在 extension
中实现储存变量:
1 2 3 4 5 6 7 8 |
|
这样,当某个类型遵循 SomeProtocol
时,可以不用实现这些已经进行默认实现的接口。
协议的组合与继承¶
一个类型可以遵循多个协议,需要实现所有协议规定的接口:
1 2 3 4 5 6 7 8 9 10 11 |
|
一个协议可以继承多个协议,其作用为添加这些协议的规定的接口作为自身的限制条件,需要实现所有的接口:
1 2 3 4 5 6 7 8 9 10 11 |
|
Potocol-oriented Programming
通过遵循协议、对协议进行默认的实现、协议的继承,使得 struct
和 enum
这些没有继承功能的类型可以因此实现类似于继承的效果。
现代 Swift 框架大量使用 protocol
进行面向协议的编程(potocol-oriented programming)。其思想类似于 OOP,但在某些问题的处理上比类继承要更加简洁,且不易出错。
extension¶
class
、struct
、enum
和 protocol
可以通过 extension
添加方法(计算变量或函数),包括所有的内置类型:
1 2 3 4 5 6 7 |
|
使用 extension
可以以面向对象的方式给任何类型添加方法。
另外,一般为了区分不同的 protocol
的接口,在遵循不同的协议时,往往将各个协议规定的接口实现在不同的 extension
中:
1 2 3 4 5 6 7 8 9 |
|
元组¶
元组(tuple)用于传递复合值,用括号组织,可以带标签,用标签或者成员(而非下标)0、1、2 等取出其中的值:
1 2 3 4 5 6 7 8 |
|
元组的元素数量是固定的,不存在越界的情况。
函数¶
Swift 中,函数也是一种类型,可以储存在变量中,也可以作为参数/返回值参与另一个函数。函数类型的声明为 (参数列表) -> 返回类型
:
1 2 |
|
使用大括号来生成匿名函数(闭包,closure),在括号内使用 参数列表 -> 返回类型 in
给传入的各个参数命名:
1 2 3 |
|
调用方法与函数相同:
1 |
|
上下文明确时,可以省略括号内的 -> Int
:
1 2 3 |
|
另外,可以省略参数列表,用 $0
、$1
等指代第一、第二个元素:
1 2 3 4 5 |
|
Note
闭包的一个典型应用场景是作为参数传入函数中作为回调函数。如,进行网络请求,当请求完成时,调用传入的回调函数来进行结果的处理:
1 2 3 4 5 6 7 |
|
上面的例子使用了一种叫做 trailing closures 的语法,使得回调函数的函数体可以写在函数的“外面”,详见 Swift 官方教程。