0%

在家目录创建全局忽略文件:

1
$ touch .gitignore_global

之后打开:

1
$ vim .gitignore_global

写入要全局忽略的文件:

1
2
3
.DS_Store
*/.DS_Store
xcuserdata/

之后打开家目录的 .gitconfig 文件,这是 Git 的全局配置文件,在里面填入:

1
2
[core] 
excludesfile = ~/.gitignore_global

当然这一步我们也可以直接在命令行中操作:

1
$ git config --global core.excludesfile ~/.gitignore_global

之后,所有的 Git 项目都会忽略掉 .DS_Store 文件了。当然,全局忽略其它的文件也是一样的方式。

很多人有这样的需求:不仅有 GitHub,还会在 BitBucket 上管理私有的代码库,同时可能因为访问国外服务器速度慢,还会在码云、Coding 上有自己的代码库。HTTPS 毕竟不如 SSH 来的方便和安全,于是我们就有了使用多对 SSH 密钥的需求。

生成密钥

先查看家目录是否有一个 .ssh 的目录,如果没有说明还没有任何 SSH 密钥,我们先输入如下命令生成密钥。

1
2
3
$ ssh-keygen -t rsa -C "qiweipeng@hotmail.com"
Generating public/private rsa key pair.
Enter file in which to save the key (/Users/qiweipeng/.ssh/id_rsa):

这里让我们输入密钥保存的位置,如果直接回车,默认的保存位置就是家目录中的 .ssh 目录,默认名字是 id_rsa,由于我们需要为不同的网站设置不同的,所以我们自定义文件名 id_rsa_github,之后的密码可以不设置,我们两次回车:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Enter file in which to save the key (/Users/qiweipeng/.ssh/id_rsa):/Users/qiweipeng/.ssh/id_rsa_github
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in id_rsa_github.
Your public key has been saved in id_rsa_github.pub.
The key fingerprint is:
SHA256:SRqk4YpV6A2tWc1JZDLn5jJVycjYSpffl+0Y55v+meM qiweipeng@hotmail.com
The key's randomart image is:
+---[RSA 2048]----+
| o=O*=.. |
| oo=XX.o |
| ..Boo=... o |
| o+.o+ +... + o |
|. . o o S . * |
| o . o |
| o |
| o.o|
| .oEo|
+----[SHA256]-----+

此时一对 SHH 密钥已经生成好了,他们分别是 id_rsa_githubid_rsa_github.pub,对应着私钥和公钥,位置就在我们指定的 .ssh 目录下。

之后,我们以同样方法再创建一对密钥,命名为 id_rsa_bitbucketid_rsa_bitbucket.pub,这样,我们第一步生成密钥的工作就做完了。

上传公钥

以 GitHub 为例,首先查看公钥然后复制:

1
$ cat ~/.ssh/id_rsa_github.pub

登陆 GitHub,在 Settings - SSH and CPG keys 的位置点击 New SSH key,然后粘贴公钥。

完成配置

此时我们仅仅创建了两对密钥,因此 .ssh 文件夹中也只有这两对密钥;我们在这个文件夹创建一个 config 文件:

1
$ touch config

之后打开填入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
# github
Host github.com
HostName github.com
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa_github
User qiweipeng

# bitbucket
Host bitbucket.org
HostName bitbucket.org
PreferredAuthentications publickey
IdentityFile ~/.ssh/id_rsa_bitbucket
User qiweipeng

这里面重要的一个是 HostName,一定要填写正确,一个是 IdentityFile 就填写对应的私钥。

如果是码云,HostName 就是 gitee.com;如果是 Coding 呢 HostName 就是 git.coding.net

测试

同样以 GitHub 为例

1
2
3
4
5
6
$ ssh -T git@github.com
The authenticity of host 'github.com (13.229.188.59)' can't be established.
RSA key fingerprint is SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'github.com,13.229.188.59' (RSA) to the list of known hosts.
Hi qiweipeng! You've successfully authenticated, but GitHub does not provide shell access.

第一次链接需要确认一下,它给了一个 RSA key fingerprint,这是为了安全考虑,让你确认一下这个指纹是否是来自 GitHub 服务器的,我们可以对照这里查看,如果没问题就 yes,之后可以看到已经成功连接啦!

此时我们查看 .ssh 目录,可以看到,多了一个 known_hosts 文件,这是一个信任列表,又上面的命令也能看到 Permanently added 'github.com,13.229.188.59' (RSA) to the list of known hosts.

类似地,BitBucket、码云、Coding 的命令分别是 git@bitbucket.orggit@gitee.comgit@git.coding.net,不出意外,是可以测试成功的。

到此为止,我们就可以使用 Git 进行多账号操作了。

本文基本涵盖 Swift 中关于构造器的所有基础知识点,错误难免,希望各位指正。

基本使用

“Initialization is the process of preparing an instance of a class, structure, or enumeration for use. This process involves setting an initial value for each stored property on that instance and performing any other setup or initialization that is required before the new instance is ready for use.”

Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4.1).” iBooks.

这是官方的定义,从中可以看到:

  • 构造器可以使用的场景不仅仅是类,还可以是结构体和枚举(我们知道 Swift 中的结构体和枚举的强大已经不是 Objective-C 可以相比的了);
  • 实例在构造完成之前是无法使用的;
  • 构造过程主要是包括存储型属性的赋初值的过程,另外还包括一些其他必须的设置和初始化工作;

为了阅读的通畅,我们先展示类、结构体、枚举的构造过程,这样可以有一个初步的认识。之后再进一步深入。

类的构造过程

1
2
3
4
5
6
7
8
9
10
11
class Person {
var name: String
let id: String

init(name: String, id: String) {
self.name = name
self.id = id
}
}

let roger = Person(name: "Roger", id: "123456")

这里定义一个 Person 类,拥有两个属性,name(因为名字可以更改,所以是 var,又因为所有人都要有名字,所以是非可选型)和 id(ID 一般是无法更改的,所以是 let)。Swift 中的构造器没有返回值,在其中我们完成了两个属性的初始化。

属性是可以赋默认值的,没有赋默认值的属性必须在构造器中完成初始化;赋默认值的属性如果没有在构造器中完成初始化,构造过程会用其默认值为其初始化。

上面代码我们暂且让 id 可更改,我们为其设置一个默认值 "000000"

1
2
3
4
5
6
7
8
9
10
11
class Person {
var name: String
var id: String = "000000"

init(name: String) {
self.name = name
}
}

let roger = Person(name: "Roger")
roger.id // "000000"

可以看到,构造过程中我们只初始化了一个属性,有默认值的属性如果在这个过程中没有被初始化,那么会自动通过默认值为其初始化。

类只有一种情况下会提供默认的构造器,就是这个类的所有属性均存在默认值,并且还没有自定义构造器的情况,这个默认的构造器就是无参数的构造器。

1
2
3
4
5
6
7
8
class Person {
var name: String = "Guest"
var id: String = "000000"
}

let roger = Person()
roger.name // "Guest"
roger.id // "000000"

