《The Swift Programming Language》读书摘要

Swift_Notes_001.png

本篇读书摘要大概有2万2千字和170个代码段,涵盖了原书11万字的几乎所有知识点,慢读需要3到4个小时。  
在原书通读过3到5遍后,每周再浏览一到两遍读书摘要,两个月后对Swift的基础知识算是能做到了然于胸了。     

基础部分


Swift包含了C和Objecitve-C中所有的基础数据类型,Int 表示整型值;DoubleFloat 表示浮点型值;Bool 表示布尔型值;String 表示字符串型值;Character 表示字符型值。


Swift提供了三个基本的集合类型:ArrayDictionarySet


在Swift中,如果你要处理的值不需要改变,那么使用常量可以让你的代码更加安全并且能更清晰的表达你的意图。


Swift增加了Objective-C中没有的高阶数据类型 —— 元祖(Tuple)。


Swift增加了Objective-C中没有的可选类型(Optional),用于处理值缺失的情况。


Swift是一门类型安全的语言,这意味着Swift可以让你清楚的知道值的类型。


常量和变量必须要在使用前声明,用 let 来声明常量,用 var 来声明变量。

let maximumNumberOfLoginAttempts = 10
var currentLoginAttempt = 0

可以在一行中声明多个常量或变量,用逗号隔开。

var a = 0, b = 0, c = 0.0  // a和b均为Int类型  c为Double类型

当你声明常量或变量的时候可以加上类型标注(type annotation),用来说明常量或变量中要存储的值的类型。如果要添加类型标注,需要在常量或者变量名后面加一个冒号和一个空格,然后再加上类型名称。

var a, b: Int, c: Double  // a和b均为Int类型  c为Double类型

一般来说,很少需要写类型标注。如果在声明常量或者变量的时候赋了一个初始值,Swift可以推断出这个常量或变量的类型。


你可以用 print(_:separator:terminator:) 函数来输出常量或变量的值。
print(_:separator:terminator:) 函数是一个用来输出一个或多个值到适当输出区的全局函数,在Xcode中,该函数将会把内容输出到控制台面板上。

// print(_:separator:terminator:) 函数在标准库中的定义
public func print(_ items: Any..., separator: String = default, terminator: String = default)

/**
Parameters:
  - items: Zero or more items to print.
  - separator: A string to print between each item. The default is a single space (" ").
  - terminator: The string to print after all items have been printed. The default is a newline ("\n").
*/

Swift采用字符串插值(string interpolation)的方式把常量名或变量名当做占位符插入到字符串中,在运行时用当前常量或变量的值替换这些占位符。

let name = "Todd Cheng"
let age = 18
print("my name is \(name), my age is \(age)")

Swift支持多行注释的嵌套。


你可以访问不同整数类型的 maxmin 属性来获取对应整数类型的最大值和最小值。

let a = UInt8.max  // a的值为255,且a的类型为UInt8
let b = UInt8.min  // b的值为0,且b的类型为UInt8

Swift提供了一个特殊的整数类型 Int,长度与当前平台的原生字长相同。

  • 在32位平台设备上,Int 就是 Int32
  • 在64位平台设备上,Int 就是 Int64

Swift还提供了一个特殊的无符号整数类型 UInt,长度与当前平台的原生字长相同。

  • 在32位平台设备上,UInt 就是 UInt32
  • 在64位平台设备上,UInt 就是 UInt64

尽量不要使用 UInt 或其他整数类型(如 Int8Int16Int32Int64UInt8UInt16UInt32UInt64 等),最好使用 Int,即使你要存储的整数常量或者变量已知是非负的。统一使用 Int 可以提高代码的可复用性,避免不同类型之间的转换,并且可以匹配整数类字面量的类型推断。


Swift提供了两种浮点数类型:

  • Double 表示64位浮点数。可以保证精确到小数点后15位。
  • Float 表示32位浮点数。可以保证精确到小数点后6位。
let a = 1.01234567890123456789
let b = 1.0123456789012345
let c = 1.0123456789012346

a == b  // false
a == c  // true
let a: Float = 1.12345678
let b: Float = 1.1234567
let c: Float = 1.1234568

a == b  // false
a == c  // true

Swift是一门类型安全的语言。类型安全的语言可以让你清楚的知道代码要处理的值的类型。比如代码需要一个 String,你绝不可能一不留神赋值一个 Int


当你在声明常量或者变量的时候赋给它们一个字面量(literal value)即可触发类型推断。


整数字面量可以被写成:

  • 十进制数,没有前缀。
  • 二进制数,前缀是 0b
  • 八进制数,前缀是 0o
  • 十六进制数,前缀是 0x

浮点数可以有一个可选的指数(exponent),用大写 E 或者小写 e 来表示。比如 1.25E2 就是 125.01.25e-2 就是 0.0125


整数和浮点数可以添加下划线 _ 来增强可读性,并且不会影响字面量的值。

let a = 1000_000.000_000_1
let b = 10000_0000

类型别名就是给现有的类型定义一个新名字,可以使用 typealias 关键字来定义类型别名。


如果你在需要使用 Bool 类型的地方使用了非 Bool 值,Swift的类型安全机制会报错。比如:

let i = 1
if i { }  // 这行代码不会通过编译,会报错   
if i == 1 {}  // 正确写法

这种方式可以避免一些错误并保证这块代码的意图总是清晰的。


元组把多个值组合成一个复合值。元组内的值可以是任意类型,并不要求是相同类型。

let http404Error = (404, "Not Found")
// http404Error的类型为 (Int, String)

通过下标来访问元组中的单个元素,下标从0开始。

let http404Error = (404, "Not Found")
http404Error.0
http404Error.1

可以在定义元组的时候给每一个元素命名。

let http404Error = (code: 404, description: "Not Found")
http404Error.code
http404Error.description

你可以将一个元组的内容分解成单独的常量或变量,然后再分别使用它们。

let (statusCode, statusMessage) = (404, "Not Found")
statusCode
statusMessage

如果你只需要一部分元组值,分解的时候可以把忽略的部分用下划线 _ 标记。

let (_, justStatusMessage) = (404, "Not Found")
justStatusMessage

使用可选类型来处理值可能缺失的情况。可选类型表示要么有值,为 x;要么没有值,为 nil


C和Objective-C中并没有可选类型这个概念。Objective-C中的 nil 只能用在对象身上,对于结构体、基本的C类型或者枚举类型都不起作用。对于这些类型,Objective-C一般会返回一个特殊值(比如 NSNotFound )来暗示值缺失。这种办法是基于调用者知道并记得对特殊值判断的前提下的。然而,Swift的可选类型可以让你暗示任意值的缺失,并不需要一个特殊值。


如果你声明一个可选常量或者变量但是没有赋值,它会自动被设置为 nil


Swift中的 nil 和Objective-C中 nil 并不一样。在Objective-C中,nil 是一个指向不存在对象的指针;在Swift中,nil 不是指针,它是一个确定的值,用来表示值缺失。任何类型的可选状态都可以被设置为 nil,不只是对象类型。


可以使用 if 语句让一个可选类型的常量或变量与 nil 比较是否相等来判断该可选常量或变量是否包含值。
如果可选类型有值,它将不等于 nil。并且当确定了一个可选类型确实包含值之后,可以在可选类型的常量或变量名字后面加一个感叹号 ! 来获取其中的值,这被称为可选类型的强制解包。

let a = Int("123")
if a != nil {
    print(a!)
}

使用 ! 来获取一个不存在的可选类型会导致运行时错误。因此,在使用 ! 来强制解包之前,一定要确定可选类型包含一个非 nil 的值。


使用可选绑定来判断可选类型是否包含值,如果包含,就把值赋给一个临时的常量或变量。可选绑定可以用在 ifwhile 语句中。

if let a = Int("123") {
    print(a)
}

你可以在一个 if 语句中包含多个可选绑定或多个布尔条件,使用逗号分开就行,只要有任意一个可选绑定的值为 nil,或者任意一个布尔条件的值为 false,则整个 if 语句的条件判断为 false。


if 条件语句中使用常量或变量来创建一个可选绑定,仅在 if 语句的句中才能获取到该常量或变量的值。
相反,在 guard 语句中使用常量或变量来创建一个可选绑定,仅在 guard 语句外且在语句后才能获取到该常量或变量的值。


有时候在代码中,在可选类型被第一次赋值之后,可以确定该可选类型总会有值,在这种情况下,每次都要判断和解包可选值是非常低效的。这时候可以把该可选类型声明为隐式解包可选类型(implicitly unwrapped optional),把普通可选类型后的问号(如 String?)改为感叹号(如 String!)来声明一个隐式解包可选类型。

一个隐式解包可选类型其本质还是一个普通的可选类型,但是可以被当做非可选类型来使用,并不需要每次解包来获得可选值。

let optionalString: String? = "Optional String"
print(optional!)

let implicitlyOptionalString: String! = "Implicitly Optional String"
print(implicitlyOptionalString)

如果在隐式解包可选类型没有值的时候尝试取值,仍会触发运行时错误。这和在没有值的普通可选类型后面加感叹号是一样的。


你仍然可以把隐式解包可选类型当做普通的可选类型,用 if 来判断是否有值,用可选绑定来解包可选值。


相对于可选类型用值的存在与缺失来表达一个函数是否包含满足条件的值,错误处理可以推断出函数调用失败的原因,并传播至程序的其他地方。


一个函数可以通过在声明中添加 throws 关键字来表明函数有可能会抛出错误消息。
当一个函数能抛出错误消息时,在调用该函数的时候应该前置 try 关键字来捕获错误消息。

func canThrowAnError() throws {
    // 这个函数有可能抛出错误
}

do {
    try canThrowAnError()
    aFunc()
} catch xxxError.oneError {
    oneErrorHandle()
} catch xxxError.twoError {
    twoErrorHandle()
}

一个 do 语句创建了一个新的包含作用域,使得错误能被传播到一个或多个 catch 从句中。


可选类型能够让你判断值是否存在,你可以在代码中优雅的处理值缺失的情况。然而,在某些情况下,如果值缺失或者值并不满足特定的条件,代码就没办法继续执行,这时,你可以在代码中触发一个断言来结束代码运行并通过调试来找到无法继续执行下去的原因。
你可以使用断言来保证在运行其他代码之前,某些重要的条件已经被满足。断言会在运行时判断一个逻辑条件是否为true。如果条件判断为true,代码会继续进行;如果条件判断为false,应用程序将被终止。在调试环境下,你可以从Xcode中清楚的看到不合法的状态发生在哪里并以此来检查断言被触发时代码的状态。


你可以使用全局函数 assert(_:_:file:line:) 来实现一个断言。

// assert(_:_:file:line:) 函数在标准库中的定义

/// To check for invalid usage in Release builds, 
/// see `precondition(_:_:file:line:)`.

/// * In playgrounds and `-Onone` builds (the default for Xcode's Debug
///   configuration): If `condition` evaluates to `false`, stop program
///   execution in a debuggable state after printing `message`.

/// * In `-O` builds (the default for Xcode's Release configuration), 
/// `condition` is not evaluated, and there are no effects.

/// - Parameters:
///   - condition: The condition to test. `condition` is only evaluated in
///     playgrounds and `-Onone` builds.
///   - message: A string to print if `condition` is evaluated to `false`. The
///     default is an empty string.
///   - file: The file name to print with `message` if the assertion fails. The
///     default is the file where `assert(_:_:file:line:)` is called.
///   - line: The line number to print along with `message` if the assertion
///     fails. The default is the line number where `assert(_:_:file:line:)`
///     is called.

public func assert(_ condition: @autoclosure () -> Bool, 
                     _ message: @autoclosure () -> String = default, 
                          file: StaticString = #file, 
                          line: UInt = #line)  

let age = -3
assert(age > 0, "A person's age cannot be less than zero")  // 不满足条件,断言被触发,应用终止。

当代码使用优化编译的时候,断言将被禁用。例如在Xcode中,使用默认的 Target Release 配置选项来编译时,断言会被禁用。


基本运算符


Swift支持大部分的C语言运算符,且改进了许多特性来减少常规编码错误。如:

  • 赋值符 = 不返回值。为了防止把想要判断相等运算符 == 的地方写成赋值运算符导致的错误。
  • 算术运算符(+, -, *, /, % 等)不允许值溢出,以此来避免保存变量时由于变量大于或小于其类型所能承载的范围时导致的异常结果。(Swift允许你使用溢出运算符 & 来实现溢出)

运算符分为一元、二元和三元运算符:

  • 一元运算符操作一个操作数。如 -a+a!aa! 等。
  • 二元运算符操作两个操作数。如 +-*/%=等。
  • 三元运算符操作三个操作数。和C语言一样,Swift只有一个三元运算符,就是三目运算符(a ? b : c)。

与C语言和Objective-C不同,Swift的赋值操作并不返回任何值。所以以下代码是错误的:

if x = y { }  // 错误代码,因为 x = y 并不返回任何值,而 if 后面需要一个逻辑布尔值。

这个特性使你无法把 == 错写成 =


与C语言和Objective-C不同的是,Swift默认情况下不允许在数值运算中出现溢出情况。但是你可以使用Swift的溢出运算符来实现溢出运算(如 a &+ b)。


a % b,当 b 为负数时,b 的符号会被忽略。也就是说,a % ba % -b 的结果是一样的。

9 % 4    // 1
9 % -4   // 1

-9 % 4    // -1
-9 % -4   // -1

复合赋值运算符(如 +=-=*=/=%= 等)没有返回值。类似 let b = a += 2 这样的代码是错误的。


Swift提供了恒等 === 和不恒等 !== 这两个比较符来判断两个常量或变量是否引用了同一个对象实例。


当元组中的值可以比较时,你可以使用比较运算符来比较他们的大小。比如,IntSting 类型的值可以比较大小,所以类型为 (Int, String) 的元组也可以比较大小。相反,Bool 类型的值无法比较大小(但可以比较是否相等),也意味着存在 Bool 类型的元组也就无法比较大小(但可以比较是否相等)。
比较元组大小会按照从左到右、逐值比较的方式,直到发现有两个值不等时停止。如果所有的值都相等,那么这一对元组就称之为相等。


Swift的标准库中只支持7个元素以内的元组来比较大小,如果元组中元素超过7个,则需要自己实现比较操作。


三目运算符( a ? b : c )提供了高效率且便捷的方式来表达二选一的概念。需要注意的是,过度使用三目运算符会使简洁的代码变得难懂,我们应该避免在一个语句中使用多个三目运算符。


空和运算符 a ?? b 将对可选类型 a 进行空判断,如果 a 包含值就进行解包,否则就返回一个默认值 b
a 必须是可选类型,默认值 b 的类型必须要和 a 存储的值的类型保持一致。


空和运算符 a ?? b 其实是对三目运算符 a != nil ? a! : b 的简洁写法。


Swift提供了两个方便表达一个区间的运算符:闭区间运算符 a...b 以及 半开区间运算符 a..<b

半开区间运算符的实用性在于当你使用一个从0开始的列表时,可以非常方便的从0遍历到列表的长度。


逻辑运算符的操作对象是逻辑布尔值。
Swift支持C语言的三个标准逻辑运算符:

  • 逻辑非 !a
  • 逻辑与 a && b
  • 逻辑或 a || b

逻辑与和逻辑或运算符是支持短路计算的。


字符串和字符


String 类型是一种快速、现代化的字符串实现。每一个字符串都是由编码无关的 Unicode 字符组成,并支持访问字符的多种 Unicode 表示形式。


要创建一个空字符串作为初始值,可以将空的字符串字面量赋值给变量,也可以初始化一个新的 String 实例。

var emptyString1 = ""          // 空字符串字面量
var emptyString2 = String()    // 初始化方法
// emptyString1 与 emptyString2 均为空字符串并相等

可以通过 StringisEmpty 属性来判断字符串是否为空:

if emptyString1.isEmpty { }

Swift中的 String 类型是值类型。也就是说,如果你创建了一个字符串,那么当其为其他常量或变量进行赋值操作,或在函数中传递时,会进行值拷贝。

Swift编译器会优化字符串的使用,使实际的复制只发生在绝对必要的情况下,这意味着把字符串作为值类型的同时可以获得极高的性能。


可以通过 for-in 循环遍历字符串的 characters 属性来获取每一个字符的值。

for character in "Todd" { 
    print(character) 
}

字符串可以通过传递一个 Character 数组来进行初始化:

let catCharacters: [Character] = ["C", "A", "T"]
let catString = String(catCharacters)

字符串可以通过加法运算符 + 来把两个字符串连接在一起:

let string1 = "Hello "
let string2 = "Todd!"
let string3 = string1 + string2

也可以通过加法赋值运算符 += 来把一个字符串添加到一个已经存在的字符串变量上:

var string4 = "Welcome "
string4 += "Todd!"

还可以通过 append() 函数将一个字符附加到一个字符串变量的尾部:

var string5 = "Chen"
var character: Character = "g"  
string5.append(character)

// 在Swift的标准库中,String 的 append(_:) 函数有两种定义。
public mutating func append(_ c: Character)
public mutating func append(_ other: String)

Unicode 是一个国际标准,用于文本的编码和表示。它使你能够用标准的格式来表示来自任意语言几乎所有的字符。Swift的 StringCharacter 类型是完全兼容 Unicode 标准的。


字符串字面量的特殊字符:

  • 转义字符:\0(空字符),\\(反斜线),\t(水平制表符),\n(换行符),\r(回车符),\"(双引号),\'(单引号)。
  • Unicode 标量:写成 \u{n}u 为小写),其中 n 为任意一到八位十六进制数且可用的 Unicode 标量。

