本篇读书摘要大概有2万2千字和170个代码段,涵盖了原书11万字的几乎所有知识点,慢读需要3到4个小时。
在原书通读过3到5遍后,每周再浏览一到两遍读书摘要,两个月后对Swift的基础知识算是能做到了然于胸了。
基础部分
Swift包含了C和Objecitve-C中所有的基础数据类型,Int
表示整型值;Double
和 Float
表示浮点型值;Bool
表示布尔型值;String
表示字符串型值;Character
表示字符型值。
Swift提供了三个基本的集合类型:Array
、Dictionary
、Set
。
在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支持多行注释的嵌套。
你可以访问不同整数类型的 max
和 min
属性来获取对应整数类型的最大值和最小值。
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
或其他整数类型(如 Int8
,Int16
,Int32
,Int64
,UInt8
,UInt16
,UInt32
,UInt64
等),最好使用 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.0
,1.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
的值。
使用可选绑定来判断可选类型是否包含值,如果包含,就把值赋给一个临时的常量或变量。可选绑定可以用在 if
和 while
语句中。
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 =
line: UInt =
let age = -3
assert(age > 0, "A person's age cannot be less than zero") // 不满足条件,断言被触发,应用终止。
当代码使用优化编译的时候,断言将被禁用。例如在Xcode中,使用默认的 Target Release 配置选项来编译时,断言会被禁用。
基本运算符
Swift支持大部分的C语言运算符,且改进了许多特性来减少常规编码错误。如:
- 赋值符
=
不返回值。为了防止把想要判断相等运算符==
的地方写成赋值运算符导致的错误。 - 算术运算符(
+, -, *, /, %
等)不允许值溢出,以此来避免保存变量时由于变量大于或小于其类型所能承载的范围时导致的异常结果。(Swift允许你使用溢出运算符&
来实现溢出)
运算符分为一元、二元和三元运算符:
- 一元运算符操作一个操作数。如
-a
,+a
,!a
,a!
等。 - 二元运算符操作两个操作数。如
+
,-
,*
,/
,%
,=
等。 - 三元运算符操作三个操作数。和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 % b
与 a % -b
的结果是一样的。
9 % 4 // 1
9 % -4 // 1
-9 % 4 // -1
-9 % -4 // -1
复合赋值运算符(如 +=
,-=
,*=
,/=
,%=
等)没有返回值。类似 let b = a += 2
这样的代码是错误的。
Swift提供了恒等 ===
和不恒等 !==
这两个比较符来判断两个常量或变量是否引用了同一个对象实例。
当元组中的值可以比较时,你可以使用比较运算符来比较他们的大小。比如,Int
和 Sting
类型的值可以比较大小,所以类型为 (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 均为空字符串并相等
可以通过 String
中 isEmpty
属性来判断字符串是否为空:
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的 String
和 Character
类型是完全兼容 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
是空串,startIndex
与 endIndex
相等且都等于 0
。
通过调用 String
的 index(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])
}
你可以使用 startIndex
和 endIndex
属性以及 index(before:)
、index(after:)
和 index(_:offsetBy:)
函数在任意一个遵循 Collection
协议的类型里面,如上面的 String
,也可以使用在 Array
、Dictionary
和 Set
中。
调用 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
,也可以使用在 Array
、Dictionary
和 Set
中。
字符串/字符可以用等于操作符 ==
和不等于操作符 !=
。
如果两个字符串/字符的可扩展字符群集是标准相等的,那就认为它们是相等的。因此,即使可扩展字符群集是由不同的 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语言提供了 Array
、Dictionary
和 Set
这三种基本的集合类型来存储数据。
数组 Array
是有序数据的集,字典 Dictionary
无序键值对的集,集合 Set
是无序无重复数据的集。
Array
、Dictionary
和 Set
中存储的数据值类型必须明确。这意味着我们不能把不正确的数据类型放入其中,同时也说明了我们完全可以对取回的值的类型非常自信。
在我们不需要改变集合的时候尽量应该创建不可变集合,这样不仅可以有更好的性能优化,还能更加清晰的表达你的意图。
数组使用有序列表存储同一类型的多个值。相同的值可以多次出现在数组中的不同位置。
写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的所有基本类型(如 String
,Int
,Double
,Bool
等)默认都是可哈希化的,可以作为集合中值的类型或者字典中键的类型。
没有关联值的枚举成员值默认也是可哈希化的。
enum Direction {
case east
case sourth
case west
case north
}
Direction.sourth.hashValue // 1
可以使用你自定义的类型作为集合中值的类型或者字典中键的类型,但是自定义的类型必须遵守 Hashable
协议。符合 Hashable
协议的类型需要提供一个类型为 Int
的 hashValue
属性。
由于 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)
}
通过访问字典的 keys
和 values
属性可以获取到字典所有的键和值。
Swift的字典类型是无序集合类型,为了以特定顺序遍历字典的键或值,可以对字典的 keys
或 values
属性使用 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
同时匹配 a
和 A
,可以将这两个值组成一个复合匹配,用逗号分开:
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
语句出现的代码段。它可以用控制转移语句如 return
,break
,continue
以及 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)) { }
xxxx
xxxx
对Swift来说,我们可以在 if
或 guard
条件语句中使用可用性条件 #available
在运行时判断不同的平台下,做不同的逻辑处理。
if
// iOS10及更高的系统下运行
} else {
// iOS10以下系统下运行
}
guard
最后一个参数 *
是必须的,用于指定在所有其它平台中(如 macOS
,watchOS
,tvOS
等)。
函数
如果函数没有参数,函数名后的一对圆括号还是不能省略的。当该无参函数被调用时,也需要在函数名后写上一对圆括号。
没有定义返回值类型的函数会返回一个特殊的 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
枚举类型中定义的值 north
,south
,east
,west
是这个枚举的成员值。
与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中,许多基本类型,诸如 String
、Array
和 Dictionary
类型均以结构体形式实现,因此,在代码中,拷贝行为看起来似乎总在发生。然而,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
父类的属性在子类的构造器中被赋值时,它在父类中的 willSet
和 didSet
观察器会被调用,随后才会调用子类的观察器。
全局变量是在函数、方法、闭包或任何类型之外的变量。
局部变量是在函数、方法或闭包内部定义的变量。
全局的常量或变量都是延迟计算的,不需要 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中,类是唯一能定义方法的类型。
类型的每一个实例都有一个隐含的属性叫做 self
,self
完全等同于该实例本身。
实际上,在代码中没必要经常写 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
关键字,指定一个或多个参数和返回值类型。
下标可以设定为读写或只读,这种行为由 getter
和 setter
来实现。
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()
方法。 - 在属性
someProperty
的getter
和setter
的重写实现中,可以通过super.someProperty
来访问父类的someProperty
属性。 - 在下标的重写实现中,可以通过
super[someIndex]
来访问父类中的相同下标。
重写方法。
class Train: Vehicle {
override func makeNoise() {
print("Choo Choo")
}
}
let train = Train()
train.makeNoise() // Choo Choo
重写属性。
你可以提供定制的 getter
(或 setter
)来重写任意继承来的属性,无论继承来的属性是存储型属性还是计算型属性。子类并不知道继承来的属性是存储型的还是计算型的,它只知道继承来的属性会有一个名字和类型。
可以将一个继承来的只读属性重写为一个读写属性,只需要在重写版本的属性里提供 getter
和 setter
即可。但是,不可以将一个继承来的读写属性重写为一个只读属性。
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 var
,final func
,final class func
,final 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()
这种写法,不能使用 someProperty
或 someMethod()
这种写法,这能让你清楚的知道是否捕获了 self
。
捕获列表中的每一项都由一对元素组成,一个元素是 weak
或 unowned
关键字,另一个元素是类实例的引用如 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
。
如下例子中,x
和 y
是等价的。
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中使用 is
和 as
操作符实现。这两个操作符给我们提供了一种简单达意的方式来检查值的类型或者转换它的类型。还可以用它来检查一个类型是否实现了某个协议。
可以将类型转换用在类和子类的层次结构上,用来检查特定类实例的类型并且转换这个类实例的类型成为这个层次结构中的其他类型。
// 基类
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
类型打交道。但事实上,Any
和 AnyObject
是有明显区别的,因为 Any
可以代表 struct
、class
、func
等等几乎所有类型,而 AnyObject
只能代表 class
生成的实例。
那为什么之前我们在Swift 2里可以用 [AnyObject]
声明数组,并且在里面放 Int
、String
等 struct
类型呢?这是因为Swift 2中,会针对这些 Int
、String
等 struct
进行一个 Implicit Bridging Conversions
,在 Array
里插入他们时,编译器会自动将其 bridge
到Objective-C的 NSNumber
、NSString
等类类型,这就是为什么我们声明的 [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
类不能有子类。
如果一个子类重写了父类的指定构造器,并且该构造器满足了某个协议的要求,那么该构造器的实现需要同时标注 required
和 override
修饰符。
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)
使用类型转换中描述的 is
和 as
操作符来检查协议一致性,即是否符合某协议,并且可以转换到指定的协议类型。
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中的 Array
和 Dictionary
都是泛型集合。
func swapTwoValues<T>(_ a: inout T, _ b: inout T) {
let temporaryA = a
a = b
b = temporaryA
}
在 swapTwoValues(_:_:)
函数中,占位类型 T
是类型参数的一个例子。类型参数指定并命名一个占位类型,并且紧跟在函数名后面,使用一对尖括号括起来 <T>
。
一旦一个类型参数被指定,就可以用它来定义一个函数的参数类型或者作为函数的返回类型,还可以用作函数主体中的注释类型。在这些情况下,类型参数会在函数调用时被实际类型所替换。
可以有多个类型参数,将它们都写在尖括号中,用逗号分开。
在大多数情况下,类型参数具有一个描述性名字,例如 Dictionary<Key, Value>
中的 Key
和 Value
,以及 Array<Element>
中的 Element
,这可以告诉阅读代码的人这些类型参数和泛型函数之间的关系。然而,当它们之间没有有意义的关系时,通常使用单个字母来命名,例如 T
、U
、V
等。
除了泛型函数,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 类型的容器)C1
的ItemType
必须和C2
的ItemType
类型相同,写作C1.ItemType == C2.ItemType
(意思就是someContainer
和anotherContainer
包含相同类型的元素)C1
的ItemType
必须符合Equatable
协议(意思就是someContainer
中的元素可以通过不等于操作符!=
来检查它们是否彼此不同)
第三条和第四条要求结合起来意味着 anotherContainer
中的元素也可以通过 !=
操作符来比较,因为它和 someContainer
中的元素类型相同。
访问控制
访问控制限制了你访问其他源文件或模块中的代码的级别。
可以给单个类型(类、结构体、枚举)设置访问级别,也可以给这些类型的属性、方法、构造器、下标等设置访问级别。协议也可以被限定在一定的范围内使用,包括协议里的全局常量、变量和函数。
Swift中的访问控制模型基于模块和源文件这两个概念。
在Swift中,Xcode的每个目标(例如框架或应用程序)都被当作独立的模块处理,一个模块可以使用 import
关键字导入另外一个模块。
源文件就是Swift中的源代码文件,它通常属于一个模块,即一个应用程序或者框架。尽管我们一般会将不同的类型分别定义在不同的源文件中,但是同一个源文件也可以包含多个类型的定义。
通过修饰符 open
,public
,internal
,fileprivate
,private
来声明实体的访问级别。
open
和 public
的区别主要在继承上:把一个类标记为 open
,更多的是在表明,该类可以作为其他模块的父类被继承。public
所修饰的类只能在本模块中被其他继承。
一个类型的访问级别会直接影响到其类型成员(属性、方法、构造器、下标)的默认访问级别。如果你将类型指定为 fileprivate
,那么该类型所有成员的默认访问级别也会变成 fileprivate
。如果你将类型指定为 open
、public
或者 internal
,那么该类型的所有成员的便保持默认的 internal
访问级别。
元组的访问级别是由元组中访问级别最严格的类型来决定的。元组不同于类、结构体、枚举、函数那样有单独的定义,元组的访问级别是在它被使用时自动推断出的,而无法明确指定。
函数的访问级别根据参数类型和返回类型中最严格的访问级别来决定的。但是,如果最后的访问级别不符合函数定义所在环境的默认访问级别,那么就需要明确地指定该函数的访问级别。
func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// 此处是函数实现部分
}
以上代码会导致编译错误。这个函数的返回类型是一个元组,该元组中包含两个自定义的类,其中一个类的访问级别是 internal
,另一个类的访问级别是 private
,所以根据元组访问级别的原则,该元组最终的访问级别是 private
。因此,该函数的访问级别也应该是 private
,正确写法如下:
private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// 此处是函数实现部分
}
枚举成员的访问级别和该枚举类型相同,不能为枚举成员单独指定不同的访问级别。
枚举类型定义中的任何原始值或关联值的类型的访问级别至少不能低于枚举类型的访问级别。
private
类型中的嵌套类型的访问级别为 private
;open
、public
或 internal
类型中的嵌套类型的访问级别为 internal
;
如果想要让嵌套类型拥有 public
访问级别,则需要显示指出。
子类的访问级别不得高于父类的访问级别。比如父类的访问级别是 internal
,子类的访问级别不能是 public
。
然而,可以通过重写为继承来的类成员提供更高的访问级别。比如下面的例子:
public class A {
private func someMethod() {}
}
internal class B: A {
override internal func someMethod() {}
}
常量、变量、属性不能拥有比它们的类型更高的访问级别。同样,下标也不能拥有比索引类型或返回类型更高的访问级别。
常量、变量、属性、下标的 Getter
和 Setter
的访问级别和它们所属类型的访问级别相同。
Setter
的访问级别可以低于对应的 Getter
的访问级别,这样就可以控制变量、属性或下标的读写权限。在 var
或 subscript
关键字之前,可以通过 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
关键字在全局作用域内进行定义,同时还要指定 prefix
、infix
或者 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)
如果对同一个值同时使用前缀与后缀运算符,则后缀运算符会先参与运算。