我们把两个属性均设置了默认值,那么即使我们没有提供构造器,也是可以通过 Person() 构造出一个实例,这个实例的所有属性全是默认值。

结构体的构造过程

1
2
3
4
5
6
struct Location {
let latitude: Double
let longitude: Double
}

let myLocation = Location(latitude: 39.1, longitude: 115.2)

可以看到,结构体中我们没有设置构造器,但还是使用了一个全参数的构造器完成了构造过程。

如果没有自定义构造器,结构体会默认提供一个包含全部属性作为参数的构造器;如果结构体的所有属性均设置了默认值,那么还会额外提供一个无参数的构造器。

1
2
3
4
5
6
7
struct Location {
var latitude: Double = 1.11
var longitude: Double = 2.22
}

var myLocation = Location()
var myLocation2 = Location(latitude: 39.1, longitude: 115.2)

可以看到,我们通过无参数或者是全参数的构造器,都构造出了一个实例。

需要说明的是,即使部分属性已经设置了默认值,那么系统默认提供的还是一个全参数的构造器。

枚举的构造过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
enum Rating {
case perfect
case great
case good
case bad

init?(score: Int) {
switch score {
case 90...100:
self = .perfect
case 80..<90:
self = .great
case 60..<80:
self = .good
case 0..<60:
self = .bad
default:
return nil
}
}
}

let rating1 = Rating.perfect
let rating2 = Rating(score: 98)

通常情况下,枚举变量可以通过 类型名.case名 的形式构造,这也是绝大多数的使用方式,不过 Swift 中我们仍然可以为枚举添加构造器。上面的例子中,我们通过得分来构造一个表示等级的枚举变量。最终两种构造方式都可以构造出一个枚举变量/常量。

需要说明的是,这个构造器是个可失败的构造器(它的语法就是在 init 后面增加一个问号),也就是允许构造失败(当传入的分数不在 0 到 100 的范围内的时候就构造失败),因此 rating1 的类型是 Ratingrating2 的类型是 Rating?,即它是一个可选型,使用时需要解包。

进一步理解

自定义构造器

显然我们无法只使用系统提供的构造器,况且很多情况下系统并不为我们提供构造器,这就需要我们自定义构造器。

上述举例中其实已经涉及了自定义构造器,不过都是最基本的直接为属性赋值的方式,下面的例子可以看出,构造过程也可能存在更多的逻辑。

1
2
3
4
5
6
7
8
9
10
class Person {
var name: String

init(firstName: String, lastName: String) {
name = firstName.capitalized + " " + lastName.capitalized
}
}

let roger = Person(firstName: "roger", lastName: "federer")
roger.name // "Roger Federer"

这个例子中我们把传入的两个参数值通过一定的逻辑后赋值给 name 属性。

一旦自定义了构造器,那么系统将不再提供任何默认的构造器,即使属性都存在默认值。类和结构体都是如此。

需要说明的一点是,这个例子中,构造器中使用的是 name 而非前面的 self.name,这是完全没问题的,前面的例子是因为参数变量的名字和属性名同名,那么在函数作用域内部,局部变量就占用了这个名字了。

构造器参数的默认值

在 Swift 中,构造器和普通函数一样,参数是可以提供默认值的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
var name: String
var age: Int

init(name: String = "Guest", age: Int) {
self.name = name
self.age = age
}
}

let guest = Person(age: 50)
let roger = Person(name: "Roger", age: 36)

guest.name // "Guest"

如果参数设置了默认值,那么系统会额外奉送一个构造器,那就是省略了所有默认参数的版本,上面的例子中,我们仅仅通过 age 作为参数也构造出了一个实例。

可失败的构造器

上面例子中,枚举的例子就是一个可失败的构造器,也就是允许返回 nil,响应的返回类型也是一个可选型。其语法很简单,就是 init 后面增加问号。

在 Swift 标准库中,可失败的构造器有很多

1
2
3
4
let num1 = Int("123")
num1 // 123 类型为 Int?
let num2 = Int("abc")
num2 // nil 类型为 Int?

这是我们很常见的把字符串转为整型的方式,Swift 是通过 Int 的一个可失败的构造器完成的,因为很显然,不是所有的字符串都可以转成整型的。

1
2
3
4
5
6
7
8
9
enum Direction: Int {
case east
case south
case west
case north
}

let d1 = Direction(rawValue: 0) // east
let d2 = Direction(rawValue: 4) // 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
class Person {
var name: String

init?(name: String) {

if name.count > 4 {
return nil
}
self.name = name
}
}

class Student: Person {

var studentNumber: String

// 这个构造器如果不加 ? 就会报错 A non-failable initializer cannot chain to failable initializer 'init(name:)' written with 'init?'
init?(name: String, studentNumber: String) {
self.studentNumber = studentNumber
super.init(name: name)
}
}

let student = Student(name: "小泉纯一郎", studentNumber: 123456) // nil

这个例子中,我们假定 name 只能是中国人名,我们简单的限制字符数只能不大于 4,否则构造失败,那么 Student 类的构造器由于调用了父类的可失败的构造器,那么这个构造器就也要是一个可失败的构造器才行。

还有一种解决方式是处理最终构造出来的对象比如强制解包(super.init(name: name)!),但这显然是不推荐的。

属性是可选型的情况下的构造过程

很多人一开始学习 Swift 时不理解为什么可选型属性可以不用初始化,只好死记硬背,其实这很好理解,只需要记住一句话:

可选型属性即使只声明,其也有默认值,它的默认值是 nil

也就是说,可选型属性即使只声明,其也具有默认值,这样就解释了为什么其“不用初始化”,实质上它和其他拥有默认值的属性一样,如果没有在构造器中初始化,那么构造过程就会默认用 nil 将其初始化。可选型本质上是枚举,nil 只是它的一个值而已。

1
2
3
4
5
6
7
8
9
10
11
class Person {
var name: String
var country: String?

init(name: String) {
self.name = name
}
}

let roger = Person(name: "Roger")
roger.country // 构造器没有为其初始化,那么它就初始化成默认值 nil

类的两段式构造

基本介绍

类因为有继承的特性,构造器不仅仅要完成本类属性初始化这么简单,它还要考虑父类继承来的属性如何初始化,以及其他逻辑的实现,于是就有了两段式构造的特征。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
var name: String
var id: String = "000000"

init(name: String) {
self.name = name
}
}

class Student: Person {
var studentNumber: String

init(name: String, id: String, studentNumber: String) {
// 第一段构造,先进行本类的属性的初始化,再通过调用父类构造器完成继承自父类的属性的初始化
self.studentNumber = studentNumber
super.init(name: name)

// 第二段构造,为父类的其他提供默认值的属性初始化;完成其他逻辑,此时已经可以使用 self 调用实例方法或属性
self.id = id
}
}

let harry = Student(name: "Harry", id: "654321", studentNumber: "123456")

这个例子中,子类的构造器里我们做了三件事:

  • 为本类的属性初始化
  • 通过调用父类构造器完成从父类继承的属性的初始化
  • 为父类中已经提供默认值的属性初始化(当然这一步还可以做更多其他逻辑,可以调用实例方法)