每一个Swift的 Character 类型都代表一个可扩展的字符群集。一个可扩展的字符群集是一个或多个 Unicode 标量的有序排列。
比如,字符 é 可以用单一的 Unicode 标量 \u{00E9} 来表示。也可以用一个标准的字符 e \u{0065} 加上一个急促重音 \u{0301} 来表示。


可以通过访问字符串的 characters 属性的 count 属性来获取字符串中 Character 值的数量。


可扩展的字符群集可以由一个或多个 Unicode 标量组合而成,这也就意味着相同字符的不同表示方式可能需要不同数量的内存空间来存储,所以Swift中的字符在一个字符串中并不一定占用相同的内存空间数量。因此在没有获取到可扩展字符群集范围的时候,是无法计算出字符串中字符数量的。当你在处理一个字符串的时候,需要注意到 characters 属性的访问必须要遍历字符串中全部的 Unicode 标量才能确定字符的数量。


每一个 String 值都有一个关联的索引类型(String.Index),它对应着字符串中每一个 Character 的位置。
字符的不同表示方式可能会占用不同数量的内存空间,所以要知道 Character 的具体位置,就必须从 String 的开头遍历每一个 Unicode 标量直到结尾。因此,Swift的字符串不能用整型做索引。


使用 startIndex 属性可以获取一个 String 的第一个 Character 索引。
使用 endIndex 属性可以获取一个 String 的最后一个 Character 的后一个位置的索引。

endIndex 不能作为一个字符串的有效下标。

如果 String 是空串,startIndexendIndex 相等且都等于 0


通过调用 Stringindex(before:)index(after:) 函数可以立即得到前面一个或后面一个索引。
还可以通过调用 String 的 index(_:offsetBy:) 函数来获取对应偏移量的索引。

有了索引之后,可以通过下标语法来获取对应索引位置的 Character

let name = "Todd"

name[name.startIndex]  // T
name[name.endIndex]  // 报错

let aIndex = name.index(after: name.startIndex)
let bIndex = name.index(before: name.endIndex)
name[aIndex]  // o
name[bIndex]  // d

let cIndex = name.index(name.startIndex, offsetBy:2)
name[cIndex]  // d

使用 characters 属性的 indices 属性会创建一个包含全部索引的范围,用来在一个字符串中访问单个字符。

for index in name.characters.indices {
   print(name[index])
}

你可以使用 startIndexendIndex 属性以及 index(before:)index(after:)index(_:offsetBy:) 函数在任意一个遵循 Collection 协议的类型里面,如上面的 String,也可以使用在 ArrayDictionarySet 中。


调用 insert(_:at:) 函数可以在一个字符串的指定索引位置插入一个字符。
调用 insert(contentsOf:at:) 函数可以在一个字符串的指定索引位置插入一个字符串。

var string = "Hello"
string.insert("!", at: string.endIndex)  // Hello!
string.insert(contentsOf: " Todd".characters, 
                      at: string.index(before: string.endIndex))  // Hello Todd!

调用 remove(at:) 函数可以在一个字符串的指定索引位置删除一个字符。
调用 removeSubrange(_:) 函数可以在一个字符串的指定索引范围删除一个字符串。

var string = "Hello!"
string.remove(at: string.index(before: string.endIndex))  // Hello

let range = string.startIndex...string.index(string.startIndex, offsetBy: 2)
string.removeSubrange(range)  // lo

你可以使用 insert(_:at:)insert(contentsOf:at:) 函数以及 remove(at:)removeSubrange(_:) 函数在任意一个遵循 RangeReplaceableCollection 协议的类型里面,如上面的 String,也可以使用在 ArrayDictionarySet 中。


字符串/字符可以用等于操作符 == 和不等于操作符 !=

如果两个字符串/字符的可扩展字符群集是标准相等的,那就认为它们是相等的。因此,即使可扩展字符群集是由不同的 Unicode 标量组成,只要它们具有同样的语言意义和外观,就认为他们标准相等。

let a = "caf\u{00E9}"
let b = "caf\u{0065}\u{0301}"
a == b  // true

通过调用 hasPrefix(_:)hasSuffix(_:) 函数来检查字符串中是否拥有特定的前缀和后缀。

hasPrefix(_:)hasSuffix(_:) 函数是在每个字符串中逐个字符比较其可扩展的字符群集是否标准相等。


Swift提供了三种方式来访问字符串的 Unicode 表示形式:

  • UTF-8,利用字符串的 utf8 属性进行访问。
  • UTF-16,利用字符串的 utf16 属性访问。
  • UTF-32,也就是 Unicode 标量。利用字符串的 unicodeScalars 属性访问。

集合类型


Swift语言提供了 ArrayDictionarySet 这三种基本的集合类型来存储数据。
数组 Array 是有序数据的集,字典 Dictionary 无序键值对的集,集合 Set 是无序无重复数据的集。


ArrayDictionarySet 中存储的数据值类型必须明确。这意味着我们不能把不正确的数据类型放入其中,同时也说明了我们完全可以对取回的值的类型非常自信。


在我们不需要改变集合的时候尽量应该创建不可变集合,这样不仅可以有更好的性能优化,还能更加清晰的表达你的意图。


数组使用有序列表存储同一类型的多个值。相同的值可以多次出现在数组中的不同位置。


写Swift数组应该遵循像 Array<Element> 这样的形式,其中 Element 是这个数组中唯一允许存在的数据类型。也可是使用 [Element] 这样的简单语法。在实际编写代码的过程中,推荐使用 [Element] 这种写法。


通过构造函数创建一个空数组:

var someInts = [Int]()

清空数组(置空):

var someInts = [Int]()
someInts.append(3)
someInts = []

创建特定大小并有默认值的数组:

var threeDoubles = Array(repeating: 0.0, count: 3)  // [0.0, 0.0, 0.0]

通过两个数组相加创建一个数组:

var oneDouble = [Double]()
oneDouble.append(1.0)

let twoDoubles = Array(repeating: 2.0, count: 2)

let threeDoubles = oneDouble + twoDoubles  // [1.0, 2.0, 2.0]

用数组字面量来创建数组:

var shoppingList = ["Eggs", "Milk"]

// 如果数组字面量中有多种数据类型,则必须采用类型标注
var one: [Any] = [1, 1.0, "一"]

通过访问数组的 count 属性来获取数组中数据项的数量。

通过访问数组的 isEmpty 属性来判断数组是否为空。


数组的增删改查:

var array = [Int]()

// 增
array.append(1)
array.append(contentsOf: 2...5)  // array.append(contentsOf: [2, 3, 4, 5])
array += [6, 7, 8]  // array += 6...8

// 删
array.remove(at: 0)  // 删除第一个
array.removeFirst()  // 删除第一个
array.removeLast()  // 删除最后一个
array.removeFirst(2)  // 删除前两个
array.removeLast(2)  // 删除后两个
array.removeSubrange(2...5)  // 删除第2个到第5个

// 改
array[0] = -1
array[1...3] = [9]
array.insert(-2, at: 0)
array.insert(contentsOf: 10...15, at: array.count)

// 查
array[0]
array[0...3]

使用 for-in 循环来遍历数组中的所有数据项。

如果你同时需要数据项和索引值,可以使用 enumerated() 函数来进行数组遍历。enumerated() 函数返回一个由每一个索引值和数据值组成的元组的新数组。

for (index, value) in array.enumerated() {
    print(index)
    print(value)
}

集合 Set 用来存储相同类型并且没有确定顺序的值。当集合元素顺序不重要时或者希望确保每个元素只出现一次时可以使用集合而不是数组。


一个类型要存储在集合中,该类型必须是可哈希化的。也就是说,该类型必须能提供一个方法来计算它的哈希值。

哈希值是 Int 类型的。相等对象的哈希值也相同。比如,若 a == b,则 a.hashValue == b.hashValue。

let a = 1
let b = 1
a == b  // true
a.hashValue == b.hashValue  // true

Swift的所有基本类型(如 StringIntDoubleBool 等)默认都是可哈希化的,可以作为集合中值的类型或者字典中键的类型。

没有关联值的枚举成员值默认也是可哈希化的。

enum Direction {
    case east
    case sourth
    case west
    case north
}

Direction.sourth.hashValue  // 1

可以使用你自定义的类型作为集合中值的类型或者字典中键的类型,但是自定义的类型必须遵守 Hashable 协议。符合 Hashable 协议的类型需要提供一个类型为 InthashValue 属性。
由于 Hashable 协议还遵守了 Equatable 协议,所以还需要提供一个对相等运算符 == 的实现。


写Swift集合应该遵循像 Set<Element> 这样的形式,其中 Element 表示 Set 中允许存储的类型。和数组不同的是,集合没有等价的简写形式。


通过构造函数创建一个空集合:

var letters = Set<Character>()

清空集合中的内容:

var letters = Set<Character>()
letters.insert("a")
letters = []

用数组字面量来创建集合:

var favoriteGenres: Set<String> = ["Rock", "Classical", "Hip pop"]

一个 Set 类型不能从数组字面量中被单独推断出来,因此 Set 类型必须显示声明。然而,由于Swift的类型推导功能,如果你想使用一个数组字面量构造一个 Set 并且该数组字面量中所有的元素类型都相同,那么你无须写出 Set 的具体类型。上面的代码可以简化为:

var favoriteGenres: Set = ["Rock", "Classical", "Hip pop"]

通过访问集合的 count 属性来获取集合中元素的数量。

通过访问集合的 isEmpty 属性来判断集合是否为空。


对 Set 的基本操作:

var favoriteGenres: Set = ["Rock", "Classical", "Hip pop"]

// 添加一个新元素
favoriteGenres.insert("Jazz")

// 删除一个元素
// 如果Set中存在想要删除的元素,则返回该元素值,否则返回nil
if let removedGenre = favoriteGenres.remove("Rock") { }

// Set中是否包含某个元素
favoriteGenres.contains("Classical")

使用 for-in 循环来遍历集合。

Swift中的 Set 类型没有确定的顺序,为了按照特定的顺序来遍历一个 Set 中的值,可以使用 sorted() 函数,它将返回一个有序数组,这个数组的元素排列由操作符 <(升序) 对元素进行比较的结果来确定的。

for genre in favoriteGenres.sorted() {
    print(genre)
}

两个集合基本操作:

let aSet: Set = [1, 3, 5, 7, 9]
let bSet: Set = [0, 2, 4, 6, 8]

// a与b的并集
aSet.union(bSet)

// a与b的交集
aSet.intersection(bSet)

// 在集合a中,不在集合b中
aSet.subtracting(bSet)

// 两个集合是否相等(两个集合是否包含全部相同的元素)
aSet == bSet

// 一个集合是否是另一个集合的子集
aSet.isSubset(of: bSet)

// 一个集合是否是另一个集合的超集
aSet.isSuperset(of: bSet)

字典是一种存储同一类型的值的容器。每个值都关联唯一的键。
和数组不同的是,字典中的数据项并没有具体的顺序。


Dictionary<Key, Value> 的这种写法来定义Swift中的字典,其中 Key 是字典中键的数据类型,Value 是字典中值的数据类型。也可以使用 [Key: Value] 这样的简化形式来创建一个字典类型。推荐使用后者简化写法。


字典中 Key 的类型必须遵守 Hashable 协议。


利用构造函数创建一个空字典:

var namesOfIntegers = [Int: String]()

清空字典中的内容:

var namesOfIntegers = [Int: String]()
namesOfIntegers[6] = "six"
namesOfIntegers = [:]

使用字典字面量来创建字典:

var ab = ["aaa": "AAA", "bbb": "BBB"]

// 如果键的类型或者值的类型不一致,则必须采用类型标注
var person: [String: Any] = ["name": "Todd", "age" : 18]

通过访问字典的 count 属性来获取字典中键值对的数量。

通过访问字典的 isEmpty 属性来判断字典是否为空。


对于字典的一些基本操作:

var namesOfIntegers = [Int: String]()

// 增/改
namesOfIntegers[1] = "one"
namesOfIntegers[2] = "two"
namesOfIntegers[3] = "three"

// 如果键不存在就新增,如果存在就修改
// updateValue(_:forKey:)有返回值,返回更新值之前的原值
namesOfIntegers.updateValue("Three", forKey: 3)  // three
namesOfIntegers.updateValue("Four", forKey: 4)  // nil

// 查(通过下标语法来获取键对应的值)
namesOfIntegers[4]
namesOfIntegers[5]

// 删
namesOfIntegers[4] = nil

// removeValue(forKey:)有返回值,返回被删除的值
namesOfIntegers.removeValue(forKey: 3)  // Three
namesOfIntegers.removeValue(forKey: 5)  // nil

使用 for-in 循环来遍历字典中的键值对:

for (key, value) in namesOfIntegers {
    print(key)
    print(value)
}

通过访问字典的 keysvalues 属性可以获取到字典所有的键和值。

Swift的字典类型是无序集合类型,为了以特定顺序遍历字典的键或值,可以对字典的 keysvalues 属性使用 sorted() 函数。


控制流


使用 for-in 循环来遍历一个集合中的所有元素,如数字范围、字符串中的字符、数组中的元素以及字典中的键值对等。

for index in 1...5 { }
for character in string.characters { }
for name in nameArray { }
for (key, value) in infoDictionary { }

如果不需要区间序列内每一项的值,可以使用下划线 _ 来忽略这个值。

// 打印5次
for _ in 1...5 { 
    print("Todd")
}

while 循环会一直运行一段语句直到条件变成false。这类循环适合使用在迭代次数未知的情况下。

Swift提供两种 while 循环形式:

  • while 循环。每次在循环开始时计算条件是否符合。
  • repeat - while 循环。每次在循环结束时计算条件是否符合。

Swift提供了两种条件分支语句:if 语句和 switch 语句。当条件较为简单且可能的情况很少时,使用 if 语句;当条件较复杂、有更多排列组合的时候 switch 语句更适用。并且,switch语句在需要用到模式匹配的情况下会更有用。


switch 语句会尝试把某个值与若干个模式进行匹配。根据第一个匹配成功的模式,switch 语句会执行对应的代码。


switch 语句必须是完备的。这就是说,每一个可能的值都必须至少有一个分支与之对应。在某些不可能涵盖所有值的情况下,可以使用默认分支来涵盖其他所有没有对应的值,这个默认分支必须在 switch 语句的最后面。


与C和Objective-C中的 switch 语句不同,在Swift中,当匹配的 case 分支中的代码执行完毕后,程序会立即终止 switch 语句,并不会继续执行下一个 case 分支。也就是说,不需要在 case 分支中显式的使用 break 语句。这使得 switch 语句更安全、更易用,也能避免因忘记写 break 语句而产生的错误。

如果想要显式贯穿 case 分支,可以使用 fallthrough 语句。

虽然在 case 语句后 break 不是必须的,但是依然可以在 case 分支中的代码执行完毕前使用 break 跳出 switch 语句。


每一个 case 分支都必须包含至少一条语句。像下面这样的代码是无效的,因为第一个 case 分支是空的:

let character: Character = "a"

switch character {
case "a":  // 编译报错
case "A":
    print("The letter is A")
default:
    print("Other letters")
}

如上代码,为了让单个 case 同时匹配 aA,可以将这两个值组成一个复合匹配,用逗号分开:

let character: Character = "a"

switch character {
case "a", "A":
    print("The letter is a/A")
default:
    print("Other letters")
}

case 分支的模式可以是一个值区间。


使用元组在同一个 switch 语句中匹配多个值。元组中的元素可以是值,也可以是区间。还可以使用下划线 _ 来匹配所有可能的值。

let point = (0, 1)

switch point {
case (0, 0):
    print("origin")
case (_, 0):
    print("x axis")
case (0, _):
    print("y axis")
case (-2...2, -2...2):
    print("inside the 4*4 box")
default:
    print("outside the 4*4 box")
}

从这个例子中可以看出,Swift允许多个 case 匹配同一个值。但是,如果存在多个匹配,那么只会执行第一个被匹配到的 case 分支,剩下分支都会被忽视掉。


case 分支允许将匹配的值绑定到一个临时的常量或变量,并且在 case 分支体内使用 —— 这种行为被称为值绑定 value binding

let point = (2, 0)

switch point {
case (let x, 0):
    print("x axis and x is \(x)")
case (0, let y):
    print("y axis and y is \(y)")
case let (x, y):
    print("x is \(x) and y is \(y)")
}

可以看到,这个 switch 语句不包含默认分支。这是因为最后一个 case 声明了一个可以匹配余下所有值的元组。这使得 switch 语句已经完备了,因此不需要默认分支。


case 分支的模式可以使用 where 语句来判断额外的条件。

let point = (2, 2)

switch point {
case let (x, y) where x == y:
    print("on the line x = y")
case let (x, y) where x == -y:
    print("on the line x = -y")
case let (x, y):
    print("x is \(x) and y is \(y)")
}

当多个条件可以使用同一种方法来处理时,可以将这几种可能放在同一个 case 后面,并且用逗号隔开。当 case 后面的任意一种模式匹配的时候,这条分支就会被匹配。并且,如果匹配列表过长,还可以分行书写。

let character: Character = "e"

switch character {
case "a", "e", "i", "o", "u":
    print("\(character) is a vowel")
case "b", "c", "d", "f", "g", "h", "j", "k", "l", "m",
     "n", "p", "q", "r", "s", "t", "v", "w", "x", "y",
     "z":
    print("\(character) is a consonant")
default:
    print("\(character) is not a vowel or a consonant")
}

控制转移语句可以改变代码的执行顺序,通过它可以实现代码的跳转。Swift有五种控制转移语句:

  • continue:告诉一个循环体立刻停止本次循环,重新开始下次循环,但并不会离开整个循环体。
  • break:会立刻结束整个控制流的执行。如果想要更早的结束一个 switch 代码块或者一个循环体时,可以使用 break 语句。
  • fallthrough
  • return
  • throw

在Swift中,可以在循环体和条件语句中嵌套循环体和条件语句来创造复杂的控制流结构。并且循环体和条 件语句都可以使用 break 语句来提前结束整个代码块。因此,显式地指明 break 语句想要终止的是哪个循环体或者条件语句会很有用。类似地,如果你有许多嵌套的循环体,显式指明 continue 语句想要影响哪一个循环体也会非常有用。

为了实现这个目的,可以使用标签来标记一个循环体或者条件语句,对于一个条件语句,可以使用 break 加标签的方式来结束这个被标记的语句。对于一个循环语句,可以使用 break 或者 continue 加标签来结束或者继续这条被标记语句的执行。

声明一个带标签的语句是通过在该语句的关键词的同一行前面放置一个标签作为这个语句的前导关键字,并且该标签后面跟一个冒号。下面是一个针对 while 循环体的标签语法,同样的规则适用于所有的循环体和条件语句。

// 格式
labelName: while condition {
    statements
}

// 例子
oneRoop: while true {
    twoRoop: while true {
        var i = Int(arc4random_uniform(10))
        var j = Int(arc4random_uniform(10))
        if i == 5 {
            break oneRoop
        }
        if j == 5 {
            break twoRoop
        }
    }
}

if 语句一样,guard 的执行取决于其后表达式的布尔值。不同于 if 语句,一个 guard 语句总是有一个 else 从句,如果条件不为真则执行 else 从句中的代码。

如果 guard 语句的条件被满足,则继续执行 guard 语句大括号后的代码。如果条件不被满足,在 else 分支上的代码就会被执行,这个分支必须转移控制以退出 guard 语句出现的代码段。它可以用控制转移语句如 returnbreakcontinue 以及 throw,或者调用一个不返回的方法或函 数,例如 fatalError()

相比于可以实现同样功能的 if 语句,使用 guard 语句会提升代码的可读性。它可以使代码连贯的被执行而不需要将它包含在 else 语句块中。

func greet(person: [String: String]) {
    guard let name = person["name"] else { return }
    print("Hello \(name)")
    guard let location = person["location"] else { return }
    print("I hope the weather is nice in \(location).")
}

在Swift开发中,为了适配不同的操作系统版本,有时候必须要对API的兼容性做处理。

在Objective-C中,通常采用如下三种方式:

if ([UIDevice currentDevice].systemVersion.intValue >= 8) { }

if (self respondsToSelector:@selector(xxxAPI)) { }

#if __IPHONE_OS_VERSION_MIN_REQUIRED >= 70000
    xxxx
#else
    xxxx
#endif

对Swift来说,我们可以在 ifguard 条件语句中使用可用性条件 #available 在运行时判断不同的平台下,做不同的逻辑处理。

if #available(iOS 10.0, *) {
    // iOS10及更高的系统下运行
} else { 
    // iOS10以下系统下运行 
}

guard #available(iOS 9.0, *) else { return xxx }

最后一个参数 * 是必须的,用于指定在所有其它平台中(如 macOSwatchOStvOS 等)。


函数


如果函数没有参数,函数名后的一对圆括号还是不能省略的。当该无参函数被调用时,也需要在函数名后写上一对圆括号。

没有定义返回值类型的函数会返回一个特殊的 Void 值,它其实是一个空的元组 (),没有任何元素。


使用元组可以让函数返回多个值。

// 函数的定义
func minAndMax(array: [Int]) -> (min: Int, max: Int)? {
    guard let firstNumber = array.first else { return nil }

    var currentMin = firstNumber
    var currentMax = firstNumber

    for number in array {
        if currentMin > number {
            currentMin = number
        } else if currentMax < number {
            currentMax = number
        }
    }

    return (currentMin, currentMax)
}



// 函数的调用
if let a = minAndMax(array: [1, 2, 3, 4]) {
    // 元组成员的名字已经在函数的返回值类型中定义过了
    a.min
    a.max
}

每个函数参数都有一个参数标签以及一个参数名称。参数标签是在调用函数的时候使用;参数名称在函数的实现中使用。默认情况下,参数名称就是参数标签。

// 参数标签就是参数名称
func addTwoNumbers(num1: Int, num2: Int) -> Int {
    return num1 + num2
}
addTwoNumbers(num1: 10, num2: 20)

// 指定参数标签(不再使用参数名称作为参数标签)
func addTwoNumbers(oneNumber num1: Int, anotherNumber num2: Int) -> Int {
    return num1 + num2
}
addTwoNumbers(oneNumber: 10, anotherNumber: 20) 

// 忽略参数标签
func addTwoNumbers(_ num1: Int, _ num2: Int) -> Int {
    return num1 + num2
}
addTwoNumbers(10, 20)

在函数体中通过给参数赋值可以为任意一个参数定义默认值。当默认值被定义后,调用这个函数时可以忽略这个参数。

func addTwoNumbers(num1: Int, num2: Int = 1) -> Int {
    return num1 + num2
}

addTwoNumbers(num1: 10)
addTwoNumbers(num1: 10, num2: 20)

如果一个函数有非默认值参数和默认值参数,一般来说,没有默认值的参数更加重要,因此,最好将不带默认值的参数放在参数列表的前面,可以使相同的函数在不同情况下调用时显得更为清晰。


通过在参数类型后加入 ... 的方式来定义可变参数。一个可变参数可以接受零个或多个值。
可变参数的传入值在函数体中变为此类型的一个数组。
一个函数最多只能拥有一个可变参数。

func sum(_ numbers: Int...) -> Int? {
    guard !numbers.isEmpty else { return nil }

    var result = 0
    for number in numbers {
        result += number
    }
    return result
}

let a = sum()  // nil
let b = sum(1, 2, 3)  // 6

函数的参数默认是常量,试图在函数体中更改参数值将会导致编译错误。如果你想要修改函数的参数值,并且想要让这些修改后的参数值在函数调用结束后仍然保留,那么就应该把这个参数定义为输入输出参数。

定义一个输入输出参数时,在参数类型前加 inout 关键字。

只能传递变量给输入输出参数,不能传入常量或者字面量,因为这些量是不能被修改的。在调用的时候,在参数名前加 & 符。

func swapTwoInts(a: inout Int, b: inout Int) {
    let temporaryA = a
    a = b
    b = temporaryA
}
var a = 10
var b = 20
swapTwoInts(a: &a, b: &b)

在Swift中,每个函数都有其特定的函数类型 – 由函数的参数值类型和返回值类型组成的类型。

func addTwoInts(_ a: Int, _ b: Int) -> Int {
    return a + b
}

func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
    return a * b
}

上面这两个函数的类型为 (Int, Int) -> Int

func printHelloWorld() {
    print("Hello World!")
}

上面这个函数的类型为 () -> Void


可以把函数类型当做普通的类型一样来处理,比如可以把一个函数当做其他函数的参数,也能当做其他函数的返回值等等。

func addTwoInts(_ a: Int, _ b: Int) -> Int {
    return a + b
}

func multiplyTwoInts(_ a: Int, _ b: Int) -> Int {
    return a * b
}
func zeroResult(_ a: Int, _ b: Int) -> Int {
    return 0
}

// 把函数类型当做普通类型
var mathFunction: (Int, Int) -> Int
mathFunction = addTwoInts
mathFunction(3, 5)  // 8
mathFunction = multiplyTwoInts
mathFunction(3, 5)  // 15  

// 把函数类型当做另一个函数的参数类型
func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
    print("Result: \(mathFunction(a, b))")
}
printMathResult(addTwoInts, 3, 5)  // Result: 8


// 把函数类型当做返回值类型
// 下面这个函数根据传入不同的字符串来返回不同功能的函数
func printMathResult(addOrMultipl: String) -> (Int, Int) -> Int {
    if addOrMultipl == "add" {
        return addTwoInts
    } else if addOrMultipl == "multiply" {
        return multiplyTwoInts
    } else {
        return zeroResult
    }
}

let add = printMathResult(addOrMultipl: "add")  // 获得具有加法功能的函数
let multiply = printMathResult(addOrMultipl: "multiply")  // 获得具有乘法功能的函数
let nothing = printMathResult(addOrMultipl: "nothing")  // // 获得一个无论传什么参数,都返回0的函数

add(3, 5)  // 8
multiply(3, 5)  // 15
nothing(3, 5)  // 0

Swift中的函数允许嵌套使用。


闭包


闭包是自包含的函数代码块,可以在代码中被传递和使用。Swift中的闭包与Objective-C中的 Block 以及其他编程语言中的匿名函数比较相似。


在Swift的代码中,闭包一般有如下三种形式:

  • 全局函数是一个有名字但不会捕捉任何值的闭包。
  • 嵌套函数是一个有名字并可以捕获其外围函数域内值的闭包。
  • 闭包表达式是一个利用轻量级语法所写的并且可以捕获其上下文中变量和常量值的匿名闭包。

闭包表达式的标准形式:

{ (parameters) -> returnType in 
    statement
}

闭包的函数体部分由关键字 in 引入,该关键字表示闭包的参数和返回值类型定义已经完成,闭包的函数体部分即将开始。


闭包表达式提供了一些语法优化,使得撰写闭包变得简单明了。下面通过对一个例子的几次迭代来展示闭包表达式的语法优化。

Swift的标准库提供了一个 sorted(by:) 函数,该函数接收一个闭包作为其参数,对数组中的值进行排序,最终返回一个排序好的新数组,原数组不会被修改。

sorted(by:) 函数接收一个 (Element, Element) -> Bool 类型的闭包,该闭包需要两个参数,并返回一个布尔值来表明第一个参数与第二个参数前后位置。

let names = ["Todd", "Chris", "Alex", "Barry", "Eric"]

// 提供一个符合sorted(by:)函数中闭包参数类型的普通函数
func backward(s1: String, s2: String) -> Bool {
    return s1 > s2
}
names.sorted(by: backward)

// 利用标准的闭包表达式语法构建一个内联排序闭包
var reversedNames01 = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

// 根据上下文推断闭包表达式的参数和返回值类型
// 因为排序闭包函数是作为sorted(by:)函数的参数传入的,因此,Swift可以推断其参数和返回值的类型。
// sorted(by:)函数被一个字符串数组调用,因此其参数必定是 (String, String) -> Bool 类型。
let reversedNames02 = names.sorted(by: { s1 , s2 in return s1 > s2 })

// 单行表达式闭包可以通过省略 return 关键字来隐式返回单行表达式的结果
let reversedNames03 = names.sorted(by: { s1 , s2 in s1 > s2 })

// Swift为内联闭包提供了参数名称缩写功能,你可以直接通过 $0,$1,$2 等来顺序调用闭包的参数
let reversedNames04 = names.sorted(by: { $0 > $1 })

// Swift的String类型定义了关于大于号( > )的字符串实现,其作为一个函数接受两个String类型的参数并返回Bool类型的值。
// 而这正好与sorted(by:)函数的参数需要的函数类型相符合。
let reversedNames05 = names.sorted(by: >)

// 如果你需要将一个很长的闭包表达式作为最后一个参数传递给函数,可以使用尾随闭包来增强函数的可读性。
// 尾随闭包是一个书写在函数括号之后的闭包表达式,函数支持将其作为最后一个参数调用。
let reversedNames06 = names.sorted() { $0 > $1 }

// 如果闭包表达式是函数的唯一参数,则当你使用尾随闭包时,还可以把 () 省略掉
let reversedNames07 = names.sorted { $0 > $1 }

闭包可以在其被定义的上下文中捕获常量和变量,即使这些常量和变量的原作用域已经不存在了,闭包仍然可以在函数体内引用和修改这些值。

在Swift中,可以捕获值的闭包的最简单的形式就是嵌套函数。嵌套函数可以捕获其外部函数的所有参数以及定义的常量和变量。

func makeIncrementer(amonut: Int) -> () -> Int {
    var totalNumber = 0
    func incrementer() -> Int {
        totalNumber += amonut
        return totalNumber
    }
    return incrementer
}

let incrementByTen = makeIncrementer(amonut: 10)
incrementByTen()  // 10
incrementByTen()  // 20

let incrementBySeven = makeIncrementer(amonut: 7)
incrementBySeven()  // 7
incrementBySeven()  // 14

函数和闭包都是引用类型。也就是说,无论你将函数或闭包赋值给一个常量还是变量,实际上你都是将常量或变量的值设置为对应函数或闭包的引用。

let incrementByTen = makeIncrementer(amonut: 10)
incrementByTen()  // 10
incrementByTen()  // 20

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()  // 30
incrementByTen()  // 40

当一个闭包作为参数传递到一个函数中,但是这个闭包在函数返回之后才被执行,我们称该闭包从函数中逃逸。当遇到这种情况时,可以在闭包参数名之前加上标注 @escaping,用来指明这个闭包是允许逃逸出这个函数的。

func doSomething(justPrintSomething: @escaping (String) -> Void) -> Void {
    print("在主线程中做一部分事情,准备开启一个子线程做其余事情")
    DispatchQueue.global().async {
        print("开启一个子线程做事情")
        DispatchQueue.main.async {
            print("做完事情回到主线程")
            print("执行闭包")
            justPrintSomething("数据")
        }
    }
}

doSomething { (resultString: String) in
    print("传递出来的结果:\(resultString)")
}

/*
在主线程中做一部分事情,准备开启一个子线程做其余事情
开启一个子线程做事情
做完事情回到主线程
执行闭包
传递出来的结果:数据
*/

将一个闭包标记为 @escaping 意味着你必须在闭包中显式地引用 self


@autoclosure 关键字用来修饰函数的闭包类型的参数,并且这种闭包的类型只能是 () -> T 这种类型。在调用的时候,闭包可以用对应的表达式来代替,Swift会自动把对应的表达式转化成闭包样式。这种便利语法让你能够省略闭包的花括号,用一个普通的表达式来代替显式的闭包。

// 普通函数
func logIfTrue(_ predicate: () -> Bool) {
    if predicate() {
        print("log something")
    }
}
logIfTrue{ 2 > 1 }

// 用 @autoclosure 修饰闭包参数的函数
func logIfTrue(_ predicate: @autoclosure () -> Bool) {
    if predicate() {
        print("log something")
    }
}
logIfTrue( 2 > 1 )  // Swift会自动把 2 > 1 这个表达式转化为 () -> Bool 类型的闭包

自动闭包 @autoclosure 最有用的地方就是延迟求值,因为直到你调用这个闭包,代码段才会被执行。延迟求值对于那些耗时操作或高计算成本的代码来说是很有益处的,因为它使得你能控制代码的执行时机。Swift标准库中的 ??&&|| 等操作符的实现就用到了这个特性。


枚举


枚举为一组相关的值定义了一个共同的类型,可以让你在代码中以类型安全的方式来使用这些值。


在Swift中,枚举是一等类型。

Swift中的枚举非常灵活,不必给每一个枚举成员提供一个值。如果想要给枚举成员提供一个值(原始值),则该值的类型可以是整型、浮点型、字符型和字符串型。

Swift中的枚举采用了很多只被类所支持的特性,比如计算属性,用来提供枚举值的附加信息;构造函数,用来提供一个初始值;实例方法,用来提供和枚举值相关联的功能;还可以遵守协议来提供标准的功能;还可以在原来实现的基础上扩展枚举的功能。


enum Direction {
    case north
    case south
    case east
    case west
}

var direction = Direction.north
direction = .east

Direction 枚举类型中定义的值 northsoutheastwest 是这个枚举的成员值。

与C和Objective-C不同的是,Swift中的枚举成员在被创建的时候没有默认的整型值,这些枚举的成员值本身就是完备的值,这些值的类型是已经明确定义好的 Direction 类型。

每个枚举定义了一个全新的类型,就像是Swift中的其他类型一样。枚举的名字应该以大写字母开头,且最好是单数形式。


使用 switch 语句来匹配单个枚举值。
在判断一个枚举类值时,switch 语句必须穷举所有情况。强制穷举能确保枚举成员不会被意外遗漏。

var direction = Direction.north

switch direction {
case .north:
    print("North")
case .south:
    print("South")
case .east:
    print("East")
case .west:
    print("West")
}

枚举成员可以指定任意类型的关联值存储在枚举成员中,且同一个枚举中不同的枚举成员可以有不同类型的关联值。

枚举关联值的主要作用是让你在存储成员值的同时还能存储额外的自定义信息,并且当你在代码中使用该枚举成员时,还可以修改这个关联值。

// 某个系统的设置信息
enum Setting {
    case string(String)
    case int(Int)
    case bool(Bool)
}  

// 商品的两种不同类型的条形码
enum Barcode { 
    case upc(Int, Int, Int, Int) 
    case qrCode(String)
}

var productBarcode = Barcode.upc(8, 85909, 51226, 3)
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

switch productBarcode { 
case let .upc(numberSystem, manufacturer, product, check): 
    print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case .qrCode(let productCode):
    print("QR code: \(productCode).") 
}

枚举成员可以被默认值(原始值)预填充,这些原始值的类型必须相同。

enum ASCIIControlCharacter: Character { 
    case tab = "\t" 
    case lineFeed = "\n" 
    case carriageReturn = "\r" 
}

原始值和关联值是不同的。原始值是在定义枚举时被预先填充的值,如上面三个字符类型的ASCII码。对于一个特定的枚举成员,它的原始值始终不变。关联值是在创建一个基于枚举成员的常量或变量时才设置的值,枚举成员的关联值是可以变化的。


原始值的隐式赋值:如果枚举类型在定义的时候被声明为整型和字符串类型时,不需要显示地为每一个枚举成员设置原始值,Swift会自动赋值。

// 不给成员值赋原始值(整型)
enum Month: Int {
    case January, February, March, April, May, June,
         July, August, September, October, November, December
}
let a: Month = .February
a.rawValue  // 1

// 只给第一个成员值赋原始值(整型)
enum Month: Int {
    case January = 1, February, March, April, May, June,
         July, August, September, October, November, December
}
let a: Month = .February
a.rawValue  // 2

// 不给成员值赋原始值(字符串)
// 字符串类型的隐式原始值为该枚举成员的名称
enum Month: String {
    case January, February, March, April, May, June,
         July, August, September, October, November, December
}
let a: Month = .February
a.rawValue  // February

// 给某个成员值赋原始值(字符串)
enum Month: String {
    case January = "Jan.", February, March, April, May, June,
    July, August, September, October, November, December
}

let a: Month = .January
a.rawValue  // Jan.

let b: Month = .February
b.rawValue  // February

使用原始值初始化枚举实例:如果在定义枚举类型的时候显示或者隐式地使用了原始值,那么Swift会自动为该枚举类型生成一个初始化方法,这个方法接受一个 rawValue 的参数,返回一个可选类型的枚举值。

enum Month: Int {
    case January, February, March, April, May, June,
         July, August, September, October, November, December
}
let a: Month? = Month(rawValue: 3)  // Optional(Month.April)

递归枚举是一种枚举类型,它有一个或多个成员使用该枚举类型本身的实例作为关联值。

在枚举成员前加上 indirect 来表示该成员可递归。

// 该枚举类型存储三种算术表达式:纯数字、两个表达式相加、两个表达式相乘。
enum ArithmeticExpression { 
    case number(Int) 
    indirect case addition(ArithmeticExpression, ArithmeticExpression) 
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression) 
}

// 表达式:(5 + 4) * 2
let five = ArithmeticExpression.number(5) 
let four = ArithmeticExpression.number(4) 
let two = ArithmeticExpression.number(2)
let sum = ArithmeticExpression.addition(five, four) 
let result = ArithmeticExpression.multiplication(sum, two) 

// 要操作具有递归性质的数据结构,使用递归函数是一种直截了当的方式。
func evaluate(_ expression: ArithmeticExpression) -> Int { 
    switch expression { 
    case let .number(value):
        return value 
    case let .addition(left, right):
        return evaluate(left) + evaluate(right) 
    case let .multiplication(left, right):
        return evaluate(left) * evaluate(right) 
    }
}

evaluate(result)  // 18

类和结构体


Swift中类和结构体有很多共同点:

  • 定义属性用于存储值
  • 定义方法用于提供功能
  • 定义下标操作使得可以通过下标语法来访问实例中的值
  • 定义构造器用于生成初始化值
  • 实现协议用于提供某种标准功能

与结构体相比,类还有如下附加功能:

  • 允许一个类继承另一个类
  • 类型转换允许在运行时检查和解释一个类实例的类型
  • 析构器允许一个类实例释放任何其分配的资源
  • 引用计数允许对一个类的多次引用

结构体总是通过被复制的方式在代码中传递,不使用引用计数。


每次定义一个新类或结构体的时候,实际上是定义了一个新的Swift类型。

类名使用 UpperCamelCase 方式。
属性和方法名使用 lowerCamelCase 方式。


// 定义结构体
struct Resolution {
    var width = 0
    var height = 0
}

// 定义类
class VideoMode {
    var resolution = Resolution()
    var interlaced = false
    var frameRate = 0.0
    var name: String?
}

// 生成一个结构体实例和一个类实例
let someResolution = Resolution()
let someVideoMode = VideoMode()

// 访问结构体和类的属性
someResolution.width
someVideoMode.resolution.width

// 给结构体和类的属性赋值
someResolution.width = 10
someVideoMode.resolution.width = 20

与Objective-C不同的是,Swift允许直接设置结构体属性的子属性。
在Objective-C中为什么不允许直接修改某个对象的结构体属性的成员


所有结构体都有一个自动生成的成员逐一构造器(memberwise initializer),用于初始化结构体实例中的成员属性。类实例没有默认的成员逐一构造器。

let vga = Resolution(width: 640, height: 480)

值类型被赋予给一个变量、常量或者被传递给一个函数的时候,其值会被拷贝。

在Swift中,所有的结构体和枚举类型都是值类型。

实际上,在Swift中,所有的基本类型:整型、浮点型、布尔类型、字符串、数组、字典等都是值类型,并且在底层都是以结构体的形式来实现的。


与值类型不同,引用类型在被赋予到一个变量、常量或者被传递给一个函数的时候,拷贝的是引用的指针,其值不会被拷贝。

类是引用类型,有可能有多个常量或变量同时引用同一个类实例。Swift提供两个恒等运算符(等价于 Identical to===、不等价于 Not identical to!==)来判定两个常量或者变量是否引用同一个类实例。


在Swift中,许多基本类型,诸如 StringArrayDictionary 类型均以结构体形式实现,因此,在代码中,拷贝行为看起来似乎总在发生。然而,Swift幕后只在绝对必要时才执行实际的拷贝,从而来确保性能最优化。


属性


存储属性(stored property)就是存储在特定类或结构体实例中的一个常量或变量。

可以在定义存储属性的时候指定默认值,也可以在构造函数中设置或修改存储属性的值。

// 用于描述整数范围的结构体,范围值在被创建后不能修改
struct FixedLengthRange {
    var firstValue: Int
    let length: Int
}

// 0, 1, 2
var rangeOfThreeItems = FixedLengthRange(firstValue: 0, length: 3)

// 6, 7, 8
rangeOfThreeItems.firstValue = 6

如果创建了一个结构体的实例并将其值赋值给一个常量,则无法修改实例中的任何属性值,不管那些属性在定义的时候被声明为了常量还是变量。

// 0, 1, 2, 3
let rangeOfFourItems = FixedLengthRange(firstValue: 0, length: 4)
rangeOfFourItems.firstValue = 6  // 报错

这是由于结构体属于值类型的原因,当值类型的实例被声明为常量时,它的所有属性也就成了常量。


延迟存储属性(懒加载存储属性 lazy stored property)是指第一次被调用的时候才会计算其初始值的属性。在属性声明前使用 lazy 来标示一个延迟存储属性。

必须将延迟存储属性声明成变量,因为属性的初始值可能在实例构造完成之后才会得到。而常量属性在构造完成之前必须要有值。

延迟存储属性可应用在当属性值在实例构造过程结束后才能知道,或者属性的初始值需要经过复杂的大量计算时。

如果延迟存储属性在没有被初始化就同时被多个线程访问,则无法保证该属性只会被初始化一次。


类、结构体和枚举还可以定义计算属性(computed property)。计算属性不直接存储值,而是提供一个 getter 来获取值和一个可选的 setter 来间接设置其他属性的值。

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

struct Size {
    var width = 0.0, height = 0.0
}

struct Rect {
    var origin = Point()
    var size = Size()
    var center: Point {
        get {
            let centerX = origin.x + (size.width / 2)
            let centerY = origin.y + (size.height / 2)
            return Point(x: centerX, y: centerY)
        }
        set(newCenter) {
            origin.x = newCenter.x - (size.width / 2)
            origin.y = newCenter.y - (size.height / 2)
        }
    }
}
var square = Rect(origin: Point(x: 0.0, y: 0.0), size: Size(width: 10.0, height: 10.0))
square.center
square.center = Point(x: 15.0, y: 15.0)

如果计算属性的 setter 没有定义表示新值的参数值,则可以使用默认名称 newValue

只有 getter 没有 setter 的计算属性就是只读计算属性。

计算属性只能是变量,包括只读计算属性,因为他们的值是不固定的。

只读计算属性的声明可以去掉 get 关键字和花括号:

struct Rect {
    var origin = Point()
    var size = Size()
    var center: Point {
        let centerX = origin.x + (size.width / 2)
        let centerY = origin.y + (size.height / 2)
        return Point(x: centerX, y: centerY)
    }
}

属性观察器(property observer)监控和响应属性值的变化,每次属性被设置新值的时候都会调用属性观察器,即使新值和当前值相同也不例外。

属性观察器:

  • willSet:在新的值被设置之前调用。
  • didSet:在新的值被设置之后调用。

willSet 观察器会将新的属性值作为常量参数传入,在 willSet 的实现代码中可以为这个参数指定一个名称,如果不指定默认使用 newValue 来表示。
didSet 观察器会将旧的属性值作为常量参数传入,在 didSet 的实现代码中可以为这个参数指定一个名称,如果不指定默认使用 oldValue 来表示。

class StepCounter {
    // 统计一个人步行的总步数(当每次总步数更新时,就打印总步数以及每次增加的步数)
    var totalSteps: Int = 0 {
        willSet(newTotalSteps) {
            print("Total steps: \(newTotalSteps)")
        }
        didSet {
            if totalSteps > oldValue {
                print("Added steps: \(totalSteps - oldValue)")
            }
        }
    }
}
let stepCounter = StepCounter()
stepCounter.totalSteps = 200
// Total steps: 200
// Added steps: 200

stepCounter.totalSteps = 360
// Total steps: 360
// Added steps: 160

父类的属性在子类的构造器中被赋值时,它在父类中的 willSetdidSet 观察器会被调用,随后才会调用子类的观察器。


全局变量是在函数、方法、闭包或任何类型之外的变量。
局部变量是在函数、方法或闭包内部定义的变量。
全局的常量或变量都是延迟计算的,不需要 lazy 修饰。
局部范围的常量和变量从不延迟计算。

计算属性和属性观察器所具有的功能也可以用于全局变量和局部变量。
全局或局部变量都属于存储型变量,跟存储属性类似,它为特定类型的值提供存储空间,并允许读取和写入。
在全局或局部范围都可以定义计算型变量和为存储型变量定义观察器。
计算型变量跟计算属性一样,返回一个计算结果而不是存储值。


可以为类型本身定义属性,无论创建了多少个该类型的实例,这些属性都只有唯一的一份,这种属性就是类型属性(Type Property)。
类型属性用于定义某个类型所有实例共享的数据,比如所有实例都能用的一个常量或变量。

跟实例的存储属性不同,存储型类型属性必须有默认值,因为类型本身没有构造器,也就无法在初始化过程中使用构造器给类型属性赋值。
存储型类型属性是延迟初始化的,它们只有在第一次被访问的时候才被初始化。即使它们被多个线程同时访问,系统也保证只会对其进行一次初始化,不需要 lazy 修饰符。


在Swift中,类型属性是作为类型定义的一部分写在类型最外层的花括号内。

使用关键字 static 来定义类型属性。在为类定义计算型类型属性时,可以改用关键字 class 来支持子类对父类的实现进行重写。

// 结构体中的存储型类型属性和计算型类型属性
struct SomeStructure {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 1
    }
}

// 枚举中的存储型类型属性和计算型类型属性
enum SomeEnumeration {
    static var storedTypeProperty = "Some value."
    static var computedTypeProperty: Int {
        return 6
    }
}

// 类中的存储型类型属性和计算型类型属性
class SomeClass: SuperSomeClass {
    // 本类中的存储型类型属性
    static var storedTypeProperty = "Some value."

    // 本类中的计算型类型属性
    static var computedTypeProperty: Int {
        return 27
    }

    // 对父类重写的计算型类型属性
    class var overrideableComputedTypeProperty: Int {
        return 107
    }
}

类型属性是通过类型本身来访问的,而不是通过实例来访问的。

SomeStructure.computedTypeProperty  // 1
SomeStructure.storedTypeProperty = "Another value."

SomeEnumeration.computedTypeProperty  // 6
SomeEnumeration.storedTypeProperty = "Another value."

SomeClass.computedTypeProperty  // 27
SomeClass.overrideableComputedTypeProperty  107
SomeClass.storedTypeProperty = "Another value."

方法


类、结构体、枚举都可以定义实例方法,实例方法是为给定类型的实例封装一些具体的功能。
类、结构体、枚举还可以定义类型方法,类型方法与类型本身关联。

结构体和枚举能够定义方法是Swift和Objective-C的主要区别之一。
在Objective-C中,类是唯一能定义方法的类型。


类型的每一个实例都有一个隐含的属性叫做 selfself 完全等同于该实例本身。
实际上,在代码中没必要经常写 self,一般情况下,只要在一个方法中使用了一个已知的属性或者方法名称,如果没有明确写 self,Swift默认指的就是当前实例的属性和方法。

如果实例方法的某个参数名称与实例的某个属性名称相同的时候,可以使用 self 属性来区分参数名称和属性名称。


结构体和枚举是值类型,默认情况下,值类型的属性不能在它的实例方法中被修改。但是,如果确实需要在某个特定的方法中修改结构体或者枚举中的属性,可以把这个方法声明为可变 mutating 方法。

struct Point {
    var x = 0.0, y = 0.0
    mutating func move(x: Double, y: Double) {
        self.x = self.x + x
        self.y = self.y + y
    }
}

var somePoint = Point(x: 10, y: 20)
somePoint.move(x: 10, y: 20)
somePoint.x  // 20
somePoint.y  // 40

let fixedPoint = Point(x: 10, y: 20)
fixedPoint.move(x: 10, y: 20)  // 报错

不能在结构体类型的常量上调用可变方法,因为其属性不能被改动,哪怕属性是变量属性。


可变方法还能够给隐含的 self 属性赋值一个全新的实例。

struct Point {
    var x = 0.0, y = 0.0
    mutating func move(x: Double, y: Double) {
        self = Point(x: self.x + x, y: self.y + y)
    }
}

枚举的可变方法可以把 self 设置为同一枚举类型的不同成员。

enum LightState {
    case Off, Low, High

    mutating func next() {
        switch self {
        case .Off:
            self = .Low
        case .Low:
            self = .High
        case .High:
            self = .Off
        }
    }
}

var light = LightState.Low
light.next() // LightState.High
light.next() // LightState.Off

定义在类型本身上的方法叫做类型方法(type method)。在方法的 func 关键字前加上 static 关键字来指定类型方法,还可以用关键字 class 来允许子类重写父类的方法实现。

类型方法是在类型上调用的方法,而不能在实例上调用。

class SomeClass {
    static func someTypeMethod() {
    }
}

SomeClass.someTypeMethod()

在类型方法的方法体中,self 指向这个类型本身,而不是类型的某个实例。这意味着你可以用 self 来消除类型属性和类型方法参数之间的歧义。


下标


下标可以定义在类、结构体和枚举中,是访问集合、列表或序列中元素的快捷方式。可以使用下标的索引来设置和获取值,而不再需要调用对应的存取方法。

一个类型可以定义多个下标,通过不同的索引类型进行重载。


下标允许你通过在实例名称后面的方括号中传入一个或多个索引值来对实例进行存取。
定义下标使用 subscript 关键字,指定一个或多个参数和返回值类型。
下标可以设定为读写或只读,这种行为由 gettersetter 来实现。

subscript(index: Int) -> Int {
    get {
        return 一个Int值
    }
    set {
        执行适当的赋值操作
    }
}

只读下标。

struct TimesTable {
    let multiplier: Int
    subscript(index: Int) -> Int {
        return multiplier * index
    }
}

// threeTimesTable表示整数3的乘法表
let threeTimesTable = TimesTable(multiplier: 3)
threeTimesTable[6]  // 18

下标可以接受任意数量任意类型的参数,也可以返回任意类型的返回值。

一个类或结构体可以根据自身的需要提供多个下标实现,使用下标时通过参数的数量和类型进行区分,自动匹配合适的下标,这叫做下标的重载。

// Matrix用来表示一个Double类型的二维数组(矩阵)
struct Matrix {
    let rows: Int, columns: Int
    var grid: [Double]
    init(rows: Int, columns: Int) {
        self.rows = rows
        self.columns = columns
        grid = Array(repeating: 0.0, count: rows * columns)
    }

    // 判断下标值是否越界
    func indexIsValidForRow(row: Int, column: Int) -> Bool {
        return row >= 0 && row < rows && column >= 0 && column < columns
    }

    subscript(row: Int, column: Int) -> Double {
        get {
            assert(indexIsValidForRow(row: row, column: column), "Index out of range")
            return grid[(row * columns) + column]
        }
        set {
            assert(indexIsValidForRow(row: row, column: column), "Index out of range")
            grid[(row * columns) + column] = newValue
        }
    }
}

// 创建一个2*2的矩阵
var matrix = Matrix(rows: 2, columns: 2)

// 将矩阵右上角和左下角位置的值设置为1.5和3.2
matrix[0, 1] = 1.5
matrix[1, 0] = 3.2

继承


一个类可以继承另一个类的方法、属性和其他特性。当一个类继承其他类时,继承类叫子类,被继承类叫超类或父类。
在Swift中,继承是区分类与其他类型的一个基本特征。

类可以访问超类的方法、属性和下标,并且可以重写这些方法、属性和下标来优化或者修改它们的行为。

可以在类中对继承来的属性添加属性观察器,这样当属性值改变时,类就会被通知到。


不继承于其他类的类称之为基类。

// 定义了一个Vehicle的基类
class Vehicle {
    var currentSpeed = 0.0

    var description: String {
        return "traveling at \(currentSpeed) miles per hour"
    }

    func makeNoise() {
        // 什么也不做-因为车辆不一定会有噪音 
    }
}

let someVehicle = Vehicle()
someVehicle.currentSpeed = 80
someVehicle.description  // traveling at 80.0 miles per hour

子类生成指的是在一个已有类的基础上创建一个新类。

class Bicycle: Vehicle {
    var hasBasket = false
}

let bicycle = Bicycle()
bicycle.hasBasket = true
bicycle.currentSpeed = 15.0
bicycle.description  // traveling at 15.0 miles per hour

子类可以为继承来的实例方法、类方法、实例属性、下标提供自己定制的实现,这种行为叫做重写。

如果要重写某个特性,需要在重写定义的前面加上 override 关键字。

当在子类中重写父类的方法、属性和下标时,有时在你的重写版本中使用已经存在的父类实现会大有裨益。可以通过使用 super 前缀来访问父类的方法、属性和下标。

  • 在方法 someMethod() 的重写实现中使用 super.someMethod() 来调用父类的 someMethod() 方法。
  • 在属性 somePropertygettersetter 的重写实现中,可以通过 super.someProperty 来访问父类的 someProperty 属性。
  • 在下标的重写实现中,可以通过 super[someIndex] 来访问父类中的相同下标。

重写方法。

class Train: Vehicle {
    override func makeNoise() {
        print("Choo Choo")
    }
}
let train = Train()
train.makeNoise()  // Choo Choo

重写属性。
你可以提供定制的 getter(或 setter)来重写任意继承来的属性,无论继承来的属性是存储型属性还是计算型属性。子类并不知道继承来的属性是存储型的还是计算型的,它只知道继承来的属性会有一个名字和类型。
可以将一个继承来的只读属性重写为一个读写属性,只需要在重写版本的属性里提供 gettersetter 即可。但是,不可以将一个继承来的读写属性重写为一个只读属性。

class Car: Vehicle {
    var gear = 1
    override var description: String {
        return super.description + " in gear \(gear)"
    }
}
let car = Car()
car.currentSpeed = 25.0
car.gear = 3
car.description  // traveling at 25.0 miles per hour in gear 3

重写观察属性。
可以通过重写属性为一个继承来的属性添加属性观察器。这样当继承来的属性值发生改变时就会被通知到,无论那个属性原本是如何实现的。
不可以为继承来的常量存储型属性或继承来的只读计算型属性添加属性观察器。这些属性的值是不可以被设置 的。
不可以同时提供重写的 setter 和重写的属性观察器。如果想观察属性值的变化,并且已经为属性提供了定制的 setter,那么在 setter 中就可以观察到任何值变化了。

class AutomaticCar: Car {
    override var currentSpeed: Double {
        didSet {
            gear = Int(currentSpeed / 10.0) + 1
        }
    }
}

let automatic = AutomaticCar()
automatic.currentSpeed = 35.0
automatic.description  // traveling at 35.0 miles per hour in gear 4

可以通过把方法、属性或下标标记为 final 来防止它们被子类重写,如:final varfinal funcfinal class funcfinal subscript)。

如果重写了带有 final 标记的方法、属性或下标,在编译时会报错。

在关键字 class 前添加 final 修饰符来将整个类标记为 final,这样的类是不可被继承的。


构造过程


构造过程是使用类、结构体或枚举类型的实例之前的准备过程。具体操作包括设置实例中每个存储型属性的初始值和执行其他必须的设置或初始化工作。

类和结构体在创建实例时,必须为所有存储型属性设置合适的初始值。可以在构造器中为存储型属性赋初值,也可以在定义属性时为其设置默认值。

当为存储型属性设置默认值或者在构造器中为其赋值时,它们的值是被直接设置的,不会触发任何属性观察器。


自定义构造过程。

struct Color {
    let red, green, blue: Double  // 常量属性

    init(red: Double, green: Double, blue: Double) {
        self.red = red
        self.green = green
        self.blue = blue
    }

    init(white: Double) {
        red = white
        green = white
        blue = white
    }
}

let magenta = Color(red: 1.0, green: 0.0, blue: 1.0) 
let halfGray = Color(white: 0.5)

如果结构体或类的所有属性都有默认值,同时没有自定义的构造器,那么Swift会给这些结构体或类提供一个默认构造器。这个默认构造器将简单地创建一个所有属性值都设置为默认值的实例。

如果结构体没有提供自定义的构造器,除了默认构造器,Swift还为其提供了一个逐一成员构造器。


构造器可以通过调用其它构造器来完成实例的部分构造过程。这一过程称为构造器代理,它能减少多个构造器间的代码重复。

构造器代理的实现规则和形式在值类型和类类型中有所不同。值类型(结构体和枚举类型)不支持继承,所以它们的构造器代理过程相对简单,因为它们只能代理给自己的其它构造器。类则不同,它可以继承自其它类,这意味着类有责任保证其所有继承的存储型属性在构造时也能正确的被初始化。


对于值类型,可以使用 self.init 在自定义的构造器中调用相同类型中的其他构造器。

如果为某个值类型定义了一个自定义的构造器,将无法访问到默认构造器(如果是结构体,还将无法访问到逐 一成员构造器)。这种限制可以防止为值类型增加了一个额外的且十分复杂的构造器之后,仍然会错误的使用自动生成的构造器。

假如希望默认构造器、逐一成员构造器以及自定义的构造器都能用来创建实例,可以将自定义的构造器写到扩展 extension 中,而不是写在值类型的原始定义中。

struct Size {
    var width = 0.0, height = 0.0
}

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

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

    init() {}

    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)
    }
}

类里面的所有存储型属性(包括所有继承自父类的属性)都必须在构造过程中设置初始值。

Swift为类类型提供了两种构造器来确保实例中的所有存储型属性都能获得初始值,它们分别是指定构造器和便利构造器。


指定构造器是类中最主要的构造器。一个指定构造器将初始化类中提供的所有属性,并根据父类链往上调用父类的构造器来实现父类的初始化。

每一个类都必须拥有至少一个指定构造器。在某些情况下,类通过继承了父类中的指定构造器而满足了这个条件。

init(parameters) {
    statements 
}

便利构造器是类中比较次要的、辅助型的构造器。可以定义便利构造器来调用同一个类中的指定构造器,并为 其参数提供默认值。也可以定义便利构造器来创建一个特殊用途或特定输入值的实例。

convenience init(parameters) {
    statements 
}

为了简化指定构造器和便利构造器之间的调用关系,Swift采用了以下三条规则来限制构造器之间的代理调用:

  • 指定构造器必须调用其直接父类的的指定构造器。
  • 便利构造器必须调用同类中定义的其它构造器。
  • 便利构造器必须最终导致一个指定构造器被调用。

一个更方便记忆的方法是:

  • 指定构造器必须总是向上代理。
  • 便利构造器必须总是横向代理。

Swift中类的构造过程包含两个阶段:第一个阶段,每个存储型属性被引入它们的类指定一个初始值,当每个存储型属性的初始值被确定后,第二阶段开始,它给每个类一次机会,在新实例准备使用之前进一步定制它们的存储型属性。

Swift编译器将执行四种有效的安全检查,以确保两段式构造过程能不出错地完成:

  • 指定构造器必须保证它所在类引入的所有属性先初始化完成,之后才能将其它构造任务向上代理给父类中 的构造器。
  • 指定构造器必须先向上代理调用父类构造器,然后再为继承的属性设置新值。如果没这么做,指定构造器赋予的新值将被父类中的构造器所覆盖。
  • 便利构造器必须先代理调用同一类中的其它构造器,然后再为任意属性赋新值。如果没这么做,便利构造器赋予的新值将被同一类中其它指定构造器所覆盖。
  • 构造器在第一阶段构造完成之前,不能调用任何实例方法,不能读取任何实例属性的值,不能引用 self 作为一个值。

两段式构造过程的具体流程如下:

阶段一:

  • 某个指定构造器或便利构造器被调用。
  • 完成新实例内存的分配,但此时内存还没有被初始化。
  • 指定构造器确保其所在类引入的所有存储型属性都已赋初值。存储型属性所属的内存完成初始化。
  • 指定构造器将调用父类的构造器,完成父类属性的初始化。
  • 这个调用父类构造器的过程沿着构造器链一直往上执行,直到到达构造器链的最顶部。
  • 当到达了构造器链最顶部,且已确保所有实例包含的存储型属性都已经赋值,这个实例的内存被认为已经完全初始化。此时阶段一完成。

阶段二:

  • 从顶部构造器链一直往下,每个构造器链中类的指定构造器都有机会进一步定制实例。构造器此时可以访问 self、修改它的属性并调用实例方法等等。
  • 最终,任意构造器链中的便利构造器可以有机会定制实例和使用 self

Swift中的子类默认情况下不会继承父类的构造器。如果希望自定义的子类中能提供一个或多个跟父类相同的构造器,可以在子类中提供这些构造器的自定义实现。

当你在编写一个和父类中指定构造器相匹配的子类构造器时,实际上是在重写父类的这个指定构造器。因 此,在定义子类构造器时必须带上 override 修饰符。哪怕是重写了系统自动提供的默认构造器,也需要带上 override 修饰符。

相反,如果你编写了一个和父类便利构造器相匹配的子类构造器,由于子类不能直接调用父类的便利构造器,因此,子类并未对一个父类构造器提供重写。最后的结果就是,当在子类中“重写”了一个父类便利构造器时,不需要加 override 前缀。


虽然子类在默认情况下不会继承父类的构造器,但是如果满足特定条件,父类构造器是可以被自动继承的。

假如子类中引入的所有新属性都提供了默认值,那么Swift提供了以下两条便利规则:

  • 如果子类没有定义任何指定构造器,它将自动继承所有父类的指定构造器。
  • 如果子类提供了所有父类指定构造器的实现(无论是通过规则一继承过来的,还是提供了自定义实现),它将自动继承所有父类的便利构造器。

以下例子展示了指定构造器、便利构造器以及构造器的自动继承。

// 基类
class Food {
    var name: String

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

    convenience init() {
        self.init(name: "[Unnamed]")
    }
}
let mysteryMeat = Food()

class RecipeIngredient: Food {
    var quantity: Int

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

    // 便利构造器重写了父类的指定构造器,因此必须在前面使用override修饰符
    override convenience init(name: String) {
        self.init(name: name, quantity: 1)
    }
}
let oneMysteryItem = RecipeIngredient() 
let oneBacon = RecipeIngredient(name: "Bacon") 
let sixEggs = RecipeIngredient(name: "Eggs", quantity: 6)

// 由于ShoppingListItem类为自己引入的所有属性都提供了默认值,并且自己没有定义任何构造器
// ShoppingListItem将自动继承所有父类中的指定构造器和便利构造器。
class ShoppingListItem: RecipeIngredient {
    var purchased = false
    var description: String {
        var output = "\(quantity) x \(name)"
        output += purchased ? " ?" : " ?"
        return output
    }
}
ShoppingListItem()
ShoppingListItem(name: "Bacon")
ShoppingListItem(name: "Eggs", quantity: 6)

如果一个类、结构体或枚举类型的对象,在构造过程中有可能失败,则为其定义一个可失败构造器。比如给构造器传入无效的参数值,缺少某种所需的外部资源,又或是不满足某种必要的条件等。

为了妥善处理这种构造过程中可能会失败的情况,可以在一个类、结构体或是枚举类型的定义中,添加一个或 多个可失败构造器。其语法为在 init 关键字后面添加问号 init?

可失败构造器的参数名和参数类型,不能与其它非可失败构造器的参数名和参数类型相同。

可失败构造器会创建一个类型为自身类型的可选类型的对象。通过 return nil 语句来表明可失败构造器的初始化失败。
严格来说,构造器都不支持返回值。因为构造器本身的作用,只是为了确保对象能被正确构造。因此只能使用用 return nil 表明可失败构造器构造失败,而不能用关键字 return 来表明构造成功。

// 可失败构造器检查传入的参数是否为一个空字符串
// 如果为空字符串,则构造失败
// 否则,species属性被赋值,构造成功
struct Animal {
    let species: String
    init?(species: String) {
        if species.isEmpty { return nil }
        self.species = species
    }
}

let someCreature = Animal(species: "Giraffe") 
// someCreature的类型是 Animal? 而不是 Animal

通过生成一个或多个参数的可失败构造器来获取枚举类型中特定的枚举成员。如果提供的参数无法匹配任何枚举成员,则构造失败。

enum TemperatureUnit {
    case Kelvin, Celsius, Fahrenheit

    init?(symbol: Character) {
        switch symbol {
        case "K":
            self = .Kelvin
        case "C":
            self = .Celsius
        case "F":
            self = .Fahrenheit
        default:
            return nil
        }
    }
}

let fahrenheitUnit = TemperatureUnit(symbol: "F")
// fahrenheitUnit类型为 TemperatureUnit?

带原始值的枚举类型会自带一个可失败构造器 init?(rawValue:),该可失败构造器有一个名为 rawValue 的参数,如果该参数的值能够和某个枚举成员的原始值匹配,则该构造器会构造相应的枚举成员,否则构造失败。

enum TemperatureUnit: Character {
    case Kelvin = "K", Celsius = "C", Fahrenheit = "F" 
}

let fahrenheitUnit = TemperatureUnit(rawValue: "F")
// fahrenheitUnit类型为 TemperatureUnit?

类、结构体、枚举的可失败构造器可以横向代理到类型中的其他可失败构造器。类似的,子类的可失败构造器也能向上代理到父类的可失败构造器。
无论是向上代理还是横向代理,如果代理到的其他可失败构造器触发了构造失败,整个构造过程将立即终止,接下来的任何构造代码不会再被执行。

class Product {
    let name: String

    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

class CartItem: Product {
    let quantity: Int

    init?(name: String, quantity: Int) {
        if quantity < 1 { return nil }
        self.quantity = quantity
        super.init(name: name)
    }
}

let twoSocks = CartItem(name: "sock", quantity: 2)
let zeroShirts = CartItem(name: "shirt", quantity: 0)  // nil
let oneUnnamed = CartItem(name: "", quantity: 1)  // nil

如同其它的构造器,可以在子类中重写父类的可失败构造器,也可以用子类的非可失败构造器重写父类的可失败构造器。这样就可以定义一个不会构造失败的子类,即使父类的构造器允许构造失败。

当子类的非可失败构造器重写父类的可失败构造器时,向上代理到父类的可失败构造器的唯一方式是对父类的可失败构造器的返回值进行强制解包。

可以用非可失败构造器重写可失败构造器,但反过来却不行。

class Document {
    var name: String?

    // 该构造器创建了一个 name 属性值为 nil 的 document 实例
    init() {}

    // 该构造器创建了一个 name 属性值为非空字符串的 document 实例
    init?(name: String) {
        self.name = name
        if name.isEmpty { return nil }
    }
}

class AutomaticallyNamedDocument: Document {
    override init() {
        super.init()
        self.name = "[Untitled]"
    }

    override init(name: String) {
        super.init()
        if name.isEmpty {
            self.name = "[Untitled]"
        } else {
            self.name = name
        }
    }
}

// 在子类的非可失败构造器中使用强制解包来调用父类的可失败构造器
class UntitledDocument: Document { 
    override init() { 
        super.init(name: "[Untitled]")! 
    } 
}

通常在 init 关键字后添加问号的方式 init? 来定义一个可失败构造器,也可以通过在 init 后面添加惊叹号的方式来定义一个可失败构造器 init! ,该可失败构造器将会构建一个对应类型的隐式解包可选类型的对象。


在类的构造器前添加 required 修饰符表明所有该类的子类都必须实现该构造器。

在子类重写父类的必要构造器时,必须在子类的构造器前也添加 required 修饰符,表明该构造器也要求应用于继承链后面的子类。在重写父类中必要的指定构造器时,不需要添加 override 修饰符。

class SomeClass {
    required init() {
        // 构造器的实现代码
    }
}

class SomeSubclass: SomeClass {
    required init() {
        // 构造器的实现代码
    }
}

如果某个存储型属性的默认值需要一些定制或设置,可以使用闭包或全局函数为其提供定制的默认值。每当某 个属性所在类型的新实例被创建时,对应的闭包或函数会被调用,而它们的返回值会当做默认值赋值给这个属 性。

class SomeClass {
    let someProperty: SomeType = {
        // 在这个闭包中给 someProperty 创建一个默认值 
        // someValue 必须和 SomeType 类型相同 
        return someValue
    }()
}

注意到闭包结尾的大括号后面接了一对空的小括号,这是用来告诉Swift立即执行此闭包。如果忽略了这对括号,相当于将闭包本身作为值赋值给了属性,而不是将闭包的返回值赋值给属性。

还需要注意的是如果使用闭包来初始化属性,在闭包执行时,实例的其它部分都还没有初始化。这意味着你不能在闭包里访问其它属性,即使这些属性有默认值。同样,你也不能使用隐式的 self 属性,或者调用任何实例方法。


析构过程


Swift通过自动引用计数来处理实例的内存管理,自动释放不再需要的实例以释放资源。

在类的定义中,每个类最多只能有一个析构器,而且析构器不带任何参数。

析构器是在实例释放发生前被自动调用的,不能主动调用析构器。如果子类继承了父类的析构器,并且在子类析构器实现的最后,父类的析构器会被自动调用,即使子类没有提供自己的析构器,父类的析构器也同样会被调用。因为直到实例的析构器被调用后,实例才会被释放。


自动引用计数


Swift使用自动引用计数机制来跟踪和管理应用程序的内存。通常情况下,Swift内存管理机制会一直起作用,你无须自己来考虑内存的管理。ARC会在类的实例不再被使用时,自动释放其占用的内存。

引用计数仅仅应用于类的实例。结构体和枚举类型是值类型,不是引用类型,也不是通过引用的方式存储和传递的。


每次创建一个新的类实例的时候,ARC会分配一块内存来储存该实例信息。内存中包含实例的类型信息以及这个实例所有相关的存储型属性的值。
此外,当实例不再被使用时,ARC就会释放实例所占用的内存,并让释放的内存能挪作他用,以确保不再被使用的实例不会一直占用内存空间。

当ARC释放了正在被使用中的实例时,该实例的属性和方法将不能被访问和调用。
为了确保使用中的实例不会被销毁,ARC会跟踪和计算每一个实例正在被多少属性、常量和变量所引用。哪怕实例的引用数为1,ARC都不会销毁这个实例。

为了达到上述的功能,无论你将实例赋值给属性、常量或变量,它们都会创建此实例的强引用。之所以称之 为“强”引用,是因为它会将实例牢牢地保持住,只要强引用还在,实例是不允许被销毁的。


在实际代码中,我们有可能会写出一个类实例的强引用数永远不能变成0的代码。如果两个类实例互相持有对方的强引用,就会产生所谓的循环强引用。
Swift提供了两种办法用来解决在使用类的属性时所遇到的循环强引用问题:弱引用(weak reference)和无主引用(unowned reference)。


声明属性或者变量时,在前面加上 weak 关键字表明这是一个弱引用。

由于弱引用不会保持所引用的实例,即使引用存在,实例也有可能被销毁。因此,ARC会在引用的实例被销毁后自动将其赋值为 nil。并且因为弱引用可以允许它们的值在运行时被赋值为 nil,所以它们会被定义为可选类型的变量,而不是常量。


和弱引用类似,无主引用不会保持住引用的实例。而和弱引用不同的是,无主引用最好应用在其他实例有相同或者更长的生命周期。你可以在声明属性或者变量时,在前面加上关键字 unowned 表示这是一个无主引用。

无主引用通常都被期望拥有值。不过ARC无法在实例被销毁后将无主引用设为 nil,因为非可选类型的变量不允许被赋值为 nil

使用无主引用,必须确保引用始终指向一个未销毁的实例。如果试图在实例被销毁后,访问该实例的无主引用,会触发运行时错误。


循环强引用还会发生在当你将一个闭包赋值给类实例的某个属性,并且这个闭包体中又使用了这个类实例时,比如在闭包体中访问了实例的某个属性 self.someProperty,或者在闭包体中调用了实例的某个方法 self.someMethod(),这两种情况都将导致了闭包捕获 self,从而产生了循环强引用。

循环强引用的产生,是因为闭包和类相似,都是引用类型。当把一个闭包赋值给某个属性时,其实是将这个闭包的引用赋值给了属性。从本质上来说,这跟之前的问题是一样的(两个强引用让彼此一直有效),和两个类实例相互引用不同,这种情况是一个是类实例,另一个是闭包。

在定义闭包时同时定义捕获列表作为闭包的一部分,通过这种方式可以解决闭包和类实例之间的循环强引用。捕获列表定义了闭包体内捕获一个或者多个引用类型的规则。跟解决两个类实例间的循环强引用一样,声明每个捕获的引用为弱引用或无主引用,而不是强引用。

Swift有如下要求:只要在闭包内使用了 self 的成员,就要用 self.someProperty 或者 self.someMethod() 这种写法,不能使用 somePropertysomeMethod() 这种写法,这能让你清楚的知道是否捕获了 self


捕获列表中的每一项都由一对元素组成,一个元素是 weakunowned 关键字,另一个元素是类实例的引用如 self 或初始化过的变量如 delegate = self.delegate!

lazy var someClosure: (Int, String) -> String = {
    [unowned self, weak delegate = self.delegate!] in
    // 这里是闭包的函数体
}()

弱引用总是可选类型,并且当引用的实例被销毁后,弱引用的值会自动置为 nil,我们需要在闭包体内检查它是否存在。


可选链式调用


可选链式调用是一种可以在当前值可能为 nil 的可选值上请求和调用属性、方法及下标的方式。如果可选值有值,那么调用就会成功;如果可选值是 nil,那么调用将返回 nil。多个调用可以连接在一起形成一个调用链,如果其中任何一个节点为 nil,整个调用链都会失败,即返回 nil

Swift中的可选链式调用和Objective-C中向 nil 发送消息有些相像,但是Swift的可选链式调用可以应用于任意类型,并且能检查调用是否成功。


为了让可选链式调用可以在空值 nil 上起作用,不论这个调用的属性、方法及下标的返回值是不是可选值,它的返回结果都是一个可选值。因此,就可以利用这个返回值来判断可选链式调用是否调用成功,如果有返回值则说明调用成功,返回 nil 则说明调用失败。

可选链式调用的返回结果与原本的返回结果具有相同的类型,但是被包装成了一个可选值。例如,使用可选链式调用访问属性,当可选链式调用成功时,如果属性原本的返回结果是 Int 类型,则会变为 Int? 类型。

class Person {
    var residence: Residence?
}

class Residence {
    var numberOfRooms = 1
}

let john = Person()
john.residence!.numberOfRooms  // 运行时错误
john.residence?.numberOfRooms  // 返回值类型为 Int?

可以通过连接多个可选链式调用在更深的模型层级中访问属性、方法以及下标。然而,多层可选链式调用并不会增加返回值的可选层级。

也就是说:

  • 如果访问的值不是可选的,可选链式调用将会返回可选值。
  • 如果访问的值就是可选的,可选链式调用不会让可选返回值变得“更可选”。

比如:

  • 通过可选链式调用访问一个 Int 值,将会返回 Int?,无论使用了多少层可选链式调用。
  • 类似的,通过可选链式调用访问 Int? 值,依旧会返回 Int? 值,并不会返回 Int??

除了上面在一个可选值上通过可选链式调用来获取属性值,还可以在一个可选值上通过可选链式调用来调用方法,并且可以根据需要继续在方法的可选返回值上进行可选链式调用。
在方法后加上问号来实现可选链式调用的本质是在方法的可选返回值上进行可选链式调用,而不是方法本身。


错误处理


错误处理是响应错误以及从错误中恢复的过程。Swift提供了在运行时对可恢复错误的抛出、捕获、传递和操作的支持。

某些操作无法保证总是执行完所有代码或总是生成有用的结果。可选类型可用来表示值缺失,但是当某个操作失败时,最好能得知失败的原因,从而可以作出相应的应对。


在Swift中,错误用符合 Error 协议的类型的值来表示。


Swift的枚举类型尤为适合构建一组相关的错误状态,枚举的关联值还可以提供错误状态的额外信息。


抛出一个错误表明有意外情况发生,导致正常的执行流程无法继续执行。抛出错误使用 throw 关键字。


Swift中有四种处理错误的方式。你可以把函数抛出的错误传递给调用此函数的代码;用 do-catch 语句处理错误;将错误作为可选类型处理;或者断言此错误根本不会发生。


当一个函数抛出一个错误时,程序流程将会发生改变,因此识别出代码中会抛出错误的地方也就十分重要。为了标识出这些地方,在调用一个能抛出错误的函数、方法或者构造器之前,加上 try 关键字,或者 try?try! 这种变体。


为了表示一个函数、方法或构造器可以抛出错误,在函数声明的参数列表之后加上 throws 关键字。一个标有 throws 关键字的函数被称作 throwing 函数。

func canThrowErrors() throws -> String { }

一个 throwing 函数可以在其内部抛出错误,并将错误传递到函数被调用时的作用域。

只有 throwing 函数可以传递错误。任何在某个非 throwing 函数内部抛出的错误只能在函数内部处理。


可以使用一个 do-catch 语句运行一段闭包代码来处理错误。如果在 do 子句中的代码抛出了一个错误,这个错误会与 catch 子句做匹配,从而决定哪条子句能处理它。

下面是 do-catch 语句的一般形式:

do { 
    try expression 
    statements 
} catch pattern 1 { 
    statements 
} catch pattern 2 where condition { 
    statements 
}

可以使用 try? 通过将错误转换成一个可选值来处理错误。如果在评估 try? 表达式时一个错误被抛出,那么表达式的值就是 nil

如下例子中,xy 是等价的。

func someThrowingFunction() throws -> Int {
    // ... 
}

let x = try? someThrowingFunction()

let y: Int?
do {
    y = try someThrowingFunction()
} catch {
    y = nil
}

有时候某个 throwing 函数实际上在运行时是不会抛出错误的,在这种情况下,你可以在表达式前面写 try! 来禁用错误传递,这会把调用包装在一个不会有错误抛出的运行时断言中。如果真的抛出了错误,将会得到一个运行时错误。

// 如果图片无法加载,则抛出一个错误
let photo = try! loadImage(atPath: "./Resources/John Appleseed.jpg")

可以使用 defer 语句在即将离开当前代码块时执行一系列语句。该语句让你能执行一些必要的清理工作,不管是以何种方式离开当前代码块的(无论是由于抛出错误而离开,还是由于诸如 return 或者 break 的语句)。


类型转换


类型转换在Swift中使用 isas 操作符实现。这两个操作符给我们提供了一种简单达意的方式来检查值的类型或者转换它的类型。还可以用它来检查一个类型是否实现了某个协议。


可以将类型转换用在类和子类的层次结构上,用来检查特定类实例的类型并且转换这个类实例的类型成为这个层次结构中的其他类型。

// 基类
class MediaItem {
    var name: String
    init(name: String) {
        self.name = name
    }
}

// MediaItem基类的子类
class Movie: MediaItem {
    var director: String
    init(name: String, director: String) {
        self.director = director
        super.init(name: name)
    }
}

// MediaItem基类的子类
class Song: MediaItem {
    var artist: String
    init(name: String, artist: String) {
        self.artist = artist
        super.init(name: name)
    }
}

// library类型被推导为 [MediaItem] 类型
// 若要迭代library,依次取出的实例会是 MediaItem 类型
let library = [
    Movie(name: "Casablanca", director: "Michael Curtiz"),
    Song(name: "Blue Suede Shoes", artist: "Elvis Presley"),
    Movie(name: "Citizen Kane", director: "Orson Welles"),
    Song(name: "The One And Only", artist: "Chesney Hawkes"),
    Song(name: "Never Gonna Give You Up", artist: "Rick Astley")
]

// 用类型检查操作符 is 来检查一个实例是否属于特定子类型。
var movieCount = 0
var songCount = 0
for item in library {
    if item is Movie {
        movieCount += 1
    } else if item is Song {
        songCount += 1
    }
}

某类型的一个常量或变量可能在幕后实际上属于一个子类。当确定是这种情况时,用类型转换操作符 as?as! 可以尝试向下转到它的子类型。

由于向下转型可能会失败,类型转型操作符带有两种不同形式。as? 返回一个试图向下转成的类型的可选值。as! 强制向下转型,如果失败,则会触发运行时错误。

for item in library {
    if let movie = item as? Movie {
        print("Movie: '\(movie.name)', dir. \(movie.director)")
    } else if let song = item as? Song {
        print("Song: '\(song.name)', by \(song.artist)")
    }
}

Swift为不确定类型提供了两种特殊的类型别名:

  • Any 可以表示任何类型,包括函数类型。
  • AnyObject 可以表示任何类类型的实例。

作者:图拉鼎
链接:https://zhuanlan.zhihu.com/p/22584349
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

在Swift 3之前,我们可以写完一个项目都只用 AnyObject 来代表大多数实例,好像不用与 Any 类型打交道。但事实上,AnyAnyObject 是有明显区别的,因为 Any 可以代表 structclassfunc 等等几乎所有类型,而 AnyObject 只能代表 class 生成的实例。
那为什么之前我们在Swift 2里可以用 [AnyObject] 声明数组,并且在里面放 IntStringstruct 类型呢?这是因为Swift 2中,会针对这些 IntStringstruct 进行一个 Implicit Bridging Conversions,在 Array 里插入他们时,编译器会自动将其 bridge 到Objective-C的 NSNumberNSString 等类类型,这就是为什么我们声明的 [AnyObject] 里可以放 struct 的原因了。
但在Swift 3当中,为了达成一门真正的跨平台语言,相关提案将 Implicit Bridging Conversions 给去掉了。所以如果你要把 String 这个 struct 放进一个 [AnyObject] 里,一定要 as NSString,这些转换都需要显示的进行了,毕竟Linux平台默认没有Objective-C的runtime。这样各平台的表现更加一致。
以上内容摘自网络博客


嵌套类型


Swift支持定义嵌套类型,可以在支持的类型中定义嵌套的枚举、类和结构体。比如在结构体类型中定义嵌套的枚举类型。

在外部引用嵌套类型时,可以在嵌套类型的类型名前加上其外部类型的类型名作为前缀。

struct BlackjackCard { 
    // 嵌套的 Suit 枚举 
    enum Suit: Character {
        case Spades = "1", Hearts = "2", Diamonds = "3", Clubs = "4" 
    }

    // 其他结构体中代码
}

let heartsSymbol = BlackjackCard.Suit.Hearts.rawValue

扩展


扩展就是为一个已有的类、结构体、枚举类型或者协议类型添加新功能。这包括在没有权限获取原始源代码的情况下扩展类型的能力(即逆向建模)。扩展和Objective-C中的分类类似。

Swift中的扩展可以:

  • 添加计算型属性和计算型类型属性
  • 定义实例方法和类型方法
  • 提供新的构造器
  • 定义下标
  • 定义和使用新的嵌套类型
  • 使一个已有类型符合某个协议

在Swift中,你甚至可以对协议进行扩展,提供协议要求的实现或者添加额外的功能,从而可以让符合协议的 类型拥有这些功能。

需要注意的是扩展可以为一个类型添加新的功能,但是不能重写已有的功能。


下面例子为Swift的内建 Double 类型添加了五个计算型实例属性,用来提供与距离单位协作的基本支持:

extension Double {
    var km: Double { return self * 1_000.0 }
    var m : Double { return self }
    var cm: Double { return self / 100.0 }
    var mm: Double { return self / 1_000.0 }
    var ft: Double { return self / 3.28084 }  // 英尺
}

25.4.mm  // 0.0254
3.0.ft  // 0.914399970739201

扩展可以添加新的计算型属性,但是不可以添加存储型属性,也不可以为已有属性添加属性观察器。


扩展能为类添加新的便利构造器,但是不能为类添加新的指定构造器或析构器。指定构造器和析构器必须总 是由原始的类实现来提供。

如果用扩展为一个值类型添加构造器,同时该值类型的原始实现中未定义任何定制的构造器且所有存储属性 提供了默认值,那么我们就可以在扩展中添加新的构造器,并且可以在新的构造器里调用默认构造器和逐一成员构造器。如果在值类型的原始实现中有定制的构造器,那么上述规则将不再适用。

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

struct Size {
    var width = 0.0, height = 0.0
}

// 结构体Rect未提供定制的构造器,因此会获得一个逐一成员构造器
// 又因为它为所有存储型属性提供了默认值,因此又会获得一个默认构造器
struct Rect {
    var origin = Point()
    var size = Size()
}

let defaultRect = Rect()
let memberwiseRect = Rect(origin: Point(x: 2.0, y: 2.0),
                          size: Size(width: 5.0, height: 5.0))

// 结构体Rect的构造器
extension Rect {
    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)
    }
}
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
                      size: Size(width: 3.0, height: 3.0))

扩展可以为已有类型添加新的实例方法和类型方法。

下面的例子为 Int 类型添加了一个名为 repetitions 的实例方法:

extension Int {
    func repetitions(task: () -> Void) {
        for _ in 0..<self {
            task()
        }
    }
}

// 打印 3 次
3.repetitions { print("Hello!") }

通过扩展添加的实例方法也可以修改该实例本身。结构体和枚举类型中如若有修改 self 或其属性的方法必须将该实例方法标注为 mutating,正如来自原始实现的可变方法一样。

给Int类型利用扩展的方式添加了一个名为 square 的可变方法
extension Int {
    mutating func square() {
        self = self * self
    }
}

var someInt = 3
someInt.square()  // 9

扩展还可以为已有类型添加新下标。

下面这个例子为Swift内建类型 Int 添加一个整型下标,该下标 [n] 返回十进制数字从右向左数的第 n 个数字:

extension Int {
    subscript(digitIndex: Int) -> Int {
        var decimalBase = 1
        for _ in 0..<digitIndex {
            decimalBase *= 10
        }
        return (self / decimalBase) % 10
    }
}
746381295[0]  // 5 
746381295[1]  // 9
746381295[2]  // 2
746381295[3]  // 1
746381295[9]  // 0 越界,犹如在最左边补0,即0746381295[9]

扩展可以为已有的类、结构体和枚举添加新的嵌套类型。

下面这个例子为 Int 添加了嵌套枚举。用来表示正整数、零和负整数:

extension Int {
    enum Kind {
        case Negative, Zero, Positive
    }

    var kind: Kind {
        switch self {
        case 0:
            return .Zero
        case let x where x > 0:
            return .Positive
        default:
            return .Negative
        }
    }
}

协议


协议定义了一个蓝图,规定了一系列用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体或枚举都可以遵循协议,并为协议定义的这些要求提供具体实现。某个类型能够满足某个协议的要求,就可以说该类型遵循这个协议。

除了遵循协议的类型必须实现的要求外,还可以对协议进行扩展,通过扩展来实现一部分要求或者实现一些附加功能,这样遵循协议的类型就能够使用这些功能。


协议不指定属性是存储型属性还是计算型属性,它只指定属性的名称和类型以及指定属性是可读的还是可读可写的。
如果协议要求属性是可读可写的,那么该属性不能是常量属性或只读的计算型属性。如果协议只要求属性是可读的,那么该属性不仅可以是可读的,如果代码需要的话,还可以是可写的。

protocol SomeProtocol { 
    var mustBeSettable: Int { get set }  // 可读可写属性
    var doesNotNeedToBeSettable: Int { get }   // 只读属性
}

在协议中定义类型属性时,总是使用 static 关键字作为前缀。当类类型遵循协议时,除了使用 static 关键字,还可以使用 class 关键字来声明类型属性。

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set } 
}

协议可以要求遵循协议的类型实现某些指定的实例方法或类方法。这些方法作为协议的一部分,像普通方法一样放在协议的定义中,但是不需要大括号和方法体。
可以在协议中定义具有可变参数的方法,但是,协议中的方法不支持提供默认值的参数。

与属性要求中所述类似,在协议中定义类方法的时候,总是使用 static 关键字作为前缀。当类类型遵循协议时,除了使用 static 关键字,还可以使用 class 关键字作为前缀。

protocol SomeProtocol {
    func randomMethod() -> Double
    static func someTypeMethod() 
}

如果在值类型(即结构体和枚举)的方法中需要改变方法所属的实例,需要将 mutating 关键字作为方法的前缀,写在 func 关键字之前,表示可以在该方法中修改它所属的实例以及实例的任意属性的值。

实现协议中的 mutating 方法时,若是类类型,则不用写 mutating 关键字。而对于结构体和枚举,则必须写 mutating 关键字。

protocol Togglable {
    mutating func toggle()
}

enum OnOffSwitch: Togglable {
    case Off, On

    mutating func toggle() {
        switch self {
        case .Off:
            self = .On
        case .On:
            self = .Off
        }
    }
}

var lightSwitch = OnOffSwitch.Off
lightSwitch.toggle()

协议可以要求遵循协议的类型实现指定的构造器。

在遵循协议的类中实现构造器,无论是作为指定构造器,还是作为便利构造器。都必须为构造器的实现标上 required 修饰符。

protocol SomeProtocol {
    init(someParameter: Int)
}

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // 这里是对遵循协议的构造器的实现部分
    }
}

使用 required 修饰符可以确保所有子类也必须提供此构造器的实现,从而也能遵循协议。

如果类已经被标记为 final,那么不需要在协议构造器的实现中使用 required 修饰符,因为 final 类不能有子类。

如果一个子类重写了父类的指定构造器,并且该构造器满足了某个协议的要求,那么该构造器的实现需要同时标注 requiredoverride 修饰符。

protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        // 这里是构造器的实现部分
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // 因为遵循协议,需要加上 required 
    // 因为继承自父类,需要加上 override 
    required override init() {
        // 这里是构造器的实现部分
    }
}

协议还可以为遵循协议的类型定义可失败构造器要求。

遵循协议的类型可以通过可失败构造器 init? 或非可失败构造器 init 来满足协议中定义的可失败构造器要求。协议中定义的非可失败构造器要求可以通过非可失败构造器 init 或隐式解包可失败构造器 init! 来满足。


尽管协议本身并未实现任何功能,但是协议可以被当做一个成熟的类型来使用。

协议可以像其他普通类型一样使用,使用场景如下:

  • 作为函数、方法或构造器中的参数类型或返回值类型
  • 作为常量、变量或属性的类型
  • 作为数组、字典或其他容器中的元素类型

下面这个例子是将协议作为类型来使用:

// 生成随机数的协议
protocol RandomNumberGenerator {
    func random() -> Double
}

class Dice {
    let sides: Int

    // generator 属性的类型为 RandomNumberGenerator
    // 因此任何遵循了 RandomNumberGenerator 协议的类型的实例都可以赋值给 generator
    // 除此之外并无其他要求
    let generator: RandomNumberGenerator

    init(sides: Int, generator: RandomNumberGenerator) {
        self.sides = sides
        self.generator = generator
    }

    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }
}

委托是一种设计模式,它允许类或结构体将一些需要它们负责的功能委托给其他类型的实例。
委托模式的实现很简单:定义协议来封装那些需要被委托的功能,这样就能确保遵循协议的类型能提供这些功能。委托模式可以用来响应特定的动作,或者接收外部数据源提供的数据,而无需关心外部数据源的类型。


即便无法修改源代码,依然可以通过扩展令已有类型遵循并符合协议。扩展可以为已有类型添加属性、方法、下标以及构造器,因此可以符合协议中的相应要求。

通过扩展令已有类型遵循并符合协议时,该类型的所有实例也会随之获得协议中定义的各项功能。


协议能够继承一个或多个其他协议,可以在继承的协议的基础上增加新的要求。协议的继承语法与类的继承相 似,多个被继承的协议间用逗号分隔。

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // 这里是协议的定义部分 
}

在协议的继承列表中,通过添加 class 关键字来限制协议只能被类类型遵循,而结构体或枚举不能遵循 该协议。class 关键字必须第一个出现在协议的继承列表中,在其他继承的协议之前。

protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
    // 这里是类类型专属协议的定义部分 
}

有时候需要同时遵循多个协议,这时可以将多个协议采用 SomeProtocol & AnotherProtocol 这样的格式进行组合,称为协议合成。

protocol Named {
    var name: String { get }
}

protocol Aged {
    var age: Int { get }
}

struct Person: Named, Aged {
    var name: String
    var age: Int
}

// wishHappyBirthday函数不关心参数的具体类型,只要参数遵循这两个协议即可
func wishHappyBirthday(to celebrator: Named & Aged) {
    print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!") }

let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)

使用类型转换中描述的 isas 操作符来检查协议一致性,即是否符合某协议,并且可以转换到指定的协议类型。

  • is 用来检查实例是否符合某个协议,若符合则返回 true,否则返回 false
  • as? 返回一个可选值,当实例符合某个协议时,返回类型为协议类型的可选值,否则返回 nil
  • as! 将实例强制向下转换到某个协议类型,如果强转失败,会引发运行时错误。
protocol HasArea {
    var area: Double { get }
}

class Circle: HasArea {
    let pi = 3.1415927
    var radius: Double
    var area: Double { return pi * radius * radius }  // 计算型属性

    init(radius: Double) {
        self.radius = radius
    }
}

class Country: HasArea {
    var area: Double  // 存储型属性

    init(area: Double) {
        self.area = area
    }
}

// Animal类并未遵循HasArea协议
class Animal {
    var legs: Int
    init(legs: Int) {
        self.legs = legs
    }
}

let objects: [AnyObject] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Animal(legs: 4)
]

for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Area is \(objectWithArea.area)")
    } else {
        print("Something that doesn't have an area")
    }
}

// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area

协议可以定义可选要求,遵循协议的类型可以选择是否实现这些要求。
在协议中使用 optional 关键字作为前缀来定义可选要求。可选要求用在需要和 Objective-C 打交道的代码中。
协议和可选要求都必须带上 @objc 标记。标记了 @objc 特性的协议只能被Objective-C的类或者 @objc 类遵循,其他类以及结构体和枚举均不能遵循这种协议。

@objc protocol CounterDataSource { 
    optional func incrementForCount(count: Int) -> Int  // 可选方法
    optional var fixedIncrement: Int { get }   // 可选属性
}

尽管技术上允许 CounterDataSource 协议中的方法和属性都是可选的,但是遵循协议的类还是最好不要让其可选。


协议可以通过扩展来为遵循协议的类型提供属性、方法以及下标的实现。通过这种方式,你可以基于协议本身来实现这些功能,而无需在每个遵循协议的类型中都重复同样的实现,更无需使用全局函数。

protocol RandomNumberGenerator {
    func random() -> Double
}

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

可以通过协议扩展来为协议要求的属性、方法以及下标提供默认的实现。如果遵循协议的类型为这些要求提供了自己的实现,那么这些自定义实现将会替代扩展中的默认实现被使用。


在扩展协议的时候,可以指定一些限制条件,只有遵循协议的类型满足这些限制条件时,才能获得协议扩展提供的默认实现。这些限制条件写在协议名之后,使用 where 子句来描述。

// 集合中的元素如果遵循TextRepresentable协议,其元素就可以获得textualDescription属性
// 比如在数组中存放了遵循TextRepresentable协议的结构体的实例,从而可以访问这些实例的textualDescription属性
extension Collection where Iterator.Element: TextRepresentable {
    var textualDescription: String {
        let itemsAsText = self.map { $0.textualDescription }
        return "[" + itemsAsText.joinWithSeparator(", ") + "]"
    }
}

泛型


泛型代码让你能够根据自定义的需求,编写出适用于任意类型且灵活可重用的函数及类型。它能让你避免代码的重复,用一种清晰和抽象的方式来表达代码的意图。

泛型是Swift最强大的特性之一,许多Swift标准库是通过泛型代码构建的。事实上,泛型的使用贯穿了整本 语言手册,比如,Swift中的 ArrayDictionary 都是泛型集合。


func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
    let temporaryA = a
    a = b
    b = temporaryA
}

swapTwoValues(_:_:) 函数中,占位类型 T 是类型参数的一个例子。类型参数指定并命名一个占位类型,并且紧跟在函数名后面,使用一对尖括号括起来 <T>

一旦一个类型参数被指定,就可以用它来定义一个函数的参数类型或者作为函数的返回类型,还可以用作函数主体中的注释类型。在这些情况下,类型参数会在函数调用时被实际类型所替换。

可以有多个类型参数,将它们都写在尖括号中,用逗号分开。


在大多数情况下,类型参数具有一个描述性名字,例如 Dictionary<Key, Value> 中的 KeyValue,以及 Array<Element> 中的 Element,这可以告诉阅读代码的人这些类型参数和泛型函数之间的关系。然而,当它们之间没有有意义的关系时,通常使用单个字母来命名,例如 TUV 等。


除了泛型函数,Swift还允许你定义泛型类型。

struct Stack<Element> {
    var items = [Element]()

    mutating func push(_ item: Element) {
        items.append(item)
    }

    mutating func pop() -> Element {
        return items.removeLast()
    }
}


var stackOfStrings = Stack<String>()
stackOfStrings.push("aaa")
stackOfStrings.push("bbb")
stackOfStrings.push("ccc")

let fromTheTop = stackOfStrings.pop()  // ccc

当想要扩展一个泛型类型的时候,并不需要在扩展的定义中提供类型参数列表。原始类型定义中声明的类型参数列表在扩展中可以直接使用,并且这些来自原始类型中的参数名称会被用作原始定义中类型参数的引用。

extension Stack { 
    var topItem: Element? {
        return items.isEmpty ? nil : items[items.count - 1] 
    } 
}

上面的这个扩展并没有定义一个类型参数列表。Stack 类型已有的类型参数名称 Element 被用在扩展中来表示计算型属性 topItem 的可选类型。


有时候如果能将使用在泛型函数和泛型类型中的类型添加一个特定的类型约束,将会是非常有用的。类型约束可以指定一个类型参数必须继承自指定类,或者符合一个特定的协议或协议组合。比如,Swift的 Dictionary 类型对字典的键的类型做了些限制,字典的键的类型必须是可哈希的,因此就必须遵循 Hashable 协议。

在一个类型参数名后面放置一个类名或者协议名,并用冒号进行分隔,来定义类型约束,它们将成为类型参数列表的一部分。

// T必须是SomeClass子类的类型约束
// U必须遵循SomeProtocol协议
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // 这里是泛型函数的函数体部分 
}

类型约束的一个例子:

func findIndex<T>(of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

上面这个函数将无法通过编译,问题出在 if value == valueToFind,因为并不是所有的Swift类型都可以用相等符号 == 比较,比如是一个自定义的类或结构体。然而,Swift标准库中定义了一个 Equatable 协议,该协议要求任何遵循该协议的类型必须实现相等符号 == 和不等符号 !=,从而能对该类型的任意两个值进行比较。因此,上面代码改为以下形式才是正确的。

func findIndex<T: Equatable>(of valueToFind: T, in array:[T]) -> Int? {
    for (index, value) in array.enumerated() {
        if value == valueToFind {
            return index
        }
    }
    return nil
}

定义一个协议时,有的时候声明一个或多个关联类型作为协议定义的一部分将会非常有用。关联类型为协议中的某个类型提供了一个占位名(或者说别名),其代表的实际类型在协议被采纳时才会被指定。通过 associatedtype 关键字来指定关联类型。

protocol Container {
    associatedtype ItemType

    // 必须通过append(_:)方法添加一个新元素到容器中
    mutating func append(_ item: ItemType)

    // 必须可以通过count属性获取容器中的元素数量,且是Int类型
    var count: Int { get }

    // 必须可以通过Int型的下标来检索容器中的每个元素
    subscript(i: Int) -> ItemType { get }
}

任何遵从 Container 协议的类型必须能够指定其存储的元素的类型,必须保证只有正确类型的元素可以加进容器中,必须明确通过其下标返回的元素的类型。

为了达到这个目的,Container 协议声明了一个关联类型 ItemType。这个协议无法定义 ItemType 是什么类型的别名,这个信息将留给遵从协议的类型来提供。

struct Stack<Element>: Container {
    var items = [Element]()

    mutating func push(_ item: Element) {
        items.append(item)
    }

    mutating func pop() -> Element {
        return items.removeLast()
    }


    // Container 协议的实现部分    
    typealias ItemType = Element  // 此句可以通过下面方法推断出来,可省略

    mutating func append(_ item: Element) {
        self.push(item)
    }

    var count: Int {
        return items.count
    }

    subscript(i: Int) -> Element {
        return items[i]
    }
}

类型约束能够为泛型函数或泛型类型的类型参数定义一些强制要求。

为关联类型定义约束也是非常有用的,可以在参数列表中通过 where 子句为关联类型定义约束。

func allItemsMatch<C1: Container, C2: Container>
    (_ someContainer: C1, _ anotherContainer: C2) -> Bool
    where C1.ItemType == C2.ItemType, C1.ItemType: Equatable { }

上面这个函数的约束有:

  • C1 必须符合 Container 协议(意思就是 someContainer 是一个 C1 类型的容器)
  • C2 必须符合 Container 协议(意思就是 anotherContainer 是一个 C2 类型的容器)
  • C1ItemType 必须和 C2ItemType 类型相同,写作 C1.ItemType == C2.ItemType(意思就是 someContaineranotherContainer 包含相同类型的元素)
  • C1ItemType 必须符合 Equatable 协议(意思就是 someContainer 中的元素可以通过不等于操作符 != 来检查它们是否彼此不同)

第三条和第四条要求结合起来意味着 anotherContainer 中的元素也可以通过 != 操作符来比较,因为它和 someContainer 中的元素类型相同。


访问控制


访问控制限制了你访问其他源文件或模块中的代码的级别。

可以给单个类型(类、结构体、枚举)设置访问级别,也可以给这些类型的属性、方法、构造器、下标等设置访问级别。协议也可以被限定在一定的范围内使用,包括协议里的全局常量、变量和函数。


Swift中的访问控制模型基于模块和源文件这两个概念。

在Swift中,Xcode的每个目标(例如框架或应用程序)都被当作独立的模块处理,一个模块可以使用 import 关键字导入另外一个模块。

源文件就是Swift中的源代码文件,它通常属于一个模块,即一个应用程序或者框架。尽管我们一般会将不同的类型分别定义在不同的源文件中,但是同一个源文件也可以包含多个类型的定义。


通过修饰符 openpublicinternalfileprivateprivate 来声明实体的访问级别。

openpublic 的区别主要在继承上:把一个类标记为 open,更多的是在表明,该类可以作为其他模块的父类被继承。public 所修饰的类只能在本模块中被其他继承。


一个类型的访问级别会直接影响到其类型成员(属性、方法、构造器、下标)的默认访问级别。如果你将类型指定为 fileprivate,那么该类型所有成员的默认访问级别也会变成 fileprivate。如果你将类型指定为 openpublic 或者 internal,那么该类型的所有成员的便保持默认的 internal 访问级别。


元组的访问级别是由元组中访问级别最严格的类型来决定的。元组不同于类、结构体、枚举、函数那样有单独的定义,元组的访问级别是在它被使用时自动推断出的,而无法明确指定。


函数的访问级别根据参数类型和返回类型中最严格的访问级别来决定的。但是,如果最后的访问级别不符合函数定义所在环境的默认访问级别,那么就需要明确地指定该函数的访问级别。

func someFunction() -> (SomeInternalClass, SomePrivateClass) { 
    // 此处是函数实现部分 
}

以上代码会导致编译错误。这个函数的返回类型是一个元组,该元组中包含两个自定义的类,其中一个类的访问级别是 internal,另一个类的访问级别是 private ,所以根据元组访问级别的原则,该元组最终的访问级别是 private。因此,该函数的访问级别也应该是 private,正确写法如下:

private func someFunction() -> (SomeInternalClass, SomePrivateClass) { 
    // 此处是函数实现部分 
}

枚举成员的访问级别和该枚举类型相同,不能为枚举成员单独指定不同的访问级别。

枚举类型定义中的任何原始值或关联值的类型的访问级别至少不能低于枚举类型的访问级别。


private 类型中的嵌套类型的访问级别为 private
openpublicinternal 类型中的嵌套类型的访问级别为 internal
如果想要让嵌套类型拥有 public 访问级别,则需要显示指出。


子类的访问级别不得高于父类的访问级别。比如父类的访问级别是 internal,子类的访问级别不能是 public

然而,可以通过重写为继承来的类成员提供更高的访问级别。比如下面的例子:

public class A { 
    private func someMethod() {} 
}

internal class B: A { 
    override internal func someMethod() {} 
}

常量、变量、属性不能拥有比它们的类型更高的访问级别。同样,下标也不能拥有比索引类型或返回类型更高的访问级别。


常量、变量、属性、下标的 GetterSetter 的访问级别和它们所属类型的访问级别相同。

Setter 的访问级别可以低于对应的 Getter 的访问级别,这样就可以控制变量、属性或下标的读写权限。在 varsubscript 关键字之前,可以通过 private(set)fileprivate(set)internal(set) 为它们的写入权限指定更低的访问级别。


自定义构造器的访问级别可以低于或等于其所属类型的访问级别。唯一的例外是必要构造器 required initializer,它的访问级别必须和所属类型的访问级别相同。

如同函数或方法的参数,构造器参数的访问级别也不能低于构造器本身的访问级别。

如果结构体和类的所有存储型属性设置了默认初始值,并且未提供自定义的构造器,那么Swift会为其提供一个默认的无参数的构造器,该默认构造器的访问级别与所属类型的访问级别相同,除非类型的访问级别大于 internal。因此,如果希望一个 public 级别的类型也能在其他模块中使用这种无参数的默认构造器,你只能自己提供一个 public 访问级别的无参数构造器。对于结构体默认的成员逐一构造器同样如此。


如果想为一个协议类型明确地指定访问级别,在定义协议时指定即可。这将限制该协议只能在适当的访问级别范围内被采纳。

需要注意的是:协议中的每一个要求都具有和该协议相同的访问级别,你不能将协议中的要求设置为其他访问级别。这样才能确保该协议的所有要求对于任意遵循者都将可用。

如果定义了一个继承自其他协议的新协议,那么新协议拥有的访问级别最高也只能和被继承协议的访问级别相 同。

一个类型可以遵循比自身访问级别低的协议,然而,一旦遵循了比自身访问级别低的协议时,该类型的访问级别则取决与它本身和所遵循协议两者间最低的访问级别。


可以在访问级别允许的情况下对类、结构体、枚举进行扩展。扩展成员具有和原始类型成员一致的访问级 别。例如,你扩展了一个 public 或者 internal 类型,扩展中的成员具有默认的 internal 访问级别,和原始类型中的成员一致。如果你扩展了一个 private 类型,扩展成员则拥有默认的 private 访问级别。

还可以明确指定扩展的访问级别吗,如 private extension xxx,从而给该扩展中的所有成员指定一个新的默认访问级别。

如果想要通过扩展来遵循协议,那么你就不能显式指定该扩展的访问级别了。协议拥有相应的访问级别,并会为该扩展中所有协议要求的实现提供默认的访问级别。


高级运算符


按位取反运算符:

let a: UInt8 = 0b00001111  // 15
~a  // 240即0b11110000

按位与运算符:

let a: UInt8 = 0b11111100
let b: UInt8 = 0b00111111
let c = a & b   // 60即0b00111100

按位或运算符:

let a: UInt8 = 0b11111100
let b: UInt8 = 0b00111111
let c = a | b  // 255即0b11111111

按位异或运算符:

let a: UInt8 = 0b11111100
let b: UInt8 = 0b00111111
let c = a ^ b  // 195即0b11000011

按位左移运算符和按位右移运算符可以对一个数的所有位进行指定位数的左移和右移。对一个数进行按位左移或按位右移,相当于对这个数进行乘以 2 或除以 2 的运算。

对无符号整数进行移位的规则如下:

  • 已经存在的位按指定的位数进行左移和右移。
  • 任何因移动而超出整型存储范围的位都会被丢弃。
  • 0 来填充移位后产生的空白位。
let a: UInt8 = 4 // 即二进制的0b00000100
a << 1  // 8  即0b00001000
a << 2  // 16 即0b00010000
a << 5  // 128即0b10000000
a << 6  // 0  即0b00000000
a >> 2  // 1  即0b00000001

默认情况下,当向一个整数赋予超过它容量的值时,Swift默认会报错,而不是生成一个无效的数。这个行为 为我们在运算过大或着过小的数的时候提供了额外的安全性。

var a = Int8.max  // 127
a = a + 1  // 报错

Swift提供的三个溢出运算符(&+&-&*)来让系统支持整数溢出运算,这些运算符都是以 & 开头的。

var a = Int8.max  // 127
a = a &+ 1  // -128
a = a &+ 2  // -127

var a = UInt8.max  // 255
a = a &+ 1  // 0
a = a &+ 2  // 1

对于无符号与有符号整型数值来说,当出现上溢时,它们会从数值所能容纳的最大数变成最小的数。同样地,当发生下溢时,它们会从所能容纳的最小数变成最大的数。


类和结构体可以为现有的运算符提供自定义的实现,这被称为运算符重载。
下面例子展示了为自定义的结构体实现加法运算符:

struct Vector2D {
    var x = 0.0, y = 0.0
}

extension Vector2D {
    static func +(left: Vector2D, right: Vector2D) -> Vector2D {
        return Vector2D(x: left.x + right.x, y: left.y + right.y)
    }
}

let vector1 = Vector2D(x: 3.0, y: 1.0)
let vector2 = Vector2D(x: 2.0, y: 4.0)
let vector3 = vector1 + vector2  // Vector2D(x: 5.0, y: 5.0)

要实现前缀或者后缀运算符,需要在声明运算符函数的时候在 func 关键字之前加上 prefix 或者 postfix 修饰符。

extension Vector2D {
    static prefix func - (vector: Vector2D) -> Vector2D {
        return Vector2D(x: -vector.x, y: -vector.y)
    }
}
-vector3 // Vector2D(x: -5.0, y: -5.0)

复合赋值运算符将赋值运算符如 += 在实现的时候,需要把运算符的左参数设置成 inout 类型,因为这个参数的值会在运算符函数内直接被修改。

extension Vector2D {
    static func += (left: inout Vector2D, right: Vector2D) {
        left = left + right
    }
}
var vector4 = Vector2D(x: 4.0, y: 4.0)
vector4 += Vector2D(x: 1.0, y: 1.0)  // Vector2D(x: 5.0, y: 5.0)

自定义的类和结构体没有对等价运算符进行默认实现,等价运算符通常被称为相等运算符 == 与不等运算符 !=。对于自定义类型,Swift 无法判断其是否相等。

extension Vector2D {
    static func == (left: Vector2D, right: Vector2D) -> Bool {
        return (left.x == right.x) && (left.y == right.y)
    }

    static func != (left: Vector2D, right: Vector2D) -> Bool {
        return !(left == right)
    }
}
let vector5 = Vector2D(x: 1.0, y: 1.0)
let vector6 = Vector2D(x: 1.0, y: 1.0)
let vector7 = Vector2D(x: 1.0, y: 2.0)
vector5 == vector6  // true
vector5 != vector7  // true

除了实现标准运算符,在Swift中还可以声明和实现自定义运算符。新的运算符要使用 operator 关键字在全局作用域内进行定义,同时还要指定 prefixinfix 或者 postfix

prefix operator +++

extension Vector2D {
    static prefix func +++ (vector: inout Vector2D) -> Vector2D {
        vector += vector
        return vector
    }
}
var vector8 = Vector2D(x: 3.0, y: 8.0)
+++vector8  // Vector2D(x: 6.0, y: 16.0)

每个自定义中缀运算符都属于某个优先级组。这个优先级组指定了这个运算符和其他中缀运算符的优先级和结合性。而没有明确放入优先级组的自定义中缀运算符会放到一个默认的优先级组内,其优先级高于三元运算符。

infix operator +-: AdditionPrecedence

extension Vector2D {
    static func +- (left: Vector2D, right: Vector2D) -> Vector2D {
        return Vector2D(x: left.x + right.x, y: left.y - right.y)
    }
}

let vector9 = Vector2D(x: 1.0, y: 2.0)
let vector10 = Vector2D(x: 3.0, y: 4.0)
let vector11 = vector9 +- vector10  // Vector2D(x: 4.0, y: -2.0)

如果对同一个值同时使用前缀与后缀运算符,则后缀运算符会先参与运算。