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可以保存的最大正數值。