“Notice that the initializer for the EquilateralTriangle class has three different steps:

  1. Setting the value of properties that the subclass declares.
  2. Calling the superclass’s initializer.
  3. Changing the value of properties defined by the superclass. Any additional setup work that uses methods, getters, or setters can also be done at this point.”

Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4.1).” iBooks.

其中前两步就是第一段的构造过程,这一段实质上已经完成了实例的构造,第一段构造构造完成,这个实例就已经可以使用了。

那么为什么还要第二段构造呢?我们可以看上面的例子,这个例子中,父类继承的属性 id 如何在子类中初始化呢?首先我们要知道父类继承来的属性必须通过调用父类构造器完成初始化,可是此时父类提供的构造器是把 id 属性按照其默认值进行初始化的,也就是说,在子类中,我们最初构造出的这个实例,其 id 属性只能是它的默认值 000000,那么我们在子类还希望按照自己的意愿为其赋初值啊?那么就应该在第二段构造过程中进行,这一段构造过程本质上其实并不再是赋初值的过程,而是更改值的过程(之前写短文讨论过属性观察器的触发时机,第二段构造中属性的赋值已经可以触发属性观察器了)。

额外说明一个小地方,这里 super 调用父类构造器是必须进行的吗?答案是是的!但还是有可以省略的情况的(省略不写不代表没有调用),但这既少见也不建议,比如下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person {
var name: String = "Guest"
}

class Student: Person {
var studentNumber: String

init(studentNumber: String) {
self.studentNumber = studentNumber
}
}

let harry = Student(studentNumber: "123456")
harry.name // "Guest"

Swift 中类的构造过程与 Objective-C 的对比

很多程序员从 Objective-C 转到 Swift 后,都会有一个疑惑。

为什么 Objective-C 中 都是先调用 super,再为属性赋值,而 Swift 中却是颠倒,必须先为属性赋值,才能调用 super 呢?

阅读了前文相信不少人已经有了答案。我们先来看看 Objective-C 一般是怎么做的:

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
46
47
// QWPPerson 类

@interface QWPPerson : NSObject

@property (nonatomic, copy) NSString *name;

- (instancetype)initWithName:(NSString *)name;

@end

@implementation QWPPerson

- (instancetype)initWithName:(NSString *)name {
self = [super init];

if (self) {
_name = name;
}

return self;
}

@end

// QWPStudent 类

@interface QWPStudent : QWPPerson

@property (nonatomic, assign) NSInteger studentNumber;

- (instancetype)initWithName:(NSString *)name andStudentNumber:(NSInteger)studentNumber;

@end

@implementation QWPStudent

- (instancetype)initWithName:(NSString *)name andStudentNumber:(NSInteger)studentNumber {
self = [super initWithName:name];

if (self) {
_studentNumber = studentNumber;
}

return self;
}

@end

通常我们都是这样写的,很多文章里的解释是,我们先调用 super 完成父类的初始化,然后再进行本类的初始化。

我们对比一下 Swift,发现什么了么?其实很简单,这里所谓的“父类的初始化”,就是 Swift 中的第一段的构造过程,只不过 Objective-C 中所有属性都有默认值,那就是 0(nil 也是 0)。所以第一阶段构造实例的过程完全是把所有属性按照默认值去初始化构造出了这个实例,而所谓的“本类的初始化”就很像 Swift 中的第二段构造过程,这里实例已经创建(如果 if 判断成立的话),之后在里面其实是修改属性的值而已,从 0 修改成一个非 0 的值。

也就是说 Swift 的构造过程相对更灵活,它可以不必一开始只能用默认值去构造这个实例。所以 Swift 的构造过程并不是说把 super 交换到了后面,而是在 super 前面增加了更多的灵活性。

“Swift’s two-phase initialization process is similar to initialization in Objective-C. The main difference is that during phase 1, Objective-C assigns zero or null values (such as 0 or nil) to every property. Swift’s initialization flow is more flexible in that it lets you set custom initial values, and can cope with types for which 0 or nil is not a valid default value.”

Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4.1).” iBooks.

对于 Objective-C,我们可以模仿 Swift 尝试一种从没写过的方式:

1
2
3
4
5
6
7
8
9
- (instancetype)initWithName:(NSString *)name andStudentNumber:(NSInteger)studentNumber {
_studentNumber = studentNumber;
self = [super initWithName:name];

if (self) {
}

return self;
}

我们把 studentNumber 的属性初始化提前到 super 调用之前尝试一下,结果仍然可以成功创建实例。

小结

不管是默认提供的构造器,还是自定义的构造器,还是说类的两段式构造方式,其核心其实都在做一件事,那就是必须保证所有的属性都初始化!只有所有属性都初始化,这个实例才算构造完成,才可以使用。

值类型的构造器代理(Initializer Delegation)

“Initializers can call other initializers to perform part of an instance’s initialization. This process, known as initializer delegation, avoids duplicating code across multiple initializers.”

Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4.1).” iBooks.

名字很唬人,但其实就是一个构造器中可以调用另一个构造器。

这一节我们只讨论值类型的,也就是不考虑类,我们以结构体为例。

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
struct Size {
var width = 0.0
var height = 0.0
}

struct Point {
var x = 0.0
var y = 0.0
}

struct Rect {
var origin = Point()
var size = Size()

init(origin: Point, size: Size) {
self.origin = origin
self.size = size
}

init(center: Point, size: Size) {
let originX = center.x - size.width / 2
let originY = center.y - size.height / 2

// 这里我们调用了另一个构造器完成了构造过程
self.init(origin: Point(x: originX, y: originY), size: size)

// 当然这个例子我们也可以不通过调用其他构造器完成构造过程
// self.origin = Point(x: originX, y: originY)
// self.size = size
}
}

通过 self 我们可以在一个构造器中调用另一个构造器,这简化了一些重复代码。

相信很多了解 Swift 的同学会发现,这和类中的便利构造器很像啊。是的,便利构造器也是一种构造器代理,只不过由于结构体没有继承的特性,那么这里的构造器不用区分什么便利不便利,大家都一样,不用加 convenience 关键字。

下面我们就逐步开始讲解 Swift 构造过程中较难的地方了,我们就从便利构造器开始。

类的继承和构造过程

便利构造器和指定构造器

首先要确定,便利构造器和指定构造器的概念只存在于类中,值类型是不做区分的。
便利构造器也是构造器代理的一种情况。
类中的非便利构造器就是指定构造器。
便利构造器中仍然可以调用另一个便利构造器,这是没有问题的,但是最终都会去调用一个指定构造器。

我们只提便利构造器的话,它也很简单,和上面值类型的情况差不多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Person {
var name: String
var age: Int

init(name: String, age: Int) {
self.name = name
self.age = age
}

// 便利构造器必须增加 convenience 关键字
convenience init(firstName: String, lastName: String, age: Int) {
self.init(name: firstName + " " + lastName, age: age)
}
}

let roger = Person(name: "Roger Federer", age: 36)
let roger2 = Person(firstName: "Roger", lastName: "Federer", age: 36)

可以看到,和值类型不同的是,这里仅仅是多了一个 convenience 关键字,其他好像没什么区别,同样是增加了一个新的构造器。

那还为什么要加这个关键字呢?那是因为类是有继承的特性的,增加了这个维度,很多东西就复杂起来啦。

构造器代理规则在类中的表现

前面我们了解了构造器的代理,其实就是一个构造器可以调用另一个构造器嘛,但是在加入继承这一维度之前,它们都是在调用本类中的其他构造器而已。

再往前面的例子中,我们有继承的例子,那些例子中我们在子类的构造器中通过 super 关键字调用了父类的构造器,事实上这也是构造器代理。

那么在类中,构造器代理满足什么样的规则呢?

Rule 1
A designated initializer must call a designated initializer from its immediate superclass.
Rule 2
A convenience initializer must call another initializer from the same class.
Rule 3
A convenience initializer must ultimately call a designated initializer.”

Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4.1).” iBooks.

总结下来就是三点:

  • 指定构造器只能调用父类的指定构造器(注意无法调用父类的便利构造器哦)
  • 便利构造器只能调用本类中的其他构造器(便利构造器中无法使用 super 关键字而只能使用 self
  • 便利构造器最终一定会调用到一个指定的构造器(也就是说实例的构建本质上还是在指定构造器中构建的,便利构造器没有构造实例的能力,需要最终依靠指定构造器)

下面这张图形象地解释了这种关系:

图 1 - 构造器代理

这样又我们可以把上面的三点总结为两点:

  • 指定构造器只能纵向调用
  • 便利构造器只能横向调用
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
class Person {
var name: String
var age: Int

init(name: String, age: Int) {
self.name = name
self.age = age
}

// 便利构造器只能调用本类中的其他构造器
convenience init(firstName: String, lastName: String, age: Int) {
self.init(name: firstName + " " + lastName, age: age)
}
}

class Student: Person {
var studentNumber: String

init(name: String, age: Int, studentNumber: String) {
self.studentNumber = studentNumber
// 指定构造器只能调用父类的指定构造器
super.init(name: name, age: age)
}

// 便利构造器只能调用本类中的其他构造器
convenience init(name: String, age: Int, id: String) {
// 这里我们假定学生的号码就是 id 号码前面加 2018
self.init(name: name, age: age, studentNumber: "2018" + id)
}
}

这个例子中,就完全遵守这样的规则。

构造器的继承和重写

构造器的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person {
var name: String
var age: Int

init(name: String, age: Int) {
self.name = name
self.age = age
}

convenience init(firstName: String, lastName: String, age: Int) {
self.init(name: firstName + " " + lastName, age: age)
}
}

这里的父类我们仍然定义一个 Person 类,拥有一个指定构造器和一个便利构造器;接下来我们设置一个子类

1
2
3
class Student: Person {
var studentNumber: String = "000000"
}

为了测试构造器的继承,我们在这个子类中什么构造器都没写,只增加了一个具有默认值的属性;结果会怎么样呢?结果就是,我们可以成功使用前面的两个构造器构造出 Student 实例:

1
2
let harry = Student(name: "Harry", age: 11)
let hermione = Student(firstName: "Hermione", lastName: "Granger", age: 12)

是的,我们成功从父类继承了他的所有构造器,一个指定构造器,一个便利构造器。

我们总结一下上面代码有什么特征:

  • 子类的所有属性都提供了默认值
  • 子类没有定义任何指定构造器(当然也不会定义便利构造器)

带来的结果呢:

  • 子类继承了父类所有构造器,包括指定构造器和便利构造器

现在我们开始改变了,首先我们尝试去掉子类属性 studentNumber 的默认值;结果报错了!提示 Class 'Student' has no initializers,它告诉我们 Student 类没有构造器!刚才我们还继承了两个构造器,现在一下子一个也没有了。

我们发现子类因为含有没有赋默认值的属性,父类的所有构造器就不再供子类继承了。这是为什么呢?答案很简单,因为父类的构造器无法完成子类属性的初始化。父类并不知道子类有什么属性,我们假设此时可以使用父类的构造器完成构造过程,那么子类的这个 studentNumber 属性应该被初始化成什么呢?

于是我们只好自己自定义构造器了

1
2
3
4
init(name: String, age: Int, studentNumber: String) {
self.studentNumber = studentNumber
super.init(name: name, age: age)
}

我们创建一个构造器,然后尝试创建实例,通过代码提示可以看到,此时子类只有一个构造器,就是我们新创建的这个构造器。父类的构造器仍然没有被继承。

于是突然想到了父类方法是可以重写的,那么父类的构造器是否也能重写呢?答案是是的,我们此时可以重写父类的指定构造器。

1
2
3
4
5
// 前面加 override 关键字实现对父类构造器的重写
override init(name: String, age: Int) {
studentNumber = "000000"
super.init(name: name, age: age)
}

现在我们自己定义了一个,然后重写一个父类的,那么我们现在应该有两个了吧!我们尝试一下:

1
2
3
let harry = Student(name: "Harry", age: 11, studentNumber: "123456")
let hermione = Student(name: "Hermione", age: 12)
let ron = Student(firstName: "Ron", lastName: "Weasley", age: 11)

我们竟然拥有了三个构造器!原来父类的便利构造器现在又被继承了!原因是:

  • 如果子类提供了所有父类指定构造器的实现,那么子类将继承父类所有的便利构造器

这样,我们基本就了解了构造器的继承原则,它最终可以总结成下面两句话:

  • 当我们没有实现任何自定义构造器的情况下,子类继承所有父类指定构造器和便利构造器(当然这种情况一定是子类所有属性都有默认值的情况,不然会报错)
  • 当我们重写了所有父类指定构造器的情况下,子类额外继承父类的所有便利构造器

当然也可以参考官方的说法,大同小异:

Assuming that you provide default values for any new properties you introduce in a subclass, the following two rules apply:
Rule 1
If your subclass doesn’t define any designated initializers, it automatically inherits all of its superclass designated initializers.
Rule 2
If your subclass provides an implementation of all of its superclass designated initializers—either by inheriting them as per rule 1, or by providing a custom implementation as part of its definition—then it automatically inherits all of the superclass convenience initializers.”

Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4.1).” iBooks.

构造器的重写

我们刚才注意到了,构造器是可以重写的,但是我们只重写了指定构造器。那么就有疑问了,便利构造器是否可以重写呢?答案是否定的,便利构造器无法被重写。但是有的同学却是看到过 override convenience init 的字眼出现,这是怎么回事呢?

这是因为重写的父类指定构造器可以是便利构造器,但是便利构造器无法被重写!这里有些绕,我们拿官方的例子说明。

1
2
3
4
5
6
7
8
9
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "[Unnamed]")
}
}

它的结构如下图所示:

图 2 - 构造器重写1

1
2
3
4
5
6
7
8
9
10
class RecipeIngredient: Food {
var quantity: Int
init(name: String, quantity: Int) {
self.quantity = quantity
super.init(name: name)
}
override convenience init(name: String) {
self.init(name: name, quantity: 1)
}
}

我们此时又定义一个 RecipeIngredient 类,这个类中有一个自定义的构造器,它通过调用父类指定构造器完成构造;同时还有一个便利构造器,这个构造器通过调用本类的指定构造器完成构造;巧合的是,这个便利构造器和父类的那个指定构造器长的一模一样!于是没办法,它既是一个便利构造器,又完成了父类指定构造器的重写,所以又加上了 override 关键字。

目前这两个类的结构如下图所示:

图 3 - 构造器重写2

子类并不是重写了一个父类的便利构造器,而是通过便利构造器的方式完成了对父类指定构造器的重写。

于是我们又满足了上面的第二条原则,我们重写了所有父类的指定构造器,于是父类的所有便利构造器也被子类继承,现在子类拥有三个构造器。

required 构造器

构造器加上 required 关键字代表子类必须实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Person {
let name: String

// 父类指定构造器
required init(name: String) {
self.name = name
}

// 父类便利构造器
convenience init(firstName: String, lastName: String) {
self.init(name: firstName + " " + lastName)
}
}

class Student: Person {

var studentNumber: Int

// 对于 required 的构造器, 重载不需要再写 override, 而是还写 required
required init(name: String) {
studentNumber = 000
super.init(name: name)
}
}

对于 required 的构造器, 重载不需要再写 override, 而是还写 required。

willSet 和 didSet 在 Swift 中用于监控存储型属性的变化,被称为属性观察器(Property Observers)。其用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {

var name: String = "Guest" {

willSet {
print("Name: willSet")
}

didSet {
print("Name: didSet")
}
}

init(name: String) {
self.name = name
}
}

let roger = Person(name: "Roger")
roger.name = "Rafa" // 此时触发属性观察器

需要注意的是:

  • 属性观察器只可用于存储型属性,不能用在计算型属性;
  • 属性观察器不会监控属性初始化过程,即属性首次赋值的时候是不会被监控的,如上面代码的构造函数是不会被监控的,即使属性声明的时候已经初始化;
  • 但是不代表构造函数执行一定不会触发属性观察器,下面会举例。
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
class Person {

var name: String = "Guest" {

willSet {
print("Name: willSet")
}

didSet {
print("Name: didSet")
}
}
}

class Student: Person {

var studentNumber: Int

init(studentNumber: Int, name: String) {
self.studentNumber = studentNumber
super.init()

self.name = name
}
}

let andy = Student(studentNumber: 123456, name: "Andy") // 构造函数触发属性观察器

在这个例子中,Student 类的构造函数完成了三件事:

  1. 初始化自身属性;
  2. 调用父类构造函数,初始化父类属性并完成构造过程,此时构造过程已经完成;
  3. 为父类的属性按照子类的要求设置默认值,此时虽然是在构造函数中完成,但实质上已经在做构造过程之后的事情了,这时属性的修改就会触发属性观察器。
  1. Setting the value of properties that the subclass declares.
  2. Calling the superclass’s initializer.
  3. Changing the value of properties defined by the superclass. Any additional setup work that uses methods, getters, or setters can also be done at this point.”

Excerpt From: Apple Inc. “The Swift Programming Language (Swift 4.1).” iBooks.

int 类型占用 4 个字节,最小的那个数是 -2147483648,它的补码是 10000000 00000000 00000000 00000000,反码是 11111111 11111111 11111111 11111111,原码是 10000000 00000000 00000000 00000000,也就是原码和补码是一样的。

如果忽略符号位,那么 -2147483648 和 0 的二进制位是一样的。事实上 int 类型的二进制码从 32 个 0 到 32 个 1,分别对应的数是 0,1,2,……,2147483646,2147483647(对应 0 后面 31 个 1),-2147483648(对应 1 后面 31 个 0),-2147483647,……,-3,-2,-1(对应 32 个 1);也就是说如果符号位不同其他位都相同叫做“对应”的话:0 和 -2147483648 对应,1 和 -2147483647 对应,……,2147483647 和 -1 对应。

尝试打印 int 类型的正整数中的前 256 个数和后 256 个数,可能会想到如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>

/**
打印 int 类型的正整数中的前 256 个数和后 256 个数
*/
int main(int argc, const char * argv[])
{
for (int i = 0b00000000000000000000000000000000; i <= 0b00000000000000000000000011111111; i++)
{
printf("%d\t",i);
}

for (int j = 0b01111111111111111111111100000000; j <= 0b01111111111111111111111111111111; j++)
{
printf("%d\t",j);
}

return 0;
}

事实上第一个循环是没有问题的,但是第二个循环,当打印完最大一个数即 2147483637 后,循环没有停止,而是继续从负的最小一个数开始继续往上打印;原因是 0b01111111111111111111111111111111 之后的这个 j++ 自增运算得到的结果是小于 j 的,即直接跳到 -2147483638 去了;也就是 “2147483637 + 1 = -2147483638”。

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
28
29
30
31
32
var numbers: Array<Int> = [3, 5, 7, 9, 1, 3, 12]

numbers.count // 元素数量
numbers.isEmpty // 数组是否为空
numbers[2] // 数组下标元素
numbers.first // 首个元素(可选型)
numbers.last // 最后一个元素(可选型)
numbers.max() // 数组最大元素(可选型)
numbers.contains(3) // 是否包含某个元素
numbers.contains { (num) -> Bool in
num > 10
} // 是否包含大于 10 的数
numbers.contains { $0 > 10} // 上条代码的简写
numbers.index(of: 7) // 某个元素首次出现的下标(可选型)

numbers.append(2) // 末尾添加一个元素
numbers.append(contentsOf: [2, 5, 7]) // 末尾增添一个数组
numbers += [2, 5, 7] // 上句代码的简写
numbers.insert(10, at: 3) // 在某个位置插入元素

numbers.removeFirst() // 删除第一个元素
numbers.removeLast() // 删除最后一个元素
numbers.remove(at: 4) // 删除指定位置元素
numbers.removeAll() // 删除所有元素

numbers[4] = 11 // 修改指定位置元素
numbers[1...3] = [6, 9, 12, 55, 7] // 将指定范围元素替换

// 数组遍历
for number in numbers {
number
}

字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var dict: Dictionary<String, String> = ["swift": "燕子", "python": "蟒蛇"]

dict.count // 字典元素数量
dict.isEmpty // 字典是否为空
dict["swift"] = "雨燕" // 增、改 返回新值
dict.updateValue("大蟒", forKey: "python") // 改 返回旧值
dict["swift"] // 查 (可选型)
dict.removeValue(forKey: "java") //删
dict["java"] = nil // 删

Array(dict.keys) // 将字典所有键放进一个数组中
Array(dict.values) // 将字典的所有值放进一个数组

// 遍历字典的键值对
for (key, value) in dict {
key
value
}

集合

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
// 集合必须显式声明类型,并且没有快速创建的方法
var setA: Set<String> = ["A", "B"]
var setB: Set<String> = ["C", "D", "E"]
var setC: Set<String> = []

// 声明空集合
var emptySet1: Set<Int> = []
var emptySet2 = Set<Double>()
var emptySet3: Set<Int> = Set()

// 将数组转成集合
var vowels = Set(["A", "E", "E", "I"])

vowels.first // 集合中随意一个元素

// 集合的添加
setC.insert("A")
setC.insert("D")
setC.insert("E")
setC.insert("E") // 重复添加是没效果的
// 删除
setC.remove("E")
//skillsOfC.removeAll() // 删除所有

// 集合的交并等运算,事实上数组也能用这些方法
setA.union(setB) // 交 但 skillsOfA 本身没有发生改变
skillsOfA.formUnion(skillsOfB) // 交 skillsOfA 改变
setA.intersection(setB) // 并, 本身不改变
skillsOfA.formIntersection(skillsOfB) // 并,本身改变
setA.subtracting(setC) // A有C没有,本身不改变
skillsOfA.subtract(skillsOfC) // A有C没有,本身改变
setA.symmetricDifference(setC) // 亦或,即A和C 中除去共有剩下的部分, 本身不改变
setA.formSymmetricDifference(setC) // 亦或,本身改变
setC.isSubset(of: setA) // 判断子集
setC.isStrictSubset(of: setA) // 判断真子集
setA.isSuperset(of: setC) // 判断超集
setA.isStrictSuperset(of: setC) // 判断真超集
setA.isDisjoint(with: setC) // 判断相离,就是看是否有共同元素

最近尝试了 Carthage,这个工具有点在于侵入型小,它只是把 Github 的库拉下来打包成静态库,然后自己去关联这个 framework 就好啦。不像 CocoPods 还创建了 workspace。

安装 Carthage

  • Terminal 执行 /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" 安装 Homebrew;
  • brew update 更新 Homebrew 到最新版本;
  • brew install carthage 安装 Carthage;

使用 Carthage

carthage version 查看 Carthage 版本

使用 Carthage 安装第三方库流程

  • Terminal 更改至项目所在文件夹;
  • touch Cartfile 创建一个空的 Carthage 文件;
  • vim Cartfile 或者 open -a Xcode Cartfile 打开文件,输入如 github "Alamofire/Alamofire" ~> 3.0(其中版本号可选),保存关闭文件;
  • carthage update --platform iOS,Carthage 开始下载和编译第三方库,完成后项目所在文件夹中多了 Cartfile.resolved 文件 和 Carthage 文件夹;
    • Cartfile.resolved 文件确保提交的项目可以使用完全相同的配置与方式运行启用。 跟踪项目当前所用的依赖版本号,保持多端开发一致,出于这个原因,强烈建议提交这个文件到版本控制中;
    • Carthage 文件夹中有两个文件夹,分别是 Build 和 ;Checkouts
      • Build 文件夹是编译后的 framework 文件;
      • Checkouts 是拉取的依赖库源文件;
  • 打开项目,点击 project,选择 target, 再选择上方的 General,将需要的 framework 文件拖到 Embedded Binaries 中,此时 Linked frameworks and Binaries 内也会出现;

其他工具常用命令

既然写了这篇文章,就把之前总结的其他工具的常用命令也放上来吧。

Homebrew

常用到如 Carthage、Git、Ruby、SQLite、Vim 等都可以通过 Homebrew 安装。

  • 安装 /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

  • 查看安装列表 brew list

  • 更新自身 brew update

  • 更新全部安装的包 brew upgrade

  • 查看帮助 brew -help

Git

  • 查看版本 git --version

RubyGems

  • 查看版本 gem -v

  • 官网查看最新版本 https://rubygems.org/pages/download

  • 更新到最新 gem update --system

  • 查看本地安装列表 gem list

  • 更新所有 Gems 到最新版本 gem update

  • 显示帮助 gem --help

  • 查看所有命令 gem help commands

CocoPods

依赖 RubyGems:

  • 查看所有命令 ‘pod –help’
  • 为指定路径安装 CocoPods cd 指定路径 pod init
  • 设置好 Podfile 之后安装 pod init
  • 为项目卸载 CocoPods cd 指定路径 pod deintegrate

是的,这真的只是入门,一点点了解而已,真正的函数式编程还差得远呢。只是初步学习了一些入门的思想,争取为以后的开发中提供一些便利。

开始

首先记一个 Swift 函数中的变化,其实也不能说是变化,因为在 Swift 3 就已经是这样了,就是函数中的参数已经不能使用 var 了,也就是函数参数全都是常量,如果希望在函数内部改变,那么只有在函数内部重新声明一个变量去接收这个参数。

1
2
3
4
5
6
7
func test(number: Int) -> Int {

var newNumber = number

// 对 newNumber 进行操作
return newNumber
}

我们如果想直接操作实参呢?更加复杂,需要增加 inout 关键字。

1
2
3
4
5
6
7
func addOne(number: inout Int) {
number = number + 1
}

var num = 5
addOne(number: &num)
num // 6

函数式编程更希望函数像是一个黑匣子一样,参数传进去,黑匣子经过一定逻辑然后返回一个结果,而并不希望我们直接操作这个参数,因此 Swift 函数参数默认都是值传递。

那么我们希望交换两个数呢?这时候肯定还是需要使用 inout 的嘛,我们定义这样一个函数。

1
2
3
4
5
6
7
8
func swapTwoInts( _ a: inout Int,_ b: inout Int) {
(a, b) = (b, a)
}

var x = 3
var y = 5

swapTwoInts(&x, &y) // 调用函数的时候,含有 inout 关键字的变量前面会有一个 &,表示地址

当然事实上,Swift 给我们提供了 swap 函数,并且是支持泛型的,也就可以交换两个字符串,两个对象,两个很多东西。我们来自己实现一下:

1
2
3
4
5
6
7
8
func swapTwo<T>(_ a: inout T, _ b: inout T) {
(a, b) = (b, a)
}

var x = 3
var y = 4

swapTwo(&x, &y)

到这里还都是函数的基本使用,没有多少函数式编程的东西啊。

函数式编程入门

Swift 为什么说它支持函数式编程呢,其中一点就是它把函数列为了“一等公民”,函数可以作为变量,可以作为参数,可以作为返回值,这为函数式编程打下了基础。

函数作为变量/常量

1
2
3
4
5
6
7
8
func add(num1 a: Int, num2 b: Int) -> Int {
return a + b
}

let add2 = add // 这里直接将函数名赋值给一个常量

add(num1: 3, num2: 5) // 8
add2(3, 5) // 8

我们把一个函数名直接赋值给了一个常量,此时 add2 的类型为 (Int, Int) -> Int,也就是说这个变量是一个函数类型的,之后我们使用 add2(3, 5) 的方式依然可以调用这个函数(只不过没有了参数名)。

函数作为参数

我们都熟悉 Objective-C 中的 Block,而 Swift 中的 Closure 其实和 Block 是一回事儿,都是匿名函数。Swift 由于函数可以作为变量,我们原来传入闭包的地方现在传入一个函数变量是完全没问题的。

sort 函数

比如 Swift 中的 sort 函数(sorted() 有返回值,不改变自身,sort() 返回类型为 Void,改变自身;事实上 Swift 中很多类似方法都是这样的,加后缀的一般都是有返回值不改变自身的)。

1
2
3
4
5
6
7
8
9
var numbers = [4, 7, 2, 1, 7, 12, 3]

func bigFirst(a: Int, b: Int) -> Bool {
return a > b
}

numbers.sort(by: bigFirst) // [12, 7, 7, 4, 3, 2, 1]

// numbers.sort(by: >) // > 本质上就是一个函数,因此这样也是可以的

上面就是函数作为参数的一个例子,这里的 sort 函数称为高阶函数。

我们能传入函数的地方也能传一个闭包。

1
2
3
4
numbers.sort { (num1, num2) -> Bool in
return num1 > num2
}
numbers.sort { $0 > $1 } // 参数可以使用默认的$0,$1...,由于只有一行代码,return 也可以省略

当然,sort 函数能实现的功能可不知这些,比如下面,我们可以把一个整型数组按照字符串的顺序排序:

1
2
3
var numbers = [123, 110, 1, 23, 235, 20, 315]

numbers.sort { String($0) < String($1) } // [1, 110, 123, 20, 23, 235, 315]

下面介绍所有支持函数式编程的语言都基本会有的三个函数,那就是著名的 map, filter 和 reduce 函数。

map 函数

map 函数作用在数组上,我们传入一个函数,告诉这个数组每一个元素应该按照什么样的逻辑发生改变,那么 map 操作就会把数组中的所有元素均按照这个逻辑去改变,并返回一个新的数组。

1
2
numbers.map { $0 + 1 } // 数组所有元素均加 1
numbers.map { String($0) } // 将所有元素均转成字符串

filter 函数

filter 函数,我们传入一个返回值为 Bool 的函数,所有返回值为 true 的元素会被放进一个新的数组中。

1
numbers.filter { $0 > 5} // 将所有大于 5 的数挑选出来组成一个新的数组

reduce 函数

reduce 函数是把数组中所有的元组组成一个值,比如数组中所有元素的和,所有元素的积,都可以使用。

1
2
3
4
var numbers = [4, 7, 2, 1, 7, 12, 3]

numbers.reduce(0, +) // 所有元素的和
numbers.reduce("") { $0 + String($1) + " " } // 返回 "4 7 2 1 7 12 3 "这样一个字符串

柯里化

王巍的《Swifter - Swift 必备 Tips》第一篇就是讲的柯里化。

1
2
3
4
5
func addOne(_ num: Int) -> Int {
return num + 1
}

addOne(2)

上面的函数把一个整型加 1,可是如果我们还需要一个加 2 的函数呢,再写一个?如果需要加 3 的函数呢?再从头到尾写一个?

事实上我们可以定义一个高阶函数,把这几个函数的相同部分抽取出来,如下面:

1
2
3
4
5
6
7
8
9
func addTo(_ adder: Int) -> (Int) -> Int {
return { num in
return adder + num
}
}

let addFour = addTo(4)

addFour(5) // 9

柯里化其实就是把一个多参数的函数,通过抽取高阶函数,把参数的传入分层的过程。我们利用它可以做什么?如果我们有多个函数,它们拥有部分相同的逻辑,我们就可以考虑把这部分逻辑抽取出来,达到代码复用的目的。

函数作为返回值

这里使用慕课网中刘宇波老师的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 按照重量的1倍计算邮费
func tier1MailFeeByWeight(weight: Int) -> Int {
return 1 * weight
}

// 按照重量的3倍计算邮费
func tier2MainFeeByWeight(weight: Int) -> Int {
return 3 * weight
}

// 根据重量的不同,选择不同的邮费计算方式,当重量小于等于10的时候选择1倍计算邮费,否则选择3倍计算邮费
func chooseMailFeeCalculationByWeight(weight: Int) -> (Int) -> Int {
return weight <= 10 ? tier1MailFeeByWeight : tier2MainFeeByWeight
}

func feeByUnitPrice(price: Int, weight: Int) -> Int {

// 这里根据参数的不同,可以得到不同的函数作为返回值
let mailFeeByWeight = chooseMailFeeCalculationByWeight(weight: weight)
return mailFeeByWeight(weight) + price * weight

}

通常在其他语言中,nil 的本质就是 0,用 0 代表没有;但是 Swift 认为,0 应该是和其他整数一样的呀?程序员怎么知道 0 就是没有而不是 0 本身呢?于是,在 Swift 中出现了可选型的概念。在 Swift 中,nil 代表空,代表没有,nil 就是 nil,不再是一个整数 0 了。

可选型的基本使用

可选型写法很简单,普通类型后面加 ? 就可以了;通常一个变量如果不应该为空,一定要存在,那么就不应该设置为可选型,比如身份证号,每个人都一定有,那么就不要设置成可选型

1
let id: Int

如果这个变量可以为空,比如错误信息,错误信息可能有也可能没有,那么就适合设置成可选型

1
var errorMessage: String?

可选型的使用必须经过一个步骤就是解包(Unwrapp),可选型是无法直接使用的

最简单的解包方式就是强制解包,含义为程序员保证这个可选型变量/常量一定不会空;而如果其真的为空了就会崩溃,因此这是有风险的,程序员必须能保证才行。

1
2
3
4
5
6
var serverResponseCode: Int? = 404

// serverResponseCode = nil // 如果加上这句代码,强制解包失败就会崩溃
// 另外,只有可选型可以设置成 nil,非可选型是无法设置为 nil 的

let code = serverResponseC! // 此时 code 为 Int 类型

通常情况下,比如网络请求得到的数据,我们无法保证其一定不会 nil,此时就应该通过判断来非暴力的解包,我们通常能想到的应该是下面这样

1
2
3
if serverResponseCode != nil {
let code = serverResponseCode!
}

事实上,因为可选型解包是一个常用操作,Swift 为我们专门提供了简化语法,下面的语法和上面的完全等价

1
2
3
if let severResponseCode = serverResponseCode {
// 此时,大括号内的 severResponseCode 就变成了一个 Int 类型,可以直接使用
}

如果有多个可选型同时需要解包,那么它们可以并列写在同一个 if 语句里

1
2
3
4
5
6
var serverResponseCode: Int? = 404
var serverResponseMessage: String? = "not found"

if let severResponseCode = serverResponseCode, let serverResponseMessage = serverResponseMessage {
// 这个作用域内,解包后的变量可以直接使用
}

当然,可以使用 if 的一般都可以使用 guard,本文就不涉及 guard 的使用了。

可见可选型的基本使用并不难,可为空就设置成可选型,用的时候就需要解包,这样就可以了。那么,Swfit 为什么要给程序员添加这个麻烦呢?当然是为了安全啦,可以这么说,只要程序员合理使用可选型,原来在 Objective-C 中的野指针访问的问题基本就可以避免啦。Swift 只是强制程序员去主动思考了而已,原来是被动的,现在是把这个过程提前到代码编写阶段,不得不去考虑。

可选型的实质

可选型事实上是一个枚举,String? 实际上是 Optional<String>,它只是一个语法糖。下面两句代码是完全等价的

1
2
var optionalString: String? = "Hello"
var optionalString: Optional<String> = Optional.some("Hello")

而通常我们的解包

1
2
3
4
5
if let aString = optionalString {
aString // 解包
} else {
print("nil")
}

其实也可以看作下面的过程

1
2
3
4
5
6
switch optionalString {
case let .some(alString):
alString // 这里就是解包后的 String 类型变量
case .none:
print("none") // 这就是解包失败
}

我们可以尝试自己写一个这样的枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
enum MyOptional<T> {
case some(T)
case none
}

let optionaString = MyOptional.some("Hello") // 这里类型是通过类型推断得到的,当然也可以显式声明其类型 MyOptional<String>

switch optionaString {
case let .some(aString):
print(aString) // 这里就相当于解包成功咯
case .none:
print("none") // 这里就是解包失败,也就是 nil
}

Optional Chaining

我们知道可选型无法直接使用,那么我们就要先解包,然后使用,如果调用一个方法后返回类型还是可选型呢?那么我们再解包,再使用。那这样也太麻烦了吧?

所以呢,Swift 的这样一个语法就是解决这样的问题的,那就是 Optional Chaining,它的语法大约是下面这样的

1
2
let aString: String? = "Hello world"
let des = aString?.first?.description

一个可选型可以直接调用它对应的非可选型的方法,只不过返回值同样变成了一个可选型而已(即使这个方法的返回类型是一个非可选型),我们知道 description 的返回类型是 String 类型,可是此时 des 的类型是 String?

隐式可选型

在 UIKit 中,有很多类型是如 UIView! 这样的,比如 UIViewController 中的 view 属性,这就是隐式可选型。

隐式可选型是什么呢?其实也是一个可选型,也就是说 Int!Int? 都是可选型,都可以为 nil;那么区别在哪呢?区别很小,就是 Int! 可以省去解包这个步骤直接使用

1
2
let num: Int! = 3
let sum = num + 4

所以很简单,Int! 就是把强制解包的步骤给提前做啦,其他跟一般的可选型没啥区别;当然,这样肯定是有风险的,因为是强制解包嘛。所以程序员声明隐式可选型闭包保证一点,就是在真正使用它的时候,其必须不为 nil。

那隐式可选型有什么作用呢?目前来讲,其应用场景基本就是在定义类的时候,类的某些属性我们可以定义成隐式可选型。下面这个例子来自慕课网刘宇波老师的案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class City {

let cityName: String
unowned var country: Country

init(cityName: String, country: Country) {
self.cityName = cityName
self.country = country
}
}

class Country {

let countryName: String
// 这里如果不使用可选型,因为类的所有成员变量还没有被全部赋值,下面就无法在构造函数中使用 self,但是程序员又可以保证这个类的对象一旦创建,这个属性一定不为nil,所以这里使用了隐式可选型
var capitalCity: City!

init(countryName: String, capitalCity: String) {
self.countryName = countryName
self.capitalCity = City(cityName: capitalCity, country: self)
}
}

这个例子中, Country 类中的 capitalCity 属性被定义成了隐式可选型。

首先,为什么不能定义成一般的可选型呢?因为一个国家的首都都是确定存在的不可能为空啊,程序员其实不想把它定义成可选型,更不希望用户使用的时候还要解包。

那么就定义成非可选型呗?也不行,因为在构造函数里,我们在给 self.capitalCity 赋值的时候使用了 self,我们知道一个类的对象在构造完成前(因为 self.capitalCity 属性没有被赋值嘛)是不可以被使用的,那这怎么办呢?既然要用这个对象就要赋值,既然要赋值就要用这个对象,死循环了。

所以解决办法就是在赋值之前就让这个对象先构造完成,也就是把这个属性改成可选型,这样可选型可以不用初始化(默认为 nil),之后呢我们马上再给这个对象的这个属性赋上值,最终在用户使用这个对象的时候,这个属性可以保证不为 nil,同时用户也不用解包即可直接使用了。

学习 Swift 的过程中,发现这门新语言在设计上确实融入了很多思考,Swift 宣称相对 Objective-C 是一门更安全的语言,那么本文就从语法设计上,举几个小例子谈谈 Swift 为了安全都做了哪些努力。

Swift 中不允许溢出错误

在 Objective-C 中,如下代码会警告但是可以编译通过,打印结果为 -128,理解进制的就会知道,因为发生了溢出(int8_t 只能保存 -128 到 127 的整数),127 在二进制层面加 1 的结果就是 -128。

1
2
int8_t aInt8 = 128;
NSLog(@"%d", aInt8); // -128

溢出在 C 语言以及 Objective-C 中都是被允许的,但是潜在的 Bug 也会因此产生,大部分开发中我们其实更希望这种情况被及时提醒而不是编译通过,最终给我们一个莫名其妙的值。这种 Bug 往往也难以寻找。

而在 Swift 中,溢出是在语法检查的时候就无法编译通过的,如下面代码:

1
let aInt8: Int8 = 128 // 错误信息:Integer literal '128' overflows when stored into 'Int8'

Swift 不提供隐式的类型转换

与 Objective-C 不同,Swift 是一门强类型语言,我们甚至无法给一个整型变量赋值一个小数,更别说像 Objective-C 那样任意指针指向任意对象了。但是我们不必在所有情况下显式声明变量类型,是因为 Swift 为我们提供了 类型推断(Type Inference),如下面代码:

1
let aDouble = 1.5 // 类型推断为 Double 类型

但是在类型转换上,Swift 要求极为严格,一个 Int 类型绝对是无法和一个 Double 类型相加的,必须显式地类型转换:

1
2
3
4
5
let x: Double = 1.2
let y: Int = 5

// 必须显式进行类型转换,避免潜在 Bug 出现
let sum = Int(x) + y

赋值运算取消返回值

我们看下面代码:

1
2
3
4
5
NSString *aString = nil;

if (aString == nil) {
NSLog(@"aString is nil."); // 打印 aString is nil.
}

这段代码本身没有意义,目的在于说明一个程序员可能发生的错误,这就是不小心把 == 写成了 =,结果会截然不同,但是 Bug 同样难以寻找。因此在有的 Objective-C 代码规范中会有这样的规定,建议上面代码中的 condition 部分写成 nil == aString 以避免类似 Bug 的发生。

而在 Swift 中,首先是没有非 0 即真的概念的(Swift 认为,0 应该是和其他整数一样的,不应该用它来表示空,因此发明了可选型),其次,赋值运算是没有返回值的,这样把它放在 if 语句中就直接会报错。

for-in、if-else、switch 等语句中的大括号不可省略

记得听过一个故事,苹果的一位程序员因为 if 语句只有一句代码所以没有加大括号,后期添加代码时仍然忘记添加,因此导致 Bug。Swift 中所有这些语句均必须添加大括号。

结尾

本文只是从一些小细节入手去谈 Swift 的安全性,其更多地安全性的表现应该是强类型可选型等等,这些内容值得单独去讲,之后会专门写文章去理解相关的概念。