简单类型
Playground
是一个可以供你输入Swift
代码并立即看到结果的沙盒环境
多行字符串
表示多行的字符串,需要使用将双引号首尾的引号个数由一个改成三个,就像下面这样:
1 | var str1 = """ |
Swift
对于书写这些引号有着特别的规定:
开始的三个引号和结束的三个引号都必须独占一行,但它们占的那两行都不会算进最终的字符串里。
假如你只是想利用多行字符串来使代码看起来更整洁,你可以通过在每行的行尾添加一个\
来确保换行不会进入最终的字符串, 就像这样:
1 | var str2 = """ |
当你通过一个分数创建一个变量时,Swift都会自动赋予这个变量Double
类型。
var pi = 3.141
字符串插值
这个特性允许你在字符串值里插入变量,从而动态地给一个String
类型的变量赋值。在字符串内部,你可以放置任意类型,任意数量的变量。 要放置这些变量,你需要以一个反斜杠开始,即\
,然后跟上用圆括号包起来的变量名
。
1 | var score = 85 var str = "你的分数是\(score)" |
复合类型
元组
元组允许你通过一个值来存储几个值。
听起来跟数组很像,但元组有所不同:你不能往元组中添加或者删除元素 —— 元组的长度是固定的。你不能改变元组中元素的类型 —— 元组创建时内部的元素类型必须是一致的。
字典默认值
如果你试图通过一个不存在于字典中的键读取字典中的值时,Swift会返回给你一个nil
,
当给定的键对应的值不存在时,我们可以指定一个默认的值返回给访问者。
1 | let favoriteIceCream = [ |
当Swift没有从字典中找到小华最喜欢的冰淇淋时,我们得到的不再是一个nil,而是一个“未知”字符串。
枚举
枚举,通常简称enums
,是一种定义一组高度关联的值的方式。它使得这组关联的值使用起来更方便。
有了枚举,我们可以定义一个叫做Result的类型,它既可以是success,也可以是failure,就像这样:
1 | enum Result { |
枚举关联值
枚举还可以存储附属于每个case
的关联值。这个特性使得你可以为枚举附加额外的数据,从而让它们传达更多细微的信息。
定义一个枚举,它存放了各种各样的活动类型:
1 | enum Activity { |
上面的枚举类型存储的信息,可以让我们知道有人在讲话,但我们不知道讲话的内容,或者可以让我们知道有人在跑步,但我们不知道他们将跑去哪里。
通过枚举的关联值,我们添加额外的细节:
1 | enum Activity { |
现在我们有了更精确的信息。我们可以说某人正在谈论足球:
1 | let talking = Activity.talking(topic: "football") |
枚举原始值
给枚举赋予一些原始值从而让它们可以表达某种含义。这么做可以让你动态地创建枚举,并且以不一样的方式来使用它们。
创建一个名叫Planet
的枚举,然后让它的每条case存储一个整数:
1 | enum Planet: Int { |
Swift会为这些case自动分配一个数字,同样的,是从0开始。你可以利用这些数字创建枚举的case。举个例子,earth会被分配到数字2,于是你可以这么创建一个earth的case:
1 | let earth = Planet(rawValue: 2) |
操作符与条件
switch语句
Swift只会运行某一个case里的代码。如果你希望继续执行下一个case的代码,你需要用到 fallthrough 关键字,就像这样:
1 | switch weather { |
范围操作符
Swift提供了两种方式给我们创建范围: 它们是..<
和 ...
操作符。
半开放范围操作符 ..<
,创建的范围不包含右边的值。
而闭合范围操作符 ...
,创建的范围包含右边的值。
范围 1..<5 包含数字1,2,3和4, 而范围 1…5 包含数字1,2,3,4和5。
对于switch语句块来说,范围非常有用。因为你可以把它们用于你的每条case。举个例子,假设我们根据某人的考试成绩打印不同的消息:
1 | let score = 85 |
循环
最常见的循环是for
循环:它在数组和范围上循环,每次拉出一个值然后把它赋予一个常量。
退出多重循环
用嵌套循环实现一个从 1 到 10 的乘法表:
1 | for i in 1...10 { |
如果想退出循环,我们需要做两件事。首先,给外层循环加一个标签,像这样:
1 | outerLoop: for i in 1...10 { |
然后,在内层循环里添加条件,在条件满足时用 break outerLoop
同时退出内外层循环:
1 | outerLoop: for i in 1...10 { |
如果只使用break
,就只能退出内层循环,外层循环会继续运行。
函数
如果你需要返回多个值,可以使用元组作为返回值的类型。
省略参数标签
在使用 print()
函数时并没有传入任何参数标签。我们会写作 print("Hello")
,而不是 print(message: "Hello")
。
通过使用下划线_
作为外部参数标签,你可以在自己的函数里实现一样的效果,就像这样:
1 | func greet(_ person: String) { |
这样写的话,调用 greet()
函数时,你就不必传入参数标签了:
1 | greet("Taylor") |
默认参数
print()
函数打印文本到屏幕,并且不论你传什么内容给它,它都会在最后添加一个换行。所以多次调用 print()
的话,那些文本是不会显示在同一行的。
你可以改变 print()
函数的这个行为:你可以用其他符号,例如空格来取代换行。print()
有一个叫 terminator
的参数,它的默认值是换行符。
通过在参数后面加上一个 =
然后写上一个值,你可以为你自己的函数提供默认参数。
1 | func greet(_ person: String, nicely: Bool = true) { |
可变函数
有一些函数是 可变 的,可变是指函数可以接收任意多个同类型的参数。例如,print()
函数实际上就是可变的:如果你传入多个参数,它们会被以空格相连打印在同一行。
1 | print("Haters", "gonna", "hate") |
你可以通过在参数类型之后添加 ...
,将一个参数声明成可变参数。
1 | func square(numbers: Int...) { |
书写会抛出错误的函数
Swift允许我们从函数中抛出错误。实现的方法是在返回值前写一个 throws
,然后在函数出错时使用 throw
关键字抛出错误。
需要定义一个 enum
,用于描述我们可能抛出的错误。这些错误必须基于Swift已经存在的 Error
类型。
1 | enum PasswordError: Error { |
现在我们来实现一个函数 checkPassword()
,这个函数检测传入的密码是否合理,当密码过于简单时,我们抛出一个错误提醒用户。具体来说,当密码被设置成 “password” 时,执行 throw PasswordError.obvious
。
1 | func checkPassword(_ password: String) throws -> Bool { |
运行可能会抛出错误的函数
Swift并不期望你在程序运行时遭遇错误,因此它不会让你直接运行可能抛出错误的函数。
需要用到三个关键字来运行会抛出错误的函数:do
开启一段可能会遭遇问题的代码,try
放在每一个可能抛出错误的函数前面,最后的 catch
让你可以优雅地处理错误。
1 | do { |
inout 参数
所有传入Swift函数的参数默认都是 常量,所以你无法更改它们。假如你就是想要在函数内改变这些参数呢?可以用 inout
修饰它们,所有在函数内对它们做出的改变都会影响到它们在函数外的原始值。
想要让一个数翻倍。
1 | func doubleInPlace(number: inout Int) { |
为了使用这个可以修改参数的函数,首先要求传入的参数本身不能是常量, 其次,在传入函数时,还要用一个 &
符号,放在参数名前面。它是参数以 inout 方式使用的显式标识。
1 | var myNum = 10 |
闭包
Swift允许我们像字符串和整数一样使用函数。具体来说,你可以创建一个函数然后把它赋给一个变量,利用那个变量来调用函数。你甚至可以把函数作为参数传给另一个函数。
以打印信息为例:
1 | let driving = { |
上面的代码实际上创建了一个匿名的函数,并将这个函数赋给了 driving
。之后你就可以把 driving()
当作一个常规的函数来用,就像这样:
driving()
在闭包中接收参数
为了让一个闭包接收参数,你需要在花括号之后把这些参数列出来,然后跟上一个 in
关键字。这样就告诉Swift,闭包的主体是从哪里开始的。
创建一个闭包,接收一个叫 place 的字符串作为唯一的参数,就像这样:
1 | let driving = { (place: String) in |
函数和闭包的一个区别是运行闭包的时候你不会用到参数标签。因此,调用 driving()
的时候,我们是这样写的:
driving("北京")
从闭包中返回值
闭包也能返回值,写法和闭包的参数类似:写在闭包内部, in
关键字前面。
1 | let drivingWithReturn = { (place: String) -> String in |
闭包作为参数
如果我们打算把这个闭包传入一个函数,以便函数内部可以运行这个闭包。我们需要把函数的参数类型指定为 () -> Void
。 它的意思是“不接收参数,并且返回 Void”。在Swift中,Void
是什么也没有的意思。
写一个travel()
函数,接收不同类型的 traveling
动作, 并且在动作前后分别打印信息:
1 | func travel(action: () -> Void) { |
现在可以用上 driving 闭包了,就像这样:
1 | travel(action: driving) |
拖尾闭包语法
如果一个函数的最后一个参数是闭包,Swift允许你采用一种被称为 “拖尾闭包语法” 的方式来调用这个闭包。你可以把闭包传入函数之后的花括号里,而不必像传入参数那样。
travel() 函数,它接收一个 action 闭包。闭包在两个 print() 调用之间执行:
1 | func travel(action: () -> Void) { |
由于函数的最后一个参数是闭包,我们可以用拖尾闭包语法来调用 travel() 函数,就像这样:
1 | travel() { |
实际上,由于函数没有别的参数了,我们还可以将圆括号完全移除:
1 | travel { |
结构体
可变方法
如果一个结构体拥有一个变量属性,但是这个结构体的实例是以常量的方式创建的,那么在实例中,这个变量属性是不能修改的。这是因为结构体本身已经是常量了,所以它的所有属性也是常量。
Swift无从得知你将以常量还是变量的方式使用结构体。所以安全起见,Swift的默认策略是:不允许你在方法里修改属性,除非你显式地要求这一点。
当你想要改变属性值时,,需要在方法前使用 mutating 关键字,就像这样:
1 | struct Person { |
由于这个方法改变了属性值,所以Swift只会允许这个方法在变量型的 Person 实例上调用。
1 | var person = Person(name: "Ed") |
String 类型是一个结构体类型,数组同样也是结构体
类
类和结构体的第一个区别是类没有逐一成员构造器。这意味着只要你的类里有属性,你就必须自行创建构造器。
1 | class Dog { |
可变性
如果你有一个常量结构体,它有一个变量属性,那么这个变量属性是无法修改的。
如果它是一个常量类,也有一个变量属性,那么这个变量属性是可以被修改的。类的方法在改变属性时,并不需要 mutating
关键字,而结构体则需要。
这个区别意味着你可以修改类中的任何变量属性,即便类的实例本身被声明为常量。
1 | class Singer { |
如果你不想属性被修改,那么你必须直接将属性声明为常量。
1 | class Singer { |
协议
协议是一种描述某个类型必须有某些属性和方法的方式。你告知Swift某个类型将使用某个协议,这个过程称为协议适配或者协议遵循。
举个例子,我们可以写一个函数接收 id
属性,但我们并不精确地关心用的是哪一种数据类型。让我们从 Identifiable
协议开始,这个协议要求所有遵循协议的类型必须有一个 id
字符串属性,并且这个字符串可读写。
1 | protocol Identifiable { |
我们无法创建协议的实例,因为协议只是一种描述,它本身并非一种类型。
协议继承
一个协议可以继承另一个协议,这个过程称为协议继承。跟类不一样的是,你可以同一时间继承多个协议。
扩展
扩展使得你可以为已经存在的类型添加方法,实现它们设计时没有做的事情。
举个例子,我们可以为 Int 类型添加一个扩展方法 squared(),用来返回当前数的平方。
1 | extension Int { |
Swift不允许你通过扩展添加存储属性,但可以用扩展添加计算属性。
1 | extension Int { |
协议扩展
协议可以描述某个类型应当有某种方法,但并没有提供方法的代码。扩展实现有具体代码的方法,但一次只能作用于一个数据类型,你没办法同时给多个类型添加相同的代码。
协议扩展同时解决了这两个问题:它们就像常规扩展一样,差异只在于你并不是只扩展一个特定的类型,比如 Int
,你扩展是的一个协议,因而所有遵循这个协议的类型都会发生改变。
举个例子,下面有一个包含了一些名字的数组和一个同样包含了一些名字的集合:
1 | let pythons = ["Eric", "Graham", "John", "Michael", "Terry", "Terry"] |
Swift的数组和集合都遵循一个叫 Collection
的协议,因此我们可以给 Collection
协议扩展一个叫 summarize()
的方法,这个方法逐一打印collection
里的元素。
1 | extension Collection { |
Array 和 Set 都将获得这个方法。让我们来尝试一下:
1 | pythons.summarize() |
面向协议编程
协议扩展可以为我们自己的协议方法提供默认实现。这使得类型遵循协议变得更加容易,并且允许我们“面向协议编程”——这是一种利用协议和协议扩展来加工代码的方式。
这里有一个叫 Identifiable
的协议,它要求所有遵循协议的类型都有一个叫 id
的属性和叫一个叫 identify()
的方法:
1 | protocol Identifiable { |
虽然我们可以让每个遵循这个协议的类型书写它们自己的 identify()
方法,但协议扩展允许我们可以提供一个默认实现:
1 | extension Identifiable { |
现在当我们再声明一个遵循 Identifiable
协议的类型时,它会自动获得 identify()
方法的实现:
1 | struct User: Identifiable { |
可选型
解包可选型
if let 和 guard let 的主要区别在于 guard let 之后可选型还可以继续使用。
让我们尝试一下 greet()
函数。它将接收一个可选字符串作为唯一的参数,当它解包发现这个参数是nil
时会打印消息并且退出函数。因为可选型 unwrapped
在 guard let
的语句块结束之后作用,我们可以在函数最后打印这个解包后的字符串。
1 | func greet(_ name: String?) { |
空合运算符
空合运算符解包一个可选型,如果可选型包含值则返回这个值,如果可选型不包含值,即可选型的值是 nil,那么返回某个默认值。
1 | func username(for id: Int) -> String? { |
它将检查 username() 函数返回的值:如果是一个字符串,它将被解包并放入 user,如果是 nil,则使用 “Anonymous” 替代。
可选链
假如你试图访问形如 a.b.c
这样的代码并且 b 是可选型,你可以在 b 后面写一个问号来启用 可选链: a.b?.c
。
当代码运行时,Swift会检查 b 是否有值,如果它是 nil,那么这行代码剩下的部分将被忽略。Swift会立即返回 nil。但是如果 b 有值,它将被解包,代码执行将继续。
可选型 try
让我们回忆一下可能抛出错误的函数那一节的知识,看下面的代码:
1 | enum PasswordError: Error { |
上面的 try 写法其实有另外两种选择,这两种选项都能加深你对可选型和强制解包的理解。
第一个是 try?
,它将可能抛出错误的函数转换成返回可选型的函数。如果函数抛出错误,那你就会得到 nil 作为函数的执行结果,否则你会得到将返回值包装之后的可选型。
尝试使用 try? 来执行 checkPassword(),像下面这样:
1 | if let result = try? checkPassword("password") { |
另外一种选择是 try!
,当你确信函数一定不会失败时你可以采用它。如果函数实际抛出了错误,你的代码将崩溃。
使用 try! 来重写前面的代码:
1 | try! checkPassword("sekrit") |
失败构造器
它是一种可能成功也可能失败的构造器。你在结构体或者类里面用init?()
来实现失败构造器。如果某些东西出错,它将返回 nil
。因此这种构造器返回的是某种类型的可选型,你用之前需要解包。
1 | let str = "5" |
举个例子,我们现在要求 Person 结构体必须通过一个9字符的ID字符串来构造。只要不是9个字符,都会返回 nil。
1 | struct Person { |
类型转换
1 | class Animal { } |
Swift 知道 Fish 和 Dog 都继承自 Animal 类,因此它通过类型推断将 pets 创建为一个 Animal 类型的数组。
如果我们想遍历 pets 数组,让所有的狗发出叫声,我们需要执行一次类型转换:Swift将检查每个pet是否 Dog 对象,以便我们调用 makeNoise() 方法。
这里用到了一个关键字 as?,它将返回一个可选型:类型转换失败时返回 nil,成功则返回转换后的类型。
1 | for pet in pets { |