136.泛型
泛型代码让你可以写出灵活,可重用的函数和类型,它们可以使用任何类型,受你定义的需求的约束。你可以写出代码,避免重复而且可以用一个清晰抽象的方式来表达它的意图。
泛型是Swift中最有力的特征之一, 而且大部分Swift的标准库是用泛型代码建立的。事实上, 在整个语言教程中,你一直在使用泛型,尽管你没有意识到这点。例如, Swift 的数组和字典类型都是泛型集合。 你可以创建一个整数数组,一个字符串数组,甚至是Swift允许创建的任何类型的数组。相似的, 你可以创建字典来保存任何指定类型的值, 什么类型并没有限制。
泛型解决的问题
这里有一个标准的非泛型的函数 swapTwoInts(::), 用来交换两个整数值:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
这个函数用了输入输出参数来交换a和b的值。
swapTwoInts(::) 函数把b的原始值交换到a, a的原始值到b. 你可以调用这个函数来交换两个整型变量中的值:
var someInt = 3
var anotherInt = 107
swapTwoInts(&someInt, &anotherInt)
print("someInt is now \(someInt), and anotherInt is now \(anotherInt)")
// 打印 "someInt is now 107, and anotherInt is now 3"
swapTwoInts(::) 函数是有用的, 不过只能用于整数。如果你想交换两个字符串, 或者两个浮点数, 你就要写更多的函数, 比如swapTwoStrings(::) 和 swapTwoDoubles(::) 函数:
func swapTwoStrings(_ a: inout String, _ b: inout String) {
let temporaryA = a
a = b
b = temporaryA
}
func swapTwoDoubles(_ a: inout Double, _ b: inout Double) {
let temporaryA = a
a = b
b = temporaryA
}
你可能注意到了,swapTwoInts(::), swapTwoStrings(::), 和 swapTwoDoubles(::) 的函数体是一样的。唯一不同的是它们接受的值的类型 (Int, String, 和 Double).
写一个可以交换任何类型值的函数,可能更有用和灵活。泛型代码让你可以写出这种函数。(这些函数的泛型版本会在下面定义。)
备注:在所有三个函数中, 重要的是a和b的类型要一样。如果a和b的类型不一样, 就不可能交换它们两个的值。 Swift 是一门类型安全的语言, 不允许把一个字符串和浮点数进行交换。如果这样做,会报一个编译期错误。
泛型函数
泛型函数可以使用任何类型。这里有一个上面 swapTwoInts(::)函数的泛型版本 swapTwoValues(::):func swapTwoValues(_ a: inout T, _ b: inout T) { let temporaryA = a a = b b = temporaryA}swapTwoValues(::) 函数体和 swapTwoInts(::) 函数体是一样的。不过, swapTwoValues(::) 函数第一行跟swapTwoInts(::) 稍微有点不一样。下面是第一行的比较:func swapTwoInts(_ a: inout Int, _ b: inout Int)func swapTwoValues(_ a: inout T, _ b: inout T)
泛型版本的函数用了一个占位符类型名(这里叫T) ,而不是使用实际的类型名 (比如 Int, String, 或者 Double). 这个占位符类型名不说T是到底是什么, 但是它表明a和b是同样的类型 T, 无论T表示什么。每次swapTwoValues(::)函数调用的时候,再决定T是什么类型。
其他的不同是泛型函数名后面跟着一个T包括在尖括号中 (). 括号告诉 Swift ,T 在 swapTwoValues(::) 函数定义中是一个占位符类型名。因为 T 是一个占位符, Swift 不能找到真正的类型 T.
现在可以像调用swapTwoInts一样调用 swapTwoValues(::) 函数, 不过你可以传入两个任何类型的值, 只要两个值的类型是一样的。每次调用 swapTwoValues(::), T的类型会从传入的值的类型推断出来。
下面两个例子里, T 分别推断为整型和字符串类型:
var someInt = 3
var anotherInt = 107
swapTwoValues(&someInt, &anotherInt)
// someInt is now 107, and anotherInt is now 3
var someString = "hello"
var anotherString = "world"
swapTwoValues(&someString, &anotherString)
// someString is now "world", and anotherString is now "hello"
备注 上面定义的 swapTwoValues(::) 函数是受到Swift 标准库函数swap的启发。它可以在你的应用里直接使用。如果你需要 swapTwoValues(::) 函数的功能, 你可以使用 Swift 已存在的 swap(::) 函数而不用自己重新实现。
类型参数
上面的 swapTwoValues(::) 例子, 占位符类型 T 是类型参数的例子。类型参数指定和命名一个占位符类型, 直接写在函数名的后面, 在一对尖括号里面 (例如 ).
只要你指定了一个类型参数, 你就可以用它来定义一个函数的参数类型 (例如 swapTwoValues(::) 函数里的a和b), 或者作为函数的返回值类型, 或者在函数体中用作一个类型注释。每种情况下, 函数调用时,类型参数会被真实的类型替换。
你可以在尖括号里写上多个类型参数,来提供更多的类型参数。用逗号分开就行。
命名类型参数
在大多数情况下, 类型参数有描述性的名字, 例如 Dictionary 中的Key 和 Value , Array里的Element, 它会告诉读者类型参数和泛型类型或者所在函数的关系。不过, 当它们之间没有一个有意义的关系时, 通常做法是用单个字母来给它们命名,比如 T, U, 和 V, 比如上面 swapTwoValues(::) 函数中的T。
备注 用驼峰式方法给参数类型命名来表明它们是一个占位符类型,而不是一个值。(例如 T 和 MyTypeParameter).
泛型类型
除了泛型函数, Swift 也可以定义泛型类型。它们是可以使用任何类型的类,结构体和枚举。跟数组和字典有着相似的方式。
这部分内容展示如何写一个泛型集合 Stack. 栈是有序集合, 跟数组类似, 但是操作更加严格。数组允许任何位置的项的插入和移除。栈只允许在集合尾部添加 (压栈)。类似的, 栈只允许项目从集合尾部移除 (出栈)。
备注:UINavigationController 使用栈来模拟在导航层次中的视图控制器。调用 UINavigationController 的 pushViewController(:animated:) 方法在导航栈上添加一个视图控制器, 调用 popViewControllerAnimated(:) 方法从导航栈上移除一个视图控制器。如果你要一个后进先出的方式来管理集合,栈可以派上用场。
下面的图展示了栈的压栈和出栈的行为:
当前栈上有三个值。第四个值添加到栈顶。现在栈内有四个值, 最近的值在最上面。
栈顶的值被移除或者出栈。弹出一个值后, 栈内现在再次是三个值。
这里有个非泛型版本的栈, 针对的是整型值的情况:
struct IntStack {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
}
这个结构体使用数组属性items来保存栈内的值。IntStack 提供了两个方法, push 和 pop, 用来压栈和出栈。这两个方法都是 mutating, 因为它们要改变结构体的 items 数组。IntStack 类型只能用于整数, 不过。如果能定义一个泛型栈类可能会更有用, 它可以管理任何类型值。这里是一些代码的泛型版本:
struct Stack{
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
}
注意,事实上泛型版本的栈和非泛型的版本很像, 只不过有一个类型参数 Element 取代了实际类型 Int. 这个类型名写在结构体名的后面,放在一对尖括号里面().
Element 是一个占位符的名字。这个未来类型可以在结构体定义中作为元素使用。在这种情况下, Element 在三个地方用作占位符:
创建一个属性items, 它是用Element 类型值来初始化的空数组。
指定 push(_:) 方法有一个参数 item, 类型是 Element
指定 pop() 方法的返回值,类型是 Element
因为它是一个泛型类型, Stack可以用来创建Swift中任何有效的类型的栈, 跟字典和数组的用法类似。
在方括号里写上栈存储类型来创建一个新的 Stack 实例。例如, 创建一个字符串的栈, 这样写 Stack():
var stackOfStrings = Stack()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// 这个栈现在有4个字符串
下面是压入四个值之后stackOfStrings 变化:
从栈中移除一个值并且返回栈顶的值, “cuatro”:
let fromTheTop = stackOfStrings.pop()
// fromTheTop 等于 "cuatro", 现在栈内有3个字符串
下面的弹出一个值后栈的变化:
扩展泛型类型
当你扩展一个泛型类型, 你不需要在扩展定义中提供一个类型参数列表。相反, 原类型定义的类型参数列表可以在扩展内部使用, 并且,使用原类型类型参数名在原来的定义中调用类型参数。
下面的例子扩展了泛型 Stack 类型,添加了一个只读计算属性 topItem, 它返回栈顶元素,而且不用弹出这个元素:
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
}
}
topItem 属性返回可选的Element 类型值。如果栈是空的, topItem 返回nil; 如果栈非空, topItem 返回items数组最后一个值。
注意,这个扩展没有定义类型参数列表。相反, Stack 类型已存在的类型参数名, Element, 在扩展内部使用来表示topItem 是一个可选类型。
计算属性topItem 现在可以使用 Stack 实例来访问栈顶的元素,而不用去移除它:
if let topItem = stackOfStrings.topItem {
print("The top item on the stack is \(topItem).")
}
// 打印 "The top item on the stack is tres."
类型限制
swapTwoValues(::) 函数和 Stack 类型可以使用任意类型。不过, 有时候对使用泛型函数和类型的类型使用限制是有用的。 类型限制指定一个类型参数必须继承自一个类,或者符合一个协议或者协议组合。
例如, Swift的字典类型对可以用作键的类型进行了限制, 字典的键类型必须是可哈希的。就是说, 它必须提供一个方法让自己独一无二。 字典需要键可哈希,为了判断特定键是否包含了一个值。如果没有这个要求, 字典就不能判断是否可以对一个键插入或者修改一个值。也不能通过给定的键找到一个值。
字典的键类型,这个需求是类型限制强制的。它规定键类型必须符合 Hashable 协议, 它定义在Swift 标准库中。Swift 的所有基本类型默认都是可哈希的。
创建泛型类型时,你可以定义自己的类型限制。这些限制提供泛型编程大部分能力。诸如哈希特性类型的抽象概念,依据的是它们概念性的特征而不是它们的显式类型。
可续限制语法
通过在类型参数名后放置一个单独的类或者协议,然后用冒号分开,来写类型限制。泛型函数的类型限制的基本语法显示如下:func someFunction(someT: T, someU: U) {
// function body goes here
}
上面的假想函数有两个参数。第一个类型参数, T, 类型限制是要求T是 SomeClass 的子类。第二个类型参数, U, 类型限制是要求U符合 SomeProtocol 协议。
类型限制的行为
这里有一个非泛型的函数findIndex(ofString:in:), 它有一个查找的字符串值和待查找的字符串数组。findIndex(ofString:in:) 函数返回一个可选的整数值。它是数组中第一个匹配字符串的索引, 如果找不到就返回nil:
func findIndex(ofString valueToFind: String, in array: [String]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
findIndex(ofString:in:) 函数可以用来在字符串数组查找一个字符串:
let strings = ["cat", "dog", "llama", "parakeet", "terrapin"]
if let foundIndex = findIndex(ofString: "llama", in: strings) {
print("The index of llama is \(foundIndex)")
}
// 打印 "The index of llama is 2"
这种查找字符串的方式只对字符串有用, 不过。 你可以写一个泛型函数来处理其他类型。这里有一个你期待的泛型版本的 findIndex(ofString:in:)函数, 叫 findIndex(of:in:). 注意,函数返回值仍然是 Int?, 因为函数返回的是可选的索引值, 不是来自数组的可选值。 不过这个函数不能编译, 原因在这个例子后面再解释:
func findIndex(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能够理解的。因为这个原因, 它不能保证这个代码对各种可能的T类型都有效, 当你尝试编译这个代码的时候,就会报错。
不过,没有任何损失。Swift 标准库定义了一个协议 Equatable, 它要求符合类型实现等于和不等于,来比较这个类型的任意两个值。所有 Swift 的标准类型都自动支持这个协议。
任何可以比较的类型都可以安全的使用 findIndex(of:in:) 函数, 因为它保证支持等于运算符。为了说明这个事实, 在你定义函数时,你可以在类型参数定义的时候写一个Equatable类型限制:
func findIndex(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
}
}
return nil
}
findIndex(of:in:) 的参数类型写作 T: Equatable, 意思是 “符合Equatable 协议的任意 T 类型。”
限制 findIndex(of:in:) 函数编译成功了,然后可以使用任意可以比较的类型, 例如 Double 或者 String:
let doubleIndex = findIndex(of: 9.3, in: [3.14159, 0.1, 0.25])
// doubleIndex is an optional Int with no value, because 9.3 is not in the array
let stringIndex = findIndex(of: "Andrea", in: ["Mike", "Malcolm", "Andrea"])
// stringIndex is an optional Int containing a value of 2
关联类型
当定义一个协议的时候, 有时候声明一个或者多个关联类型是很有用的。一个关联类型给一个类型提供一个占位符名。关联类型使用的实际类型只要协议被采用才会指定。关联类型使用associatedtype 关键字来指定。
关联类型的行为
这里有个Container协议的例子, 它声明了一个关联类型 ItemType:
protocol Container {
associatedtype ItemType
mutating func append(_ item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}
Container 协议定义了任何容器必须提供的三个必须的能力:
它必须要可以用append(_:)方法给容器添加新项目。
它必须可以通过count属性访问容器中项目的数量。
它必须可以通过下标运算获取到容器的每一项。
这个协议没有指定怎么存储项目或者它允许的类型。这个协议只是指定了任何符合类型要提供的三个功能。一个符合类型可以提供额外的功能, 只要它满足三个必须要求。
任何符合 Container 协议的类型必须能够指定它存储值的类型。特别是, 它必须确保只有正确的类型才可以添加到容器, 它必须清楚下标返回的项目的类型。
为了定义这三个必须要求, Container 协议需要一个方法去调用容器将要装载的元素类型, 不用知道特定容器类型是什么。Container 协议需要指定,传入append(_:) 方法的值必须和容器里的元素类型一样。容器下标返回的值的类型也要和容器里的元素类型一样。
为了实现这点, Container 协议定义了一个关联类型 ItemType, 写作 associatedtype ItemType. 协议没有定义 ItemType是什么—这个留给符合类型来提供。 尽管如此, ItemType 别名提供了一种方式来调用容器里的元素类型, 为了使用 append(_:) 方法和下标定义了一个类型。以确保任何容器期望的行为被执行。
这里是早前非泛型版本的 IntStack 类型, 采用和符合了 Container 协议:
struct IntStack: Container {
// original IntStack implementation
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
}
mutating func pop() -> Int {
return items.removeLast()
}
// conformance to the Container protocol
typealias ItemType = Int
mutating func append(_ item: Int) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Int {
return items[i]
}
}
IntStack 类型实现了 Container 协议要求的三个功能。
此外, IntStack 指定,为了实现 Container, 关联 ItemType 使用Int类型。typealias ItemType = Int 定义,为Container 协议的实现,把抽象类型转换为实际的Int类型。
由于 Swift 的类型推断, 实际上你不需要声明ItemType 为Int. 因为 IntStack 符合 Container 协议所有的要求, Swift 可以推断使用的关联类型 ItemType, 只要简单查找 append(_:) 方法的参数类型和下标的返回类型。事实上, 如果你删除上面的 typealias ItemType = Int, 一切都正常, 因为它知道什么类型用于 ItemType.
你也可以让你泛型版本的 Stack 类型来符合 Container 协议:
struct Stack: Container { // original Stackimplementation
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
}
mutating func pop() -> Element {
return items.removeLast()
}
// conformance to the Container protocol
mutating func append(_ item: Element) {
self.push(item)
}
var count: Int {
return items.count
}
subscript(i: Int) -> Element {
return items[i]
}
}
这一次, 类型参数 Element 用作 append(_:) 方法的参数类型和下标的返回类型。Swift 可以推断 Element 是关联类型, 在特定容器用作ItemType.
扩展存在的类型去指定关联类型
你可以扩展存在的类型去符合一个协议。这个包含带有关联类型的协议。
Swift 的数组类型已经提供了append(_:)方法, 一个count 属性, 和一个带有索引获取元素的下标。这三个能力满足 Container 协议的要求。这个意味着你可以扩展数组来符合 Container 协议。使用空扩展即可实现这个:
extension Array: Container {}
数组存在的 append(_:) 方法和下标让 Swift 可以推断使用ItemType的关联类型, 和上面的泛型 Stack 类型一样。扩展定义后, 你可以把数组当成 Container 使用。
泛型 Where 子句
类型限制, 让你在使用泛型函数或者类型时,可以在类型参数上定义需求。
给关联类型定义需求也是有用的。可以通过定义一个泛型where子句实现。 一个泛型wheare子句,让你可以要求关联类型必须符合一个协议, 或者特定类型参数和关联类型必须一样。一个泛型where子句以where关键字开始, 后面是关联类型的限制或者是类型和关联类型的相等关系。泛型where子句写在类型或者函数体花括号的前面。
下面的例子定义了一个泛型函数 allItemsMatch, 用来判断两个容器实例是否有相同顺序的相同元素。这个函数返回一个布尔值,如果所有元素都满足条件就返回 true 否则返回 false.
待比较的两个容器不需要是相同类型, 但是它们要有相同类型的元素。通过类型限制的组合跟一个泛型where子句来表示这第一点:
func allItemsMatch(_ someContainer: C1, _ anotherContainer: C2) -> Bool
where C1.ItemType == C2.ItemType, C1.ItemType: Equatable {
// Check that both containers contain the same number of items.
if someContainer.count != anotherContainer.count {
return false
}
// Check each pair of items to see if they are equivalent.
for i in 0..<someContainer.count {
if someContainer[i] != anotherContainer[i] {
return false
}
}
// All items match, so return true.
return true
}
这个函数有两个参数 someContainer 和 anotherContainer. someContainer 参数类型是 C1, anotherContainer 参数类型是 C2. C1 和 C2 是两个容器类型的类型参数,在函数调用的时候确定实际类型。
函数的两个类型参数要求如下:
C1 必须符合 Container 协议 (写作 C1: Container).
C2 也必须符合 Container 协议 (写作 C2: Container).
C1 的 ItemType 必须和C2的 ItemType 一样 (写作 C1.ItemType == C2.ItemType).
C1的 ItemType 必须符合 Equatable 协议 (写作 C1.ItemType: Equatable).
第一第二个要求定义在函数的类型参数列表里, 第三第四的要求定义在函数的泛型where子句中。
这些要求的意思是:
someContainer 是类型为C1的容器。
anotherContainer 是类型为C2的容器。
someContainer 和 anotherContainer 包含类型相同的元素。
someContainer 中的元素可以用不等于判断,看它们是否彼此不同。
第三第四个要求合并意思是, anotherContainer中的元素也可以用不等于判断, 因为它和someContainer 有着相同类型的元素。
allItemsMatch(::) 函数的这些要求使得它可以用来比较两个容器, 即使它们是不同的容器类型。
allItemsMatch(::) 函数一开始判断两个容器是否含有相同数量的元素。如果它们包含的元素的个数不一样, 它们就无法比较,函数返回false.
这个判断满足后, 函数使用for-in循环和半开区间运算符遍历someContainer中所有的元素。对于每个元素来说, 函数判断someContainer 的元素是否不等于anotherContainer 中对应的元素。如果两个元素不同, 说明两个容器不一样, 函数返回 false.
如果循环结束没有发现不匹配, 说明这两个容器是匹配的, 函数返回true.
这里 allItemsMatch(::) 函数响应如下:
var stackOfStrings = Stack()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
var arrayOfStrings = ["uno", "dos", "tres"]
if allItemsMatch(stackOfStrings, arrayOfStrings) {
print("All items match.")
} else {
print("Not all items match.")
}
// 打印 "All items match."
上面的例子创建了一个 Stack 实例来保存字符串, 然后把三个字符串压入栈。这个例子同时也创建了一个数组实例,它用和栈内容一样的字面量来初始化。尽管栈和数组是不同的类型, 不过它们都符合 Container 协议, 然后都包含相同类型的值。所以使用这两个容器作为参数,来调用 allItemsMatch(::) 函数。在上面的例子里, allItemsMatch(::) 函数正确的显示出两个容器中的所有元素都是匹配的。
使用泛型 Where 子句扩展
你也可以使用泛型 where 子句作为扩展的一部分。下面的例子扩展上面例子中的泛型栈结构, 添加了一个方法 isTop(_:).
extension Stack where Element: Equatable {
func isTop(_ item: Element) -> Bool {
guard let topItem = items.last else {
return false
}
return topItem == item
}
}
新方法 isTop(:) 首先判断栈不是空, 然后将所给项与栈顶项进行比较。如果不使用泛型 where 子句来实现, 你会有一个问题: isTop(:) 方法使用了 == 运算符, 但是栈的定义没有要求它的项是 equatable, 所有使用 == 运算符会导致一个编译错误。使用一个泛型 where 子句让你可以给扩展添加一个新需求, 这样扩展只有在栈中的项目是 equatable 才会添加 isTop(_:) 方法。
下面是是 isTop(_:) 方法执行的样子:
if stackOfStrings.isTop("tres") {
print("Top element is tres.")
} else {
print("Top element is something else.")
}
// 打印 "Top element is tres."
如果你尝试在一个元素不是等同的栈上调用 isTop(_:) 方法, 你会得到一个编译错误。struct NotEquatable { }var notEquatableStack = Stack()
let notEquatableValue = NotEquatable()
notEquatableStack.push(notEquatableValue)
notEquatableStack.isTop(notEquatableValue) // Error
你可以在扩展协议时使用泛型 where 子句。下面的例子扩展了 Container 协议, 添加了一个 startsWith(_:) 方法。
extension Container where Item: Equatable {
func startsWith(_ item: Item) -> Bool {
return count >= 1 && self[0] == item
}
}
startsWith(:) 方法首先确保容器至少有一项, 然后判断容器里的第一项是否匹配所给的项。任何符合 Container 协议的类型都快要使用这个新方法 startsWith(:) , 包含栈和数组, 只要这个容器的项目是 equatable.
if [9, 9, 9].startsWith(42) {
print("Starts with 42.")
} else {
print("Starts with something else.")
}
// 打印 "Starts with something else."
上面例子里的泛型 where 子句要求 Item 符合一个协议, 不过你可以要求 Item 是一个特定类型。例如:
extension Container where Item == Double {
func average() -> Double {
var sum = 0.0
for index in 0..<count {
sum += self[index]
}
return sum / Double(count)
}
}
print([1260.0, 1200.0, 98.6, 37.0].average())
// 打印 "648.9"
这个例子在容器里添加了一个 average() 方法, 它的 Item 类型是 Double. 它遍历容器里的项, 然后把它们相加, 然后除以容器的项数, 得到平均值。 它显式把 count 由 Int 转换为 Double.
你可以在一个泛型 where 子句中包含多个需求, 每个需求用逗号分开。
使用泛型 Where 子句关联类型
你可以在一个关联类型上包含一个泛型 where 子句。例如, 假设你想要一个包含迭代器版本的 Container, 就像使用标准库里的 Sequence 协议。这里你可以这样写:
protocol Container {
associatedtype Item
mutating func append(_ item: Item)
var count: Int { get }
subscript(i: Int) -> Item { get }
associatedtype Iterator: IteratorProtocol where Iterator.Element == Item
func makeIterator() -> Iterator
}
Iterator 上的泛型 where 子句要求迭代器遍历相同类型的元素, 并不关心迭代器的类型。makeIterator() 函数提供对容器迭代器的访问。
对于继承自其他协议的协议来说, 在协议声明中包含泛型 where 子句, 你可以给继承来的关联类型添加一个限制。例如, 下面的代码, 定义了一个 ComparableContainer 协议, 它要求 Item 遵守 Comparable 协议:
protocol ComparableContainer: Container where Item: Comparable { }
泛型下标
下标也可以是泛型, 它们可以包含泛型 where 子句。在 subscript 后面的尖括号里写占位符类型名, 然后在下标体的大括号前面写上泛型 where 子句。例如:
extension Container { subscript(indices: Indices) -> [Item]
where Indices.Iterator.Element == Int {
var result = [Item]()
for index in indices {
result.append(self[index])
}
return result
}
}
这个扩展添加了一个下标, 使用一个索引序列, 然后返回给定索引项目的数组。泛型下标的限制如下:
尖括号里的泛型参数 Indices 必须有相同的类型, 而且要符合 Sequence 协议。
下标只接受一个参数 indices, 它是 Indices 类型的实例。
泛型 where 子句要求序列迭代器遍历的元素是 Int 类型。这确保序列的索引和容器的索引类型一样。
综上所述, 这些限制意味着传给索引参数的值是一个整数序列。
137.访问控制
访问控制可以让限制其他资源文件和模块访问你的代码块。这个特性可以确保你隐藏代码实现的细节。然后指定一个首选的接口,通过它可以访问和使用代码。
你可以针对单个类型指定访问级别 (类, 结构体和枚举), 像属于这些类型的属性,方法,构造器和下标一样。协议可以限制到特定的上下文, 全局常量,变量和函数也可以。
除了提供多个级别的访问控制, 通过为特殊场景提供默认访问级别, Swift 减少指定显式访问控制级别的需要。事实上, 如果你写的是单一目的的应用, 你根本不需要指定显式访问控制级别。
备注 你的代码大部分可以使用访问控制 (属性, 类型, 函数等) ,它们被作为 “entities” 在下面部分引用, 为了简洁。
模块和源文件
Swift 的访问控制模型基于模块和源文件的概念。
一个模块是单独的代码分发单元—作为单独单元构建和传输的框架或者程序, 可以用Swift 的import 关键字被其他模块引入。
在Swift里,用Xcode 构建的每个目标都被作为单独的模块。 (例如应用的bundle或者框架)。如果你把代码组合成一个标准的独立框架—通过多个应用封装和重用这个代码—当它引入和用于一个应用时, 框架里定义的所有都会是独立模块的一部分。或者当它用在其他框架里的时候。
Swift里的源文件指的是模块中的源代码文件。尽管通常做法是在不同的源文件中定义独立的类型。一个单独的源文件可以定义多个类型,函数等等。
访问级别
Swift 为你的代码实体提供了五个不同的访问级别。这些访问级别和实体所在的源文件相关。同时也和源文件所属模块相关。
Open 访问和 public 访问让实体可以在任何定义它们的模块的源文件中使用, 也可以在引入该定义模块是其他模块的源文件中使用。当框架指定了pulic接口时,你就可以使用 open 或者 public 访问。open 和 pulic访问的不同下面会描述。
Internal 访问让实体可以在任何定义它们的模块的源文件中使用, 但是不能在该模块之外的源文件里使用。当定义一个应用的内部结构体或者框架的内部结构体时,你可以使用internal 访问。
私有文件访问只允许实体在自己定义的源文件中使用。使用私有文件访问隐藏了某个功能的实现细节。
私有访问限制实体在封闭声明时使用。当某个功能实现细节用在单独声明时,使用私有访问来隐藏这些细节。
Open 是最高级别的访问权限, 私有访问是最低级别的访问权限。
Open 访问只适用于类和类的成员, 它跟public 访问不同之处如下:
带有public访问权限的类, 或者任何更严格的访问级别, 只能在它们定义的模块里子类化。
带有public访问权限的类成员, 或者任何更严格的访问级别, 只能在它们定义的模块里,被子类重写。
Open 类可以在它们定义的模块中被子类化, 引入该模块的其他任意模块也可以。
Open 类可以在它们定义的模块中被子类重写, 引入该模块的其他任意模块也可以。
让一个类显式open表明, 你可以考虑到了来自其他模块的代码影响,这个模块使用这个类作为一个子类。你也相应的设计了自己的类的代码。
访问级别的指导原则
Swift 中的访问级别遵守统一的指导原则: 实体不可以定义成另一种低级别的实体。
例如:
一个 public 变量不能定义成internal, file-private, 或者 private 类型, 因为这个类型不能像pulic变量一样到处使用。
一个函数不能有比它的参数类型和返回类型更高的访问级别。因为函数不能用在它的组合类型不适用于周围代码的地方。
默认访问级别
如果你没有指定显式的访问级别,所有的代码中的实体会有一个默认的内部访问级别。这样做的结果是, 大多数情况下,你都不需要指定一个显式的访问级别。
单目标应用的访问级别
在你写一个单目标的应用的时候, 你的程序代码通常自包含在应用里,不需要给模块外部使用。默认的内部访问级别已经满足要求。因此, 你无需指定一个自定义的访问级别。不过你可能想把部分代码标记成 file private 或者 private,为了因此内部实现细节。
框架访问级别
当你开发一个框架的时候, 把对外的接口标记为 open 或者 public,这样它就可以被其他模块看到和访问, 比如引入这个框架的应用。 对外公开的接口是框架的API.
备注 框架的所有内部实现细节依然可以使用默认的内部访问级别, 如果想对框架内部其他代码隐藏实现细节,可以标记为 private 或者 file. 如果你想让它成为框架的API,你就需要把实体标记为 open 或者 public.
单元测试目标的访问级别
当你用单元测试目标写应用的时候, 你的代码需要对这个模块可用,为了能够测试。默认情况下, 只有标记为 open 或者 public 的实体才可以被其他模块访问。不过, 如果你使用@testable属性为产品模块标记引入声明并且使用可测试编译产品模块,单元测试目标可以访问任意内部实体。
访问控制语法
通过在实体前放置 open, public, internal, fileprivate, 或者 privateDefine 修饰符来给实体定义访问级别:
public class SomePublicClass {}
internal class SomeInternalClass {}
fileprivate class SomeFilePrivateClass {}
private class SomePrivateClass {}
public var somePublicVariable = 0
internal let someInternalConstant = 0
fileprivate func someFilePrivateFunction() {}
private func somePrivateFunction() {}
除非另有说明, 默认访问级别都是内部的。也就说说 SomeInternalClass 和 someInternalConstant 即使不写访问级别修饰符, 它们依然有内部访问级别:
class SomeInternalClass {} // implicitly internal
let someInternalConstant = 0 // implicitly internal
自定义类型
如果你想给一个自定义类型指定显式的访问级别, 在定义类型的时候指定。在访问级别允许的地方,新类型可以随便使用。例如, 如果你定义了一个 file-private 的类, 这个类只能用作属性的类型,函数的参数或者返回类型, 而且只能在类定义的源文件中。
一个类型的访问控制级别同样影响这个类型成员的默认访问级别 (它的属性,方法,构造器和下标)。如果类型的访问级别是 private 或者 file private, 它的成员的默认访问级别也将是 private 或者 file private. 如果类型的访问级别是 internal 或者 public, 它的成员的默认访问级别将会是 internal.
重要:一个 public 类型默认有 internal 成员, 而不是public 成员。如果你要成员也是 public, 你必须显式标记。这个可以保证发布的API是你想要发布的, 避免内部使用的代码作为API发布的错误。
public class SomePublicClass { // explicitly public class
public var somePublicProperty = 0 // explicitly public class member
var someInternalProperty = 0 // implicitly internal class member
fileprivate func someFilePrivateMethod() {} // explicitly file-private class member
private func somePrivateMethod() {} // explicitly private class member
}
class SomeInternalClass { // implicitly internal class
var someInternalProperty = 0 // implicitly internal class member
fileprivate func someFilePrivateMethod() {} // explicitly file-private class member
private func somePrivateMethod() {} // explicitly private class member
}
fileprivate class SomeFilePrivateClass { // explicitly file-private class
func someFilePrivateMethod() {} // implicitly file-private class member
private func somePrivateMethod() {} // explicitly private class member
}
private class SomePrivateClass { // explicitly private class
func somePrivateMethod() {} // implicitly private class member
}
元组类型
元组类型的访问级别是元组里类型中最严格的那个。例如, 如果你用两个不同的类型组成一个元组, 一个用 internal 访问,另外一个用 private 访问, 那么元组的访问级别会是 private.
备注:元组不像类,结构体和函数那样有独立的定义方式。一个元组类型的访问级别在定义时自动推断,不需要显式指定。
函数类型
函数的访问级别要计算参数和返回类型中最严格的。如果函数计算的访问级别不符合上下文的默认情况,你就要在定义函数时显式指定。
下面的例子定义了一个全局函数 someFunction(), 没有提供一个特定的访问级别修饰符。你可能希望函数有默认的 “internal”的访问级别, 但是情况不是这样。事实上, 下面的写法,someFunction() 将不能编译:
func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// function implementation goes here
}
函数的返回值是由两个自定义类组合成的元组。一个类定义成 “internal”, 另外一个类定义成 “private”. 因为, 元组类型的访问级别是 “private” .
因为这个函数的返回类型是 private, 为了函数声明的有效性,你必须标记整个函数的访问级别是 private modifier:
private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// function implementation goes here
}
用public或者internal 修饰符标记 someFunction() 是无效的, 使用默认的internal也没有用, 因为函数的 public 或者 internal 用户可能没有权限访问用在函数返回类型的私有类。
枚举类型
枚举的每个分支都会自动获得和枚举一样的访问级别。你不能给单独的分支指定访问级别
在下面的例子里, CompassPoint 枚举有一个显式的访问级别 “public”. 枚举的分支 north, south, east, 和 west 的访问级别因此也是 “public”:
public enum CompassPoint {
case north
case south
case east
case west
}
原始值和关联类型
枚举中所有原始值和管理类型用到的类型访问级别至少要和枚举一样高。如果枚举访问级别是internal,原始值的访问级别就不能是private.
嵌套类型
在private中定义的嵌套类型访问级别自动为 private. 在 file-private 中定义的嵌套类型访问级别自动为 file private. 在public或者internal中定义的嵌套类型访问级别自动为 internal. 如果想让在public 中定义的嵌套类型成为public, 你必须显式声明。
子类化
你可以子类化任何可以在当前上下文中访问中的类。子类的访问级别不能高过超类—例如, 不能给internal超类写一个public的子类。
除此之外, 你可以重写在特定上下文可见的类成员 (方法,属性,构造器和下标)。
重写的类成员比超类更容易访问。在下面的例子里, A 是一个 public 类,有一个 file-private 方法 someMethod(). B 是A的子类, 访问级别是 “internal”. 尽管如此, B 提供了一个重写的 someMethod(),它的访问级别是 “internal”, 比超类版本的方法级别要高:
public class A {
fileprivate func someMethod() {}
}
internal class B: A {
override internal func someMethod() {}
}
甚至,子类成员可以调用超类成员,即使超类成员的访问级别低于子类成员, 只要访问发生在允许访问的上下文中:
public class A {
fileprivate func someMethod() {}
}
internal class B: A {
override internal func someMethod() {
super.someMethod()
}
}
因为超类 A 和子类 B 在同一个源文件里定义, B 的 someMethod()方法可以有效调用 super.someMethod().
常量,变量,属性和下标
一个常量,变量,属性或属性不能比它的类型更 public. 用 private 类型来写一个public属性是无效的。相似的, 一个下标不能比它的索引和返回类型更public.
private var privateInstance = SomePrivateClass()
Getters 和 Setters
常量,变量,属性和下标的Getters 和 setters 自动和它们所属的常量,变量,属性和下标的访问的级别一样。
你可以给 setter 比对应getter 更低的访问级别, 来限制变量,属性或者下标读写的范围。通过写 fileprivate(set), private(set), 或者 internal(set)来指定访问级别。
备注 这个规则适用于存储属性和计算属性。尽管你没有为一个存储属性写显式的 getter 和 setter, Swift 仍然会合成一个隐式的 getter 和 setter, 用来访问存储属性的备份存储。用 fileprivate(set), private(set), 和 internal(set) 来改变这个合成setter的访问级别, 跟计算属性的显式setter使用的方法完全一样。
下面的例子定义了一个结构体 TrackedString, 用来跟踪一个字符串属性改变的次数:
struct TrackedString {
private(set) var numberOfEdits = 0
var value: String = "" {
didSet {
numberOfEdits += 1
}
}
}
TrackedString 结构体定义了一个存储字符串属性 value, 初始值为空。这个结构体同时定义了存储整型属性 numberOfEdits, 它用来跟踪value被改变的次数。通过在value属性上使用didSet属性观察者来实现跟踪。每次value属性设置新值的时候,它就把 numberOfEdits 值加1.
TrackedString 结构体和 value 属性没有显式提供访问级别修饰符, 所以它们默认的访问级别是 internal. 不过, numberOfEdits 属性的访问级别标记为 private(set),表明这个属性的 getter的访问级别仍然是 internal, 但是这个属性只能在结构体实现的代码里使用 setter. 这使得 TrackedString 可以在内部修改 numberOfEdits 属性, 但是也表示这个属性对于外部代码来说是只读的—包括 TrackedString 的扩展。
如果你创建一个 TrackedString 实例然后修改它的字符串 value 值几次, 你会看到 numberOfEdits 属性值随着变化次数一起更新:
var stringToEdit = TrackedString()
stringToEdit.value = "This string will be tracked."
stringToEdit.value += " This edit will increment numberOfEdits."
stringToEdit.value += " So will this one."
print("The number of edits is \(stringToEdit.numberOfEdits)")
// 打印 "The number of edits is 3"
尽管你可以在其他源文件查询 numberOfEdits 属性的当前值, 但是你不能进行修改。这个限制保护结构体编辑跟踪功能的实现细节。
如果需要,你可以给getter和setter方法指定显式的访问级别。下面的例子把TrackedString 定义成public.因此结构体的成员默认的访问级别是 internal. 你可以设置 numberOfEdits 属性的 getter 是 public的, 它的属性 setter 是 private的, 通过合并 public 和 private(set) 的访问修饰符:
public struct TrackedString {
public private(set) var numberOfEdits = 0
public var value: String = "" {
didSet {
numberOfEdits += 1
}
}
public init() {}
}
构造器
自定义构造器可以指定一个访问级别,这个级别小于或者等于它构造的类型。唯一的区别是必须的构造器。一个必须构造器访问级别必须跟它所属的类一致。
跟函数和方法参数一样, 构造器的参数类型不能比构造器拥有的访问级别更加私有。
默认构造器
就像默认构造器中描述的那样, Swift 会自动为所有结构体或者基类提供一个没有参数的默认构造器,这些结构体或者基类给所有属性提供了默认值,但是没有提供任何的构造器。
默认构造器的访问级别和它要构造的类型是一样的, 除非这个类型定义为 public. 对于定义为public的类型来说, 默认构造器访问级别是 internal. 在其他模块使用时,如果你想用无参数的构造器来构造 public 类型, 你必须显式定义一个 public 无参数构造器。
结构体类型的默认成员构造器
如果结构体的存储属性是private的,结构体的默认成员构造器就是 private的。同样的, 如果结构体任意一个存储属性是file private, 构造器也是 file private. 否则, 构造器的访问级别是 internal.
和上面的默认构造器一样, 在其他模块使用时, 如果你想用一个成员构造器来构造一个 public 类型的话, 你必须提供一个public成员构造器。
协议
如果你想要给一个协议类型指定一个显式的访问级别, 就在协议定义的时候这么做。这个可以让你创建协议, 这个协议只能在某些允许访问的上下文中采用。
协议定义中每个需求的访问级别和协议的访问级别是一样的。你不能把需求设置成协议不支持的访问级别。这可以保证采用协议的类型可以看见所有的需求。
备注 如果你定义了一个 public 协议, 协议的需求实现时要求一个 public 访问级别。这个行为不同于其他类型, public 类型定义意味着类型成员的访问级别是 internal.
协议继承
如果定义了一个新协议,它继承自一个存在的协议, 新协议的访问级别最多和继承协议的级别一样。例如, 已存在的协议访问级别是internal, 你写的新协议却是是 public.
协议一致性
一个类型可以符合一个访问级别比自己低的协议。例如, 你可以定义一个 public 类型用在其他模块里。如果它符合一个 internal 协议,就只能用在 internal 协议的定义模块内。
一个类型符合某个协议的上下文,访问级别是这个类型和协议中最小的一个。如果一个类型是 public, 但是协议是 internal, 这个类型的一致性协议也是 internal.
一个类型符合一个协议或者扩展符合一个协议,你必须确保类型对协议需求的实现,至少和类型的一致性协议有一样的访问级别。例如, 如果一个public 类型符合一个 internal, 这个类型实现的协议需求必须是 “internal”.
备注 在 Swift 里, 跟在 Objective-C里一样, 协议一致性是全局的—不可能在同样的程序里,类型以两种不同的方式来符合一个协议。
扩展
你可以在任何访问权限的上下文中扩展一个类,结构体或者枚举。扩展中添加的类型成员和被扩展类型中声明的类型成员有着一样的访问级别。如果你扩展一个 public 或者 internal 类型, 你添加的任何类型成员默认访问级别是 internal. 如果你扩展一个 file-private 类型, 你添加的所有类型成员的访问级别都是file private. 如果你扩展一个 private 类型, 你添加的任何类型成员访问级别都是 private.
另外, 你可以用显式访问级别修饰符来标记一个扩展,来为定义在扩展里的所有的成员设置一个新的默认访问属性。单个类型成员的扩展里依然可以重写这些新的默认级别。
使用扩展添加协议一致性
如果你用扩展来添加协议一致性,你就不能为扩展提供一个显式的访问级别修饰符。相反, 协议自己的访问级别,通常用来为在扩展中实现的协议需求提供默认访问级别。
泛型
泛型类型和泛型函数的访问级别, 是它们自身的访问级别和它们的类型参数的任何类型限制的访问级别之间最小的那个。
类型别名
你定义的所有类型别名,因为访问控制的目的,会被看做是不同的类型。一个类型别名的访问级别小于或者等于这个类型。例如, 一个private 类型的别名可以是一个 private, file-private, internal, public, 或者 open type的别名, 但是一个 public 类型别名不能是一个 internal, file-private, 或者 private 类型的别名。
备注 这个规则也适用于用来满足协议一致性的关联类型的类型别名。
138.高级运算符
除了基本运算符之外, Swift 提供了一些高级运算符来进行更复杂的值操作。包括位和位移运算符。
跟C的算术运算符不同, Swift 的算术运算符默认不会溢出。溢出会被捕获和报错。 选择溢出行为, 使用 Swift 的溢出算术运算符, 例如溢出加运算符 (&+). 所有溢出算术运算符都是以 (&)开始。
当你定义结构体,类和枚举的时候, 为这些自定义类型实现自己的标准Swift运算符是很有用的。Swift 让提供这些实现变得容易,并且能精确决定每种类型的行为。
你不会被限定在预置运算符上。Swift 给你足够的自由,用自定义的优先级和指定值,来定义你自己的中缀,前缀,后缀和赋值运算符。这些运算符的用法和预置运算符一样, 你甚至可以扩展已存在的类型来支持自定义的运算符。
位运算符
位运算符可以操作数据结构里的单个数据位。它们通常用于低级别编程, 例如图形编程和设备驱动编写。使用外部资源数据时,位运算符也很有用, 例如编解码数据。
Swift 支持C中所有的位运算符, 描述如下。
位 NOT 运算符
位 NOT 运算符 (~) 把所有位转换成一个数:
位 NOT 运算符是一个前缀运算符, 直接出现在操作数的前面, 没有空格:
let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits // 等于 11110000
UInt8 整数有8位,可以存储0到255之间的任何值。这个例子用二进制值00001111来初始化一个 UInt8 整数, 它的前四位全是0,后四位全是1. 它等于十进制的 15.
位 NOT 运算符用来创建一个新常量 invertedBits, 它等于 initialBits, 不过所有位都是反转的。0变成1, 1变成0. invertedBits 的值是 11110000, 它等于十进制的 240.
位 AND 运算符
位 AND 运算符 (&) 合并两个数的位。它返回一个新的数组,如果两个输入数的位都是1,这个新数的位才是1:
在上面的例子里, firstSixBits 和 lastSixBits 中间四位都是 1. 位 AND 运算符合并它们变成 00111100, 它等于十进制的 60:
let firstSixBits: UInt8 = 0b11111100
let lastSixBits: UInt8 = 0b00111111
let middleFourBits = firstSixBits & lastSixBits // 等于 00111100
位 OR 运算符
位 OR 运算符 (|) 比较两个数的位。如果两个数任意一个数位为1,这个运算符返回的数位就是1:
在上面的例子里, someBits 和 moreBits 不同位设置为 1. 位 OR 运算符合并它们变成 11111110, 它等于一个无符号十进制254:
let someBits: UInt8 = 0b10110010
let moreBits: UInt8 = 0b01011110
let combinedbits = someBits | moreBits // 等于 11111110
位 XOR 运算符
位 XOR 运算符, 或者 “异或运算符” (^), 比较两个数的位。如果两个数位不同返回1,如果相同则返回0:
let firstBits: UInt8 = 0b00010100
let otherBits: UInt8 = 0b00000101
let outputBits = firstBits ^ otherBits // 等于 00010001
左右移位运算符
左移位运算符 (<<) 和右移位运算符 (>>) 往左或者往右移动数位, 规则如下。
左移位和右移位实际上是乘以或者除以2. 左移一个整数1位相当于乘以2, 而右移一个整数1位相当于除以2.
无符号整数移动
无符号整数位移表现如下:
左移或者右移请求数量的位。
超出整数存储范围的移位被舍弃。
左移或者右移后,缺失的位用0填充。
这个方法叫逻辑移位。
下面这个图展示了 11111111 << 1 和 11111111 >> 1 的结果。蓝色数字是要移动的, 灰色数字是要舍弃的, 橙色的0是填充的:
下面是位移动在Swift 代码里的表现:
let shiftBits: UInt8 = 4 // 00000100 in binary
shiftBits << 1 // 00001000
shiftBits << 2 // 00010000
shiftBits << 5 // 10000000
shiftBits << 6 // 00000000
shiftBits >> 2 // 00000001
你可以使用位移动在其他数据类型里进行编解码:
let pink: UInt32 = 0xCC6699
let redComponent = (pink & 0xFF0000) >> 16 // redComponent 是 0xCC, 或者 204
let greenComponent = (pink & 0x00FF00) >> 8 // greenComponent 是 0x66, 或者 102
let blueComponent = pink & 0x0000FF // blueComponent 是 0x99, 或者 153
这个例子使用了一个 UInt32 类型的常量 pink 来保存粉色的CSS颜色值。CSS 颜色值 #CC6699 十六进制形式写作 0xCC6699. 经过位移 AND (&)和右移运算符(>>)操作, 这个颜色会被分解为 red (CC), green (66), 和 blue (99) .
红色部分通过把数字 0xCC6699 和 0xFF0000进行位AND获取。 0xFF0000 中的0 “掩藏”了 0xCC6699的第二个和第三个字节, 导致 6699 被忽略,只留下 0xCC0000 的结果。
然后把这个数字向右移动16位 (>> 16). 十六进制数字每对字母使用8位, 所以向右移动16位把 0xCC0000 转化为 0x0000CC. 它等于 0xCC, 它的十进制值是 204.
类似的, 绿色部分通过把数字 0xCC6699 和 0x00FF00进行位AND获取, 它的输出值是 0x006600. 然后把输出值向右移动8位 0x66, 它的十进制值是 102.
最后, 蓝色部分通过把数字 0xCC6699 和 0x0000FF进行位AND获取, 它的输出值是 0x000099. 它不需要向右移动, 因为 0x000099 已经等于 0x99, 它的十进制值是 153.
有符号整数移动
有符号整数的移动比无符号的要复杂, 因为有符号整数是用二进制表示的(为了简单,下面的例子用8位有符号整数展示, 不过这个原则适用于任何大小的有符号整数。)
有符号整数使用第一个数位来表示正负 (标志位)。 0表示正数, 1表示负数。
剩余位用来存储实际的值。正数的存储和无符号整数的方式是一样的, 从0往上数。这里是4在Int8中的数位的形式:
标志位是 0 (意思是正数), 7个数值位正好是数字 4, 用二进制符号表示。
负数存储是不同的。它们存储的值是绝对值减去2的n次方。这里n是数值位的数字。一个8位数有7个数值位, 所以2的7次方, 或者 128.
这里是-4在Int8中数位的形式 -4:
这次符号位是 1 (意思是负数), 七位数值位值是 124 (128 - 4):
负数编码是一个二进制补码表示。这似乎不是负数的常见表示方法, 但是它有几个优点。
首先, 你可以把-4加-1, 可以进行8位的简单二进制加法 (包括标志位), 完成后舍弃不符合8位的:
其次, 二进制补码表示让你可以像正数那样移动负数的数位。向左移动后依然会翻倍, 向右移动后会减半。为了实现这个, 当有符号整数向右移动时,使用额外的规则: 当你向右移动有符号整数时, 和无符号整数规则一样, 但是左边空出来的位要用标志位填充, 而不是0.
这个行为保证有符号整数向右移动后,有相同的标志位。 也就是算术移位。
由于正负数存储的特殊方式, 向右移动它们接近于0. 移动过程中保持标志位不变,意味着负数在接近0过程中依然是负数。
溢出运算符
如果你尝试向一个整数常数或者变量插入无法保存的值, 默认情况下, Swift 会报错而不是允许无效值的创建。当你使用过大或者过小值的时候,这个规则可以提供额外的安全性。
例如, Int16 整数范围是 -32768 到 32767. 尝试存储超过这个范围的数字会导致错误:
var potentialOverflow = Int16.max
// potentialOverflow equals 32767, which is the maximum value an Int16 can hold
potentialOverflow += 1
// 这个会报错
当值变的过大或者过小的时候,提供错误处理,在给边界值条件编码时,会更加灵活。
不过, 当你特别想要一个溢出条件来截断可用位数的时候, 你可以选择这个行为而不是触发一个错误。Swift 提供了三个算术溢出运算符,来为整数计算选择溢出行为。这些运算符都以(&)开始:
溢出加 (&+)
溢出减 (&-)
溢出乘 (&*)
值溢出
负数和整数都可以溢出。
这里有一个例子,展示当一个无符号整数在正数方向溢出时,会发生什么, 使用的是溢出加运算符 (&+):
var unsignedOverflow = UInt8.max
// unsignedOverflow 等于 255, 它是UInt8可以保存的最大值
unsignedOverflow = unsignedOverflow &+ 1
// unsignedOverflow 现在等于 0
变量 unsignedOverflow 使用UInt8 的最大值初始化nt8 (255, 或者 11111111). 然后使用溢出加运算符加1. 这个让它的二进制表示正好超过UInt8可以保存的最大值,这个导致了溢出, 如下表所示。溢出加之后这个值00000000依然在UInt8的界限内。
相似的事情会发生在无符号数向负数方向的溢出上。下面是使用了溢出减运算符的例子:
var unsignedOverflow = UInt8.min
// unsignedOverflow 等于 0, 是UInt8可以保存的最小值
unsignedOverflow = unsignedOverflow &- 1
// unsignedOverflow 现在等于 255
UInt8可以保存的最小值是0, 或者二进制 00000000. 如果使用溢出减运算符减1, 这个数字会溢出变成 11111111, 或者十进制 255 .
溢出也会发生在有符号整数。有符号整数的加减法以位形式执行, 标志位也参与加减。
var signedOverflow = Int8.min
// signedOverflow 等于 -128, 是Int8可以保存的最小值
signedOverflow = signedOverflow &- 1
// signedOverflow 现在等于 127
Int8保存的最小值是 -128, 或者二进制 10000000. 使用溢出减减1,结果是 01111111, 它会切换标志位然后得正数 127, 它是Int8可以保存的最大正数值。