136.泛型
泛型代碼讓你可以寫出靈活,可重用的函數(shù)和類型,它們可以使用任何類型,受你定義的需求的約束蒿往。你可以寫出代碼,避免重復而且可以用一個清晰抽象的方式來表達它的意圖训柴。
泛型是Swift中最有力的特征之一, 而且大部分Swift的標準庫是用泛型代碼建立的衙传。事實上, 在整個語言教程中惊搏,你一直在使用泛型,盡管你沒有意識到這點哲鸳。例如, Swift 的數(shù)組和字典類型都是泛型集合褥民。 你可以創(chuàng)建一個整數(shù)數(shù)組,一個字符串數(shù)組,甚至是Swift允許創(chuàng)建的任何類型的數(shù)組勃痴。相似的, 你可以創(chuàng)建字典來保存任何指定類型的值, 什么類型并沒有限制谒所。
泛型解決的問題
這里有一個標準的非泛型的函數(shù) swapTwoInts(::), 用來交換兩個整數(shù)值:
func swapTwoInts(_ a: inout Int, _ b: inout Int) {
let temporaryA = a
a = b
b = temporaryA
}
這個函數(shù)用了輸入輸出參數(shù)來交換a和b的值。
swapTwoInts(::) 函數(shù)把b的原始值交換到a, a的原始值到b. 你可以調用這個函數(shù)來交換兩個整型變量中的值:
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(::) 函數(shù)是有用的, 不過只能用于整數(shù)沛申。如果你想交換兩個字符串, 或者兩個浮點數(shù), 你就要寫更多的函數(shù), 比如swapTwoStrings(::) 和 swapTwoDoubles(::) 函數(shù):
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(::) 的函數(shù)體是一樣的劣领。唯一不同的是它們接受的值的類型 (Int, String, 和 Double).
寫一個可以交換任何類型值的函數(shù),可能更有用和靈活。泛型代碼讓你可以寫出這種函數(shù)铁材。(這些函數(shù)的泛型版本會在下面定義尖淘。)
備注:在所有三個函數(shù)中, 重要的是a和b的類型要一樣奕锌。如果a和b的類型不一樣, 就不可能交換它們兩個的值。 Swift 是一門類型安全的語言, 不允許把一個字符串和浮點數(shù)進行交換村生。如果這樣做,會報一個編譯期錯誤惊暴。
泛型函數(shù)
泛型函數(shù)可以使用任何類型。這里有一個上面 swapTwoInts(::)函數(shù)的泛型版本 swapTwoValues(::):func swapTwoValues(_ a: inout T, _ b: inout T) {? ? let temporaryA = a? ? a = b? ? b = temporaryA}swapTwoValues(::) 函數(shù)體和 swapTwoInts(::) 函數(shù)體是一樣的趁桃。不過, swapTwoValues(::) 函數(shù)第一行跟swapTwoInts(::) 稍微有點不一樣辽话。下面是第一行的比較:func swapTwoInts(_ a: inout Int, _ b: inout Int)func swapTwoValues(_ a: inout T, _ b: inout T)
泛型版本的函數(shù)用了一個占位符類型名(這里叫T) ,而不是使用實際的類型名 (比如 Int, String, 或者 Double). 這個占位符類型名不說T是到底是什么, 但是它表明a和b是同樣的類型 T, 無論T表示什么。每次swapTwoValues(::)函數(shù)調用的時候,再決定T是什么類型卫病。
其他的不同是泛型函數(shù)名后面跟著一個T包括在尖括號中 (). 括號告訴 Swift ,T 在 swapTwoValues(::) 函數(shù)定義中是一個占位符類型名油啤。因為 T 是一個占位符, Swift 不能找到真正的類型 T.
現(xiàn)在可以像調用swapTwoInts一樣調用 swapTwoValues(::) 函數(shù), 不過你可以傳入兩個任何類型的值, 只要兩個值的類型是一樣的。每次調用 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(::) 函數(shù)是受到Swift 標準庫函數(shù)swap的啟發(fā)益咬。它可以在你的應用里直接使用。如果你需要 swapTwoValues(::) 函數(shù)的功能, 你可以使用 Swift 已存在的 swap(::) 函數(shù)而不用自己重新實現(xiàn)屹逛。
類型參數(shù)
上面的 swapTwoValues(::) 例子, 占位符類型 T 是類型參數(shù)的例子础废。類型參數(shù)指定和命名一個占位符類型, 直接寫在函數(shù)名的后面, 在一對尖括號里面 (例如 ).
只要你指定了一個類型參數(shù), 你就可以用它來定義一個函數(shù)的參數(shù)類型 (例如 swapTwoValues(::) 函數(shù)里的a和b), 或者作為函數(shù)的返回值類型, 或者在函數(shù)體中用作一個類型注釋。每種情況下, 函數(shù)調用時,類型參數(shù)會被真實的類型替換罕模。
你可以在尖括號里寫上多個類型參數(shù),來提供更多的類型參數(shù)评腺。用逗號分開就行。
命名類型參數(shù)
在大多數(shù)情況下, 類型參數(shù)有描述性的名字, 例如 Dictionary 中的Key 和 Value , Array里的Element, 它會告訴讀者類型參數(shù)和泛型類型或者所在函數(shù)的關系淑掌。不過, 當它們之間沒有一個有意義的關系時, 通常做法是用單個字母來給它們命名,比如 T, U, 和 V, 比如上面 swapTwoValues(::) 函數(shù)中的T蒿讥。
備注 用駝峰式方法給參數(shù)類型命名來表明它們是一個占位符類型,而不是一個值。(例如 T 和 MyTypeParameter).
泛型類型
除了泛型函數(shù), Swift 也可以定義泛型類型抛腕。它們是可以使用任何類型的類,結構體和枚舉芋绸。跟數(shù)組和字典有著相似的方式。
這部分內容展示如何寫一個泛型集合 Stack. 棧是有序集合, 跟數(shù)組類似, 但是操作更加嚴格担敌。數(shù)組允許任何位置的項的插入和移除摔敛。棧只允許在集合尾部添加 (壓棧)。類似的, 棧只允許項目從集合尾部移除 (出棧)全封。
備注:UINavigationController 使用棧來模擬在導航層次中的視圖控制器马昙。調用 UINavigationController 的 pushViewController(:animated:) 方法在導航棧上添加一個視圖控制器, 調用 popViewControllerAnimated(:) 方法從導航棧上移除一個視圖控制器。如果你要一個后進先出的方式來管理集合,椛层玻可以派上用場行楞。
下面的圖展示了棧的壓棧和出棧的行為:
當前棧上有三個值。第四個值添加到棧頂⊥猎龋現(xiàn)在棧內有四個值, 最近的值在最上面子房。
棧頂?shù)闹当灰瞥蛘叱鰲!棾鲆粋€值后, 棧內現(xiàn)在再次是三個值。
這里有個非泛型版本的棧, 針對的是整型值的情況:
struct IntStack {
var items = [Int]()
mutating func push(_ item: Int) {
items.append(item)
? ? ?}
mutating func pop() -> Int {
return items.removeLast()
? ?}
}
這個結構體使用數(shù)組屬性items來保存棧內的值证杭。IntStack 提供了兩個方法, push 和 pop, 用來壓棧和出棧田度。這兩個方法都是 mutating, 因為它們要改變結構體的 items 數(shù)組。IntStack 類型只能用于整數(shù), 不過解愤。如果能定義一個泛型棧類可能會更有用, 它可以管理任何類型值每币。這里是一些代碼的泛型版本:
struct Stack{
var items = [Element]()
mutating func push(_ item: Element) {
items.append(item)
? ? ? }
mutating func pop() -> Element {
return items.removeLast()
? ? ? }
}
注意,事實上泛型版本的棧和非泛型的版本很像, 只不過有一個類型參數(shù) Element 取代了實際類型 Int. 這個類型名寫在結構體名的后面,放在一對尖括號里面().
Element 是一個占位符的名字。這個未來類型可以在結構體定義中作為元素使用琢歇。在這種情況下, Element 在三個地方用作占位符:
創(chuàng)建一個屬性items, 它是用Element 類型值來初始化的空數(shù)組。
指定 push(_:) 方法有一個參數(shù) item, 類型是 Element
指定 pop() 方法的返回值,類型是 Element
因為它是一個泛型類型, Stack可以用來創(chuàng)建Swift中任何有效的類型的棧, 跟字典和數(shù)組的用法類似梦鉴。
在方括號里寫上棧存儲類型來創(chuàng)建一個新的 Stack 實例李茫。例如, 創(chuàng)建一個字符串的棧, 這樣寫 Stack():
var stackOfStrings = Stack()
stackOfStrings.push("uno")
stackOfStrings.push("dos")
stackOfStrings.push("tres")
stackOfStrings.push("cuatro")
// 這個棧現(xiàn)在有4個字符串
下面是壓入四個值之后stackOfStrings 變化:
從棧中移除一個值并且返回棧頂?shù)闹? “cuatro”:
let fromTheTop = stackOfStrings.pop()
// fromTheTop 等于 "cuatro", 現(xiàn)在棧內有3個字符串
下面的彈出一個值后棧的變化:
擴展泛型類型
當你擴展一個泛型類型, 你不需要在擴展定義中提供一個類型參數(shù)列表肥橙。相反, 原類型定義的類型參數(shù)列表可以在擴展內部使用, 并且,使用原類型類型參數(shù)名在原來的定義中調用類型參數(shù)魄宏。
下面的例子擴展了泛型 Stack 類型,添加了一個只讀計算屬性 topItem, 它返回棧頂元素,而且不用彈出這個元素:
extension Stack {
var topItem: Element? {
return items.isEmpty ? nil : items[items.count - 1]
? ?}
}
topItem 屬性返回可選的Element 類型值。如果棧是空的, topItem 返回nil; 如果棧非空, topItem 返回items數(shù)組最后一個值存筏。
注意,這個擴展沒有定義類型參數(shù)列表宠互。相反, Stack 類型已存在的類型參數(shù)名, Element, 在擴展內部使用來表示topItem 是一個可選類型。
計算屬性topItem 現(xiàn)在可以使用 Stack 實例來訪問棧頂?shù)脑?而不用去移除它:
if let topItem = stackOfStrings.topItem {
print("The top item on the stack is \(topItem).")
}
// 打印 "The top item on the stack is tres."
類型限制
swapTwoValues(::) 函數(shù)和 Stack 類型可以使用任意類型椭坚。不過, 有時候對使用泛型函數(shù)和類型的類型使用限制是有用的予跌。 類型限制指定一個類型參數(shù)必須繼承自一個類,或者符合一個協(xié)議或者協(xié)議組合。
例如, Swift的字典類型對可以用作鍵的類型進行了限制, 字典的鍵類型必須是可哈希的善茎。就是說, 它必須提供一個方法讓自己獨一無二券册。 字典需要鍵可哈希,為了判斷特定鍵是否包含了一個值。如果沒有這個要求, 字典就不能判斷是否可以對一個鍵插入或者修改一個值垂涯。也不能通過給定的鍵找到一個值烁焙。
字典的鍵類型,這個需求是類型限制強制的。它規(guī)定鍵類型必須符合 Hashable 協(xié)議, 它定義在Swift 標準庫中耕赘。Swift 的所有基本類型默認都是可哈希的骄蝇。
創(chuàng)建泛型類型時,你可以定義自己的類型限制。這些限制提供泛型編程大部分能力操骡。諸如哈希特性類型的抽象概念,依據(jù)的是它們概念性的特征而不是它們的顯式類型九火。
可續(xù)限制語法
通過在類型參數(shù)名后放置一個單獨的類或者協(xié)議,然后用冒號分開,來寫類型限制。泛型函數(shù)的類型限制的基本語法顯示如下:func someFunction(someT: T, someU: U) {
// function body goes here
}
上面的假想函數(shù)有兩個參數(shù)当娱。第一個類型參數(shù), T, 類型限制是要求T是 SomeClass 的子類吃既。第二個類型參數(shù), U, 類型限制是要求U符合 SomeProtocol 協(xié)議。
類型限制的行為
這里有一個非泛型的函數(shù)findIndex(ofString:in:), 它有一個查找的字符串值和待查找的字符串數(shù)組跨细。findIndex(ofString:in:) 函數(shù)返回一個可選的整數(shù)值鹦倚。它是數(shù)組中第一個匹配字符串的索引, 如果找不到就返回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:) 函數(shù)可以用來在字符串數(shù)組查找一個字符串:
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"
這種查找字符串的方式只對字符串有用, 不過。 你可以寫一個泛型函數(shù)來處理其他類型冀惭。這里有一個你期待的泛型版本的 findIndex(ofString:in:)函數(shù), 叫 findIndex(of:in:). 注意,函數(shù)返回值仍然是 Int?, 因為函數(shù)返回的是可選的索引值, 不是來自數(shù)組的可選值震叙。 不過這個函數(shù)不能編譯, 原因在這個例子后面再解釋:
func findIndex(of valueToFind: T, in array:[T]) -> Int? {
for (index, value) in array.enumerated() {
if value == valueToFind {
return index
? ? ?}
? ? }
return nil
}
上面的函數(shù)不能編譯掀鹅。問題在于等號判斷, “if value == valueToFind”. 不是所有在Swift中的類型都可以用等號進行比較的。如果你創(chuàng)建自己的類或者結構體來表示一個復雜的數(shù)據(jù)模型, 這個類或者結構體‘等于’的意思不是Swift能夠理解的媒楼。因為這個原因, 它不能保證這個代碼對各種可能的T類型都有效, 當你嘗試編譯這個代碼的時候,就會報錯乐尊。
不過,沒有任何損失。Swift 標準庫定義了一個協(xié)議 Equatable, 它要求符合類型實現(xiàn)等于和不等于,來比較這個類型的任意兩個值划址。所有 Swift 的標準類型都自動支持這個協(xié)議扔嵌。
任何可以比較的類型都可以安全的使用 findIndex(of:in:) 函數(shù), 因為它保證支持等于運算符。為了說明這個事實, 在你定義函數(shù)時,你可以在類型參數(shù)定義的時候寫一個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:) 的參數(shù)類型寫作 T: Equatable, 意思是 “符合Equatable 協(xié)議的任意 T 類型夺颤×《校”
限制 findIndex(of:in:) 函數(shù)編譯成功了,然后可以使用任意可以比較的類型, 例如 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
關聯(lián)類型
當定義一個協(xié)議的時候, 有時候聲明一個或者多個關聯(lián)類型是很有用的。一個關聯(lián)類型給一個類型提供一個占位符名世澜。關聯(lián)類型使用的實際類型只要協(xié)議被采用才會指定独旷。關聯(lián)類型使用associatedtype 關鍵字來指定。
關聯(lián)類型的行為
這里有個Container協(xié)議的例子, 它聲明了一個關聯(lián)類型 ItemType:
protocol Container {
associatedtype ItemType
mutating func append(_ item: ItemType)
var count: Int { get }
subscript(i: Int) -> ItemType { get }
}
Container 協(xié)議定義了任何容器必須提供的三個必須的能力:
它必須要可以用append(_:)方法給容器添加新項目寥裂。
它必須可以通過count屬性訪問容器中項目的數(shù)量嵌洼。
它必須可以通過下標運算獲取到容器的每一項。
這個協(xié)議沒有指定怎么存儲項目或者它允許的類型封恰。這個協(xié)議只是指定了任何符合類型要提供的三個功能麻养。一個符合類型可以提供額外的功能, 只要它滿足三個必須要求。
任何符合 Container 協(xié)議的類型必須能夠指定它存儲值的類型诺舔。特別是, 它必須確保只有正確的類型才可以添加到容器, 它必須清楚下標返回的項目的類型回溺。
為了定義這三個必須要求, Container 協(xié)議需要一個方法去調用容器將要裝載的元素類型, 不用知道特定容器類型是什么。Container 協(xié)議需要指定,傳入append(_:) 方法的值必須和容器里的元素類型一樣混萝。容器下標返回的值的類型也要和容器里的元素類型一樣遗遵。
為了實現(xiàn)這點, Container 協(xié)議定義了一個關聯(lián)類型 ItemType, 寫作 associatedtype ItemType. 協(xié)議沒有定義 ItemType是什么—這個留給符合類型來提供。 盡管如此, ItemType 別名提供了一種方式來調用容器里的元素類型, 為了使用 append(_:) 方法和下標定義了一個類型逸嘀。以確保任何容器期望的行為被執(zhí)行车要。
這里是早前非泛型版本的 IntStack 類型, 采用和符合了 Container 協(xié)議:
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 類型實現(xiàn)了 Container 協(xié)議要求的三個功能。
此外, IntStack 指定,為了實現(xiàn) Container, 關聯(lián) ItemType 使用Int類型崭倘。typealias ItemType = Int 定義,為Container 協(xié)議的實現(xiàn),把抽象類型轉換為實際的Int類型翼岁。
由于 Swift 的類型推斷, 實際上你不需要聲明ItemType 為Int. 因為 IntStack 符合 Container 協(xié)議所有的要求, Swift 可以推斷使用的關聯(lián)類型 ItemType, 只要簡單查找 append(_:) 方法的參數(shù)類型和下標的返回類型。事實上, 如果你刪除上面的 typealias ItemType = Int, 一切都正常, 因為它知道什么類型用于 ItemType.
你也可以讓你泛型版本的 Stack 類型來符合 Container 協(xié)議:
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]
? }
}
這一次, 類型參數(shù) Element 用作 append(_:) 方法的參數(shù)類型和下標的返回類型司光。Swift 可以推斷 Element 是關聯(lián)類型, 在特定容器用作ItemType.
擴展存在的類型去指定關聯(lián)類型
你可以擴展存在的類型去符合一個協(xié)議琅坡。這個包含帶有關聯(lián)類型的協(xié)議。
Swift 的數(shù)組類型已經(jīng)提供了append(_:)方法, 一個count 屬性, 和一個帶有索引獲取元素的下標残家。這三個能力滿足 Container 協(xié)議的要求榆俺。這個意味著你可以擴展數(shù)組來符合 Container 協(xié)議。使用空擴展即可實現(xiàn)這個:
extension Array: Container {}
數(shù)組存在的 append(_:) 方法和下標讓 Swift 可以推斷使用ItemType的關聯(lián)類型, 和上面的泛型 Stack 類型一樣。擴展定義后, 你可以把數(shù)組當成 Container 使用茴晋。
泛型 Where 子句
類型限制, 讓你在使用泛型函數(shù)或者類型時,可以在類型參數(shù)上定義需求陪捷。
給關聯(lián)類型定義需求也是有用的∨瞪茫可以通過定義一個泛型where子句實現(xiàn)市袖。 一個泛型wheare子句,讓你可以要求關聯(lián)類型必須符合一個協(xié)議, 或者特定類型參數(shù)和關聯(lián)類型必須一樣。一個泛型where子句以where關鍵字開始, 后面是關聯(lián)類型的限制或者是類型和關聯(lián)類型的相等關系烁涌。泛型where子句寫在類型或者函數(shù)體花括號的前面苍碟。
下面的例子定義了一個泛型函數(shù) allItemsMatch, 用來判斷兩個容器實例是否有相同順序的相同元素。這個函數(shù)返回一個布爾值,如果所有元素都滿足條件就返回 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
}
這個函數(shù)有兩個參數(shù) someContainer 和 anotherContainer. someContainer 參數(shù)類型是 C1, anotherContainer 參數(shù)類型是 C2. C1 和 C2 是兩個容器類型的類型參數(shù),在函數(shù)調用的時候確定實際類型驰怎。
函數(shù)的兩個類型參數(shù)要求如下:
C1 必須符合 Container 協(xié)議 (寫作 C1: Container).
C2 也必須符合 Container 協(xié)議 (寫作 C2: Container).
C1 的 ItemType 必須和C2的 ItemType 一樣 (寫作 C1.ItemType == C2.ItemType).
C1的 ItemType 必須符合 Equatable 協(xié)議 (寫作 C1.ItemType: Equatable).
第一第二個要求定義在函數(shù)的類型參數(shù)列表里, 第三第四的要求定義在函數(shù)的泛型where子句中。
這些要求的意思是:
someContainer 是類型為C1的容器二打。
anotherContainer 是類型為C2的容器。
someContainer 和 anotherContainer 包含類型相同的元素掂榔。
someContainer 中的元素可以用不等于判斷,看它們是否彼此不同继效。
第三第四個要求合并意思是, anotherContainer中的元素也可以用不等于判斷, 因為它和someContainer 有著相同類型的元素。
allItemsMatch(::) 函數(shù)的這些要求使得它可以用來比較兩個容器, 即使它們是不同的容器類型装获。
allItemsMatch(::) 函數(shù)一開始判斷兩個容器是否含有相同數(shù)量的元素瑞信。如果它們包含的元素的個數(shù)不一樣, 它們就無法比較,函數(shù)返回false.
這個判斷滿足后, 函數(shù)使用for-in循環(huán)和半開區(qū)間運算符遍歷someContainer中所有的元素。對于每個元素來說, 函數(shù)判斷someContainer 的元素是否不等于anotherContainer 中對應的元素穴豫。如果兩個元素不同, 說明兩個容器不一樣, 函數(shù)返回 false.
如果循環(huán)結束沒有發(fā)現(xiàn)不匹配, 說明這兩個容器是匹配的, 函數(shù)返回true.
這里 allItemsMatch(::) 函數(shù)響應如下:
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."
上面的例子創(chuàng)建了一個 Stack 實例來保存字符串, 然后把三個字符串壓入棧凡简。這個例子同時也創(chuàng)建了一個數(shù)組實例,它用和棧內容一樣的字面量來初始化。盡管棧和數(shù)組是不同的類型, 不過它們都符合 Container 協(xié)議, 然后都包含相同類型的值精肃。所以使用這兩個容器作為參數(shù),來調用 allItemsMatch(::) 函數(shù)秤涩。在上面的例子里, allItemsMatch(::) 函數(shù)正確的顯示出兩個容器中的所有元素都是匹配的。
使用泛型 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 子句來實現(xiàn), 你會有一個問題: isTop(:) 方法使用了 == 運算符, 但是棧的定義沒有要求它的項是 equatable, 所有使用 == 運算符會導致一個編譯錯誤。使用一個泛型 where 子句讓你可以給擴展添加一個新需求, 這樣擴展只有在棧中的項目是 equatable 才會添加 isTop(_:) 方法胡嘿。
下面是是 isTop(_:) 方法執(zhí)行的樣子:
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
你可以在擴展協(xié)議時使用泛型 where 子句。下面的例子擴展了 Container 協(xié)議, 添加了一個 startsWith(_:) 方法资溃。
extension Container where Item: Equatable {
func startsWith(_ item: Item) -> Bool {
return count >= 1 && self[0] == item
? }
}
startsWith(:) 方法首先確保容器至少有一項, 然后判斷容器里的第一項是否匹配所給的項武翎。任何符合 Container 協(xié)議的類型都快要使用這個新方法 startsWith(:) , 包含棧和數(shù)組, 只要這個容器的項目是 equatable.
if [9, 9, 9].startsWith(42) {
print("Starts with 42.")
?} else {
print("Starts with something else.")
}
// 打印 "Starts with something else."
上面例子里的泛型 where 子句要求 Item 符合一個協(xié)議, 不過你可以要求 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. 它遍歷容器里的項, 然后把它們相加, 然后除以容器的項數(shù), 得到平均值溶锭。 它顯式把 count 由 Int 轉換為 Double.
你可以在一個泛型 where 子句中包含多個需求, 每個需求用逗號分開宝恶。
使用泛型 Where 子句關聯(lián)類型
你可以在一個關聯(lián)類型上包含一個泛型 where 子句。例如, 假設你想要一個包含迭代器版本的 Container, 就像使用標準庫里的 Sequence 協(xié)議。這里你可以這樣寫:
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() 函數(shù)提供對容器迭代器的訪問膏执。
對于繼承自其他協(xié)議的協(xié)議來說, 在協(xié)議聲明中包含泛型 where 子句, 你可以給繼承來的關聯(lián)類型添加一個限制。例如, 下面的代碼, 定義了一個 ComparableContainer 協(xié)議, 它要求 Item 遵守 Comparable 協(xié)議:
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
? ? }
}
這個擴展添加了一個下標, 使用一個索引序列, 然后返回給定索引項目的數(shù)組。泛型下標的限制如下:
尖括號里的泛型參數(shù) Indices 必須有相同的類型, 而且要符合 Sequence 協(xié)議毫痕。
下標只接受一個參數(shù) indices, 它是 Indices 類型的實例征峦。
泛型 where 子句要求序列迭代器遍歷的元素是 Int 類型。這確保序列的索引和容器的索引類型一樣消请。
綜上所述, 這些限制意味著傳給索引參數(shù)的值是一個整數(shù)序列栏笆。
137.訪問控制
訪問控制可以讓限制其他資源文件和模塊訪問你的代碼塊。這個特性可以確保你隱藏代碼實現(xiàn)的細節(jié)臊泰。然后指定一個首選的接口,通過它可以訪問和使用代碼蛉加。
你可以針對單個類型指定訪問級別 (類, 結構體和枚舉), 像屬于這些類型的屬性,方法,構造器和下標一樣。協(xié)議可以限制到特定的上下文, 全局常量,變量和函數(shù)也可以缸逃。
除了提供多個級別的訪問控制, 通過為特殊場景提供默認訪問級別, Swift 減少指定顯式訪問控制級別的需要针饥。事實上, 如果你寫的是單一目的的應用, 你根本不需要指定顯式訪問控制級別。
備注 你的代碼大部分可以使用訪問控制 (屬性, 類型, 函數(shù)等) ,它們被作為 “entities” 在下面部分引用, 為了簡潔需频。
模塊和源文件
Swift 的訪問控制模型基于模塊和源文件的概念丁眼。
一個模塊是單獨的代碼分發(fā)單元—作為單獨單元構建和傳輸?shù)目蚣芑蛘叱绦? 可以用Swift 的import 關鍵字被其他模塊引入。
在Swift里,用Xcode 構建的每個目標都被作為單獨的模塊昭殉。 (例如應用的bundle或者框架)苞七。如果你把代碼組合成一個標準的獨立框架—通過多個應用封裝和重用這個代碼—當它引入和用于一個應用時, 框架里定義的所有都會是獨立模塊的一部分∨捕或者當它用在其他框架里的時候蹂风。
Swift里的源文件指的是模塊中的源代碼文件。盡管通常做法是在不同的源文件中定義獨立的類型乾蓬。一個單獨的源文件可以定義多個類型,函數(shù)等等硫眨。
訪問級別
Swift 為你的代碼實體提供了五個不同的訪問級別。這些訪問級別和實體所在的源文件相關巢块。同時也和源文件所屬模塊相關礁阁。
Open 訪問和 public 訪問讓實體可以在任何定義它們的模塊的源文件中使用, 也可以在引入該定義模塊是其他模塊的源文件中使用。當框架指定了pulic接口時,你就可以使用 open 或者 public 訪問族奢。open 和 pulic訪問的不同下面會描述姥闭。
Internal 訪問讓實體可以在任何定義它們的模塊的源文件中使用, 但是不能在該模塊之外的源文件里使用。當定義一個應用的內部結構體或者框架的內部結構體時,你可以使用internal 訪問越走。
私有文件訪問只允許實體在自己定義的源文件中使用棚品。使用私有文件訪問隱藏了某個功能的實現(xiàn)細節(jié)靠欢。
私有訪問限制實體在封閉聲明時使用。當某個功能實現(xiàn)細節(jié)用在單獨聲明時,使用私有訪問來隱藏這些細節(jié)铜跑。
Open 是最高級別的訪問權限, 私有訪問是最低級別的訪問權限门怪。
Open 訪問只適用于類和類的成員, 它跟public 訪問不同之處如下:
帶有public訪問權限的類, 或者任何更嚴格的訪問級別, 只能在它們定義的模塊里子類化。
帶有public訪問權限的類成員, 或者任何更嚴格的訪問級別, 只能在它們定義的模塊里,被子類重寫锅纺。
Open 類可以在它們定義的模塊中被子類化, 引入該模塊的其他任意模塊也可以掷空。
Open 類可以在它們定義的模塊中被子類重寫, 引入該模塊的其他任意模塊也可以。
讓一個類顯式open表明, 你可以考慮到了來自其他模塊的代碼影響,這個模塊使用這個類作為一個子類囤锉。你也相應的設計了自己的類的代碼坦弟。
訪問級別的指導原則
Swift 中的訪問級別遵守統(tǒng)一的指導原則: 實體不可以定義成另一種低級別的實體。
例如:
一個 public 變量不能定義成internal, file-private, 或者 private 類型, 因為這個類型不能像pulic變量一樣到處使用官地。
一個函數(shù)不能有比它的參數(shù)類型和返回類型更高的訪問級別酿傍。因為函數(shù)不能用在它的組合類型不適用于周圍代碼的地方。
默認訪問級別
如果你沒有指定顯式的訪問級別,所有的代碼中的實體會有一個默認的內部訪問級別驱入。這樣做的結果是, 大多數(shù)情況下,你都不需要指定一個顯式的訪問級別赤炒。
單目標應用的訪問級別
在你寫一個單目標的應用的時候, 你的程序代碼通常自包含在應用里,不需要給模塊外部使用。默認的內部訪問級別已經(jīng)滿足要求亏较。因此, 你無需指定一個自定義的訪問級別莺褒。不過你可能想把部分代碼標記成 file private 或者 private,為了因此內部實現(xiàn)細節(jié)。
框架訪問級別
當你開發(fā)一個框架的時候, 把對外的接口標記為 open 或者 public,這樣它就可以被其他模塊看到和訪問, 比如引入這個框架的應用宴杀。 對外公開的接口是框架的API.
備注 框架的所有內部實現(xiàn)細節(jié)依然可以使用默認的內部訪問級別, 如果想對框架內部其他代碼隱藏實現(xiàn)細節(jié),可以標記為 private 或者 file. 如果你想讓它成為框架的API,你就需要把實體標記為 open 或者 public.
單元測試目標的訪問級別
當你用單元測試目標寫應用的時候, 你的代碼需要對這個模塊可用,為了能夠測試。默認情況下, 只有標記為 open 或者 public 的實體才可以被其他模塊訪問拾因。不過, 如果你使用@testable屬性為產(chǎn)品模塊標記引入聲明并且使用可測試編譯產(chǎn)品模塊,單元測試目標可以訪問任意內部實體旺罢。
訪問控制語法
通過在實體前放置 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 的類, 這個類只能用作屬性的類型,函數(shù)的參數(shù)或者返回類型, 而且只能在類定義的源文件中。
一個類型的訪問控制級別同樣影響這個類型成員的默認訪問級別 (它的屬性,方法,構造器和下標)蠢熄。如果類型的訪問級別是 private 或者 file private, 它的成員的默認訪問級別也將是 private 或者 file private. 如果類型的訪問級別是 internal 或者 public, 它的成員的默認訪問級別將會是 internal.
重要:一個 public 類型默認有 internal 成員, 而不是public 成員跪解。如果你要成員也是 public, 你必須顯式標記。這個可以保證發(fā)布的API是你想要發(fā)布的, 避免內部使用的代碼作為API發(fā)布的錯誤签孔。
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.
備注:元組不像類,結構體和函數(shù)那樣有獨立的定義方式。一個元組類型的訪問級別在定義時自動推斷,不需要顯式指定饥追。
函數(shù)類型
函數(shù)的訪問級別要計算參數(shù)和返回類型中最嚴格的图仓。如果函數(shù)計算的訪問級別不符合上下文的默認情況,你就要在定義函數(shù)時顯式指定。
下面的例子定義了一個全局函數(shù) someFunction(), 沒有提供一個特定的訪問級別修飾符但绕。你可能希望函數(shù)有默認的 “internal”的訪問級別, 但是情況不是這樣救崔。事實上, 下面的寫法,someFunction() 將不能編譯:
func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// function implementation goes here
}
函數(shù)的返回值是由兩個自定義類組合成的元組惶看。一個類定義成 “internal”, 另外一個類定義成 “private”. 因為, 元組類型的訪問級別是 “private” .
因為這個函數(shù)的返回類型是 private, 為了函數(shù)聲明的有效性,你必須標記整個函數(shù)的訪問級別是 private modifier:
private func someFunction() -> (SomeInternalClass, SomePrivateClass) {
// function implementation goes here
}
用public或者internal 修飾符標記 someFunction() 是無效的, 使用默認的internal也沒有用, 因為函數(shù)的 public 或者 internal 用戶可能沒有權限訪問用在函數(shù)返回類型的私有類。
枚舉類型
枚舉的每個分支都會自動獲得和枚舉一樣的訪問級別六孵。你不能給單獨的分支指定訪問級別
在下面的例子里, CompassPoint 枚舉有一個顯式的訪問級別 “public”. 枚舉的分支 north, south, east, 和 west 的訪問級別因此也是 “public”:
public enum CompassPoint {
case north
case south
case east
case west
}
原始值和關聯(lián)類型
枚舉中所有原始值和管理類型用到的類型訪問級別至少要和枚舉一樣高纬黎。如果枚舉訪問級別是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() {}
? ?}
甚至,子類成員可以調用超類成員,即使超類成員的訪問級別低于子類成員, 只要訪問發(fā)生在允許訪問的上下文中:
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)來指定訪問級別铐达。
備注 這個規(guī)則適用于存儲屬性和計算屬性。盡管你沒有為一個存儲屬性寫顯式的 getter 和 setter, Swift 仍然會合成一個隱式的 getter 和 setter, 用來訪問存儲屬性的備份存儲檬果。用 fileprivate(set), private(set), 和 internal(set) 來改變這個合成setter的訪問級別, 跟計算屬性的顯式setter使用的方法完全一樣瓮孙。
下面的例子定義了一個結構體 TrackedString, 用來跟蹤一個字符串屬性改變的次數(shù):
struct TrackedString {
private(set) var numberOfEdits = 0
var value: String = "" {
didSet {
numberOfEdits += 1
? ?}
? }
}
TrackedString 結構體定義了一個存儲字符串屬性 value, 初始值為空。這個結構體同時定義了存儲整型屬性 numberOfEdits, 它用來跟蹤value被改變的次數(shù)选脊。通過在value屬性上使用didSet屬性觀察者來實現(xiàn)跟蹤杭抠。每次value屬性設置新值的時候,它就把 numberOfEdits 值加1.
TrackedString 結構體和 value 屬性沒有顯式提供訪問級別修飾符, 所以它們默認的訪問級別是 internal. 不過, numberOfEdits 屬性的訪問級別標記為 private(set),表明這個屬性的 getter的訪問級別仍然是 internal, 但是這個屬性只能在結構體實現(xiàn)的代碼里使用 setter. 這使得 TrackedString 可以在內部修改 numberOfEdits 屬性, 但是也表示這個屬性對于外部代碼來說是只讀的—包括 TrackedString 的擴展。
如果你創(chuàng)建一個 TrackedString 實例然后修改它的字符串 value 值幾次, 你會看到 numberOfEdits 屬性值隨著變化次數(shù)一起更新:
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 屬性的當前值, 但是你不能進行修改恳啥。這個限制保護結構體編輯跟蹤功能的實現(xiàn)細節(jié)偏灿。
如果需要,你可以給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() {}
}
構造器
自定義構造器可以指定一個訪問級別,這個級別小于或者等于它構造的類型钝的。唯一的區(qū)別是必須的構造器翁垂。一個必須構造器訪問級別必須跟它所屬的類一致。
跟函數(shù)和方法參數(shù)一樣, 構造器的參數(shù)類型不能比構造器擁有的訪問級別更加私有硝桩。
默認構造器
就像默認構造器中描述的那樣, Swift 會自動為所有結構體或者基類提供一個沒有參數(shù)的默認構造器,這些結構體或者基類給所有屬性提供了默認值,但是沒有提供任何的構造器沿猜。
默認構造器的訪問級別和它要構造的類型是一樣的, 除非這個類型定義為 public. 對于定義為public的類型來說, 默認構造器訪問級別是 internal. 在其他模塊使用時,如果你想用無參數(shù)的構造器來構造 public 類型, 你必須顯式定義一個 public 無參數(shù)構造器。
結構體類型的默認成員構造器
如果結構體的存儲屬性是private的,結構體的默認成員構造器就是 private的碗脊。同樣的, 如果結構體任意一個存儲屬性是file private, 構造器也是 file private. 否則, 構造器的訪問級別是 internal.
和上面的默認構造器一樣, 在其他模塊使用時, 如果你想用一個成員構造器來構造一個 public 類型的話, 你必須提供一個public成員構造器啼肩。
協(xié)議
如果你想要給一個協(xié)議類型指定一個顯式的訪問級別, 就在協(xié)議定義的時候這么做。這個可以讓你創(chuàng)建協(xié)議, 這個協(xié)議只能在某些允許訪問的上下文中采用衙伶。
協(xié)議定義中每個需求的訪問級別和協(xié)議的訪問級別是一樣的祈坠。你不能把需求設置成協(xié)議不支持的訪問級別。這可以保證采用協(xié)議的類型可以看見所有的需求矢劲。
備注 如果你定義了一個 public 協(xié)議, 協(xié)議的需求實現(xiàn)時要求一個 public 訪問級別颁虐。這個行為不同于其他類型, public 類型定義意味著類型成員的訪問級別是 internal.
協(xié)議繼承
如果定義了一個新協(xié)議,它繼承自一個存在的協(xié)議, 新協(xié)議的訪問級別最多和繼承協(xié)議的級別一樣。例如, 已存在的協(xié)議訪問級別是internal, 你寫的新協(xié)議卻是是 public.
協(xié)議一致性
一個類型可以符合一個訪問級別比自己低的協(xié)議卧须。例如, 你可以定義一個 public 類型用在其他模塊里另绩。如果它符合一個 internal 協(xié)議,就只能用在 internal 協(xié)議的定義模塊內儒陨。
一個類型符合某個協(xié)議的上下文,訪問級別是這個類型和協(xié)議中最小的一個。如果一個類型是 public, 但是協(xié)議是 internal, 這個類型的一致性協(xié)議也是 internal.
一個類型符合一個協(xié)議或者擴展符合一個協(xié)議,你必須確保類型對協(xié)議需求的實現(xiàn),至少和類型的一致性協(xié)議有一樣的訪問級別笋籽。例如, 如果一個public 類型符合一個 internal, 這個類型實現(xiàn)的協(xié)議需求必須是 “internal”.
備注 在 Swift 里, 跟在 Objective-C里一樣, 協(xié)議一致性是全局的—不可能在同樣的程序里,類型以兩種不同的方式來符合一個協(xié)議蹦漠。
擴展
你可以在任何訪問權限的上下文中擴展一個類,結構體或者枚舉。擴展中添加的類型成員和被擴展類型中聲明的類型成員有著一樣的訪問級別车海。如果你擴展一個 public 或者 internal 類型, 你添加的任何類型成員默認訪問級別是 internal. 如果你擴展一個 file-private 類型, 你添加的所有類型成員的訪問級別都是file private. 如果你擴展一個 private 類型, 你添加的任何類型成員訪問級別都是 private.
另外, 你可以用顯式訪問級別修飾符來標記一個擴展,來為定義在擴展里的所有的成員設置一個新的默認訪問屬性笛园。單個類型成員的擴展里依然可以重寫這些新的默認級別。
使用擴展添加協(xié)議一致性
如果你用擴展來添加協(xié)議一致性,你就不能為擴展提供一個顯式的訪問級別修飾符侍芝。相反, 協(xié)議自己的訪問級別,通常用來為在擴展中實現(xiàn)的協(xié)議需求提供默認訪問級別研铆。
泛型
泛型類型和泛型函數(shù)的訪問級別, 是它們自身的訪問級別和它們的類型參數(shù)的任何類型限制的訪問級別之間最小的那個。
類型別名
你定義的所有類型別名,因為訪問控制的目的,會被看做是不同的類型州叠。一個類型別名的訪問級別小于或者等于這個類型棵红。例如, 一個private 類型的別名可以是一個 private, file-private, internal, public, 或者 open type的別名, 但是一個 public 類型別名不能是一個 internal, file-private, 或者 private 類型的別名。
備注 這個規(guī)則也適用于用來滿足協(xié)議一致性的關聯(lián)類型的類型別名咧栗。
138.高級運算符
除了基本運算符之外, Swift 提供了一些高級運算符來進行更復雜的值操作逆甜。包括位和位移運算符。
跟C的算術運算符不同, Swift 的算術運算符默認不會溢出致板。溢出會被捕獲和報錯交煞。 選擇溢出行為, 使用 Swift 的溢出算術運算符, 例如溢出加運算符 (&+). 所有溢出算術運算符都是以 (&)開始。
當你定義結構體,類和枚舉的時候, 為這些自定義類型實現(xiàn)自己的標準Swift運算符是很有用的斟或。Swift 讓提供這些實現(xiàn)變得容易,并且能精確決定每種類型的行為素征。
你不會被限定在預置運算符上。Swift 給你足夠的自由,用自定義的優(yōu)先級和指定值,來定義你自己的中綴,前綴,后綴和賦值運算符萝挤。這些運算符的用法和預置運算符一樣, 你甚至可以擴展已存在的類型來支持自定義的運算符御毅。
位運算符
位運算符可以操作數(shù)據(jù)結構里的單個數(shù)據(jù)位。它們通常用于低級別編程, 例如圖形編程和設備驅動編寫平斩。使用外部資源數(shù)據(jù)時,位運算符也很有用, 例如編解碼數(shù)據(jù)亚享。
Swift 支持C中所有的位運算符, 描述如下咽块。
位 NOT 運算符
位 NOT 運算符 (~) 把所有位轉換成一個數(shù):
位 NOT 運算符是一個前綴運算符, 直接出現(xiàn)在操作數(shù)的前面, 沒有空格:
let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits? // 等于 11110000
UInt8 整數(shù)有8位,可以存儲0到255之間的任何值绘面。這個例子用二進制值00001111來初始化一個 UInt8 整數(shù), 它的前四位全是0,后四位全是1. 它等于十進制的 15.
位 NOT 運算符用來創(chuàng)建一個新常量 invertedBits, 它等于 initialBits, 不過所有位都是反轉的。0變成1, 1變成0. invertedBits 的值是 11110000, 它等于十進制的 240.
位 AND 運算符
位 AND 運算符 (&) 合并兩個數(shù)的位侈沪。它返回一個新的數(shù)組,如果兩個輸入數(shù)的位都是1,這個新數(shù)的位才是1:
在上面的例子里, firstSixBits 和 lastSixBits 中間四位都是 1. 位 AND 運算符合并它們變成 00111100, 它等于十進制的 60:
let firstSixBits: UInt8 = 0b11111100
let lastSixBits: UInt8? = 0b00111111
let middleFourBits = firstSixBits & lastSixBits? // 等于 00111100
位 OR 運算符
位 OR 運算符 (|) 比較兩個數(shù)的位揭璃。如果兩個數(shù)任意一個數(shù)位為1,這個運算符返回的數(shù)位就是1:
在上面的例子里, someBits 和 moreBits 不同位設置為 1. 位 OR 運算符合并它們變成 11111110, 它等于一個無符號十進制254:
let someBits: UInt8 = 0b10110010
let moreBits: UInt8 = 0b01011110
let combinedbits = someBits | moreBits? // 等于 11111110
位 XOR 運算符
位 XOR 運算符, 或者 “異或運算符” (^), 比較兩個數(shù)的位。如果兩個數(shù)位不同返回1,如果相同則返回0:
let firstBits: UInt8 = 0b00010100
let otherBits: UInt8 = 0b00000101
let outputBits = firstBits ^ otherBits? // 等于 00010001
左右移位運算符
左移位運算符 (<<) 和右移位運算符 (>>) 往左或者往右移動數(shù)位, 規(guī)則如下亭罪。
左移位和右移位實際上是乘以或者除以2. 左移一個整數(shù)1位相當于乘以2, 而右移一個整數(shù)1位相當于除以2.
無符號整數(shù)移動
無符號整數(shù)位移表現(xiàn)如下:
左移或者右移請求數(shù)量的位瘦馍。
超出整數(shù)存儲范圍的移位被舍棄。
左移或者右移后,缺失的位用0填充应役。
這個方法叫邏輯移位情组。
下面這個圖展示了 11111111 << 1 和 11111111 >> 1 的結果燥筷。藍色數(shù)字是要移動的, 灰色數(shù)字是要舍棄的, 橙色的0是填充的:
下面是位移動在Swift 代碼里的表現(xiàn):
let shiftBits: UInt8 = 4? // 00000100 in binary
shiftBits << 1? ? ? ? ? ? // 00001000
shiftBits << 2? ? ? ? ? ? // 00010000
shiftBits << 5? ? ? ? ? ? // 10000000
shiftBits << 6? ? ? ? ? ? // 00000000
shiftBits >> 2? ? ? ? ? ? // 00000001
你可以使用位移動在其他數(shù)據(jù)類型里進行編解碼:
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. 經(jīng)過位移 AND (&)和右移運算符(>>)操作, 這個顏色會被分解為 red (CC), green (66), 和 blue (99) .
紅色部分通過把數(shù)字 0xCC6699 和 0xFF0000進行位AND獲取院崇。 0xFF0000 中的0 “掩藏”了 0xCC6699的第二個和第三個字節(jié), 導致 6699 被忽略,只留下 0xCC0000 的結果肆氓。
然后把這個數(shù)字向右移動16位 (>> 16). 十六進制數(shù)字每對字母使用8位, 所以向右移動16位把 0xCC0000 轉化為 0x0000CC. 它等于 0xCC, 它的十進制值是 204.
類似的, 綠色部分通過把數(shù)字 0xCC6699 和 0x00FF00進行位AND獲取, 它的輸出值是 0x006600. 然后把輸出值向右移動8位 0x66, 它的十進制值是 102.
最后, 藍色部分通過把數(shù)字 0xCC6699 和 0x0000FF進行位AND獲取, 它的輸出值是 0x000099. 它不需要向右移動, 因為 0x000099 已經(jīng)等于 0x99, 它的十進制值是 153.
有符號整數(shù)移動
有符號整數(shù)的移動比無符號的要復雜, 因為有符號整數(shù)是用二進制表示的(為了簡單,下面的例子用8位有符號整數(shù)展示, 不過這個原則適用于任何大小的有符號整數(shù)。)
有符號整數(shù)使用第一個數(shù)位來表示正負 (標志位)底瓣。 0表示正數(shù), 1表示負數(shù)谢揪。
剩余位用來存儲實際的值。正數(shù)的存儲和無符號整數(shù)的方式是一樣的, 從0往上數(shù)捐凭。這里是4在Int8中的數(shù)位的形式:
標志位是 0 (意思是正數(shù)), 7個數(shù)值位正好是數(shù)字 4, 用二進制符號表示拨扶。
負數(shù)存儲是不同的。它們存儲的值是絕對值減去2的n次方茁肠。這里n是數(shù)值位的數(shù)字患民。一個8位數(shù)有7個數(shù)值位, 所以2的7次方, 或者 128.
這里是-4在Int8中數(shù)位的形式 -4:
這次符號位是 1 (意思是負數(shù)), 七位數(shù)值位值是 124 (128 - 4):
負數(shù)編碼是一個二進制補碼表示。這似乎不是負數(shù)的常見表示方法, 但是它有幾個優(yōu)點官套。
首先, 你可以把-4加-1, 可以進行8位的簡單二進制加法 (包括標志位), 完成后舍棄不符合8位的:
其次, 二進制補碼表示讓你可以像正數(shù)那樣移動負數(shù)的數(shù)位酒奶。向左移動后依然會翻倍, 向右移動后會減半。為了實現(xiàn)這個, 當有符號整數(shù)向右移動時,使用額外的規(guī)則: 當你向右移動有符號整數(shù)時, 和無符號整數(shù)規(guī)則一樣, 但是左邊空出來的位要用標志位填充, 而不是0.
這個行為保證有符號整數(shù)向右移動后,有相同的標志位奶赔。 也就是算術移位惋嚎。
由于正負數(shù)存儲的特殊方式, 向右移動它們接近于0. 移動過程中保持標志位不變,意味著負數(shù)在接近0過程中依然是負數(shù)。
溢出運算符
如果你嘗試向一個整數(shù)常數(shù)或者變量插入無法保存的值, 默認情況下, Swift 會報錯而不是允許無效值的創(chuàng)建站刑。當你使用過大或者過小值的時候,這個規(guī)則可以提供額外的安全性另伍。
例如, Int16 整數(shù)范圍是 -32768 到 32767. 嘗試存儲超過這個范圍的數(shù)字會導致錯誤:
var potentialOverflow = Int16.max
// potentialOverflow equals 32767, which is the maximum value an Int16 can hold
potentialOverflow += 1
// 這個會報錯
當值變的過大或者過小的時候,提供錯誤處理,在給邊界值條件編碼時,會更加靈活。
不過, 當你特別想要一個溢出條件來截斷可用位數(shù)的時候, 你可以選擇這個行為而不是觸發(fā)一個錯誤绞旅。Swift 提供了三個算術溢出運算符,來為整數(shù)計算選擇溢出行為摆尝。這些運算符都以(&)開始:
溢出加 (&+)
溢出減 (&-)
溢出乘 (&*)
值溢出
負數(shù)和整數(shù)都可以溢出。
這里有一個例子,展示當一個無符號整數(shù)在正數(shù)方向溢出時,會發(fā)生什么, 使用的是溢出加運算符 (&+):
var unsignedOverflow = UInt8.max
// unsignedOverflow 等于 255, 它是UInt8可以保存的最大值
unsignedOverflow = unsignedOverflow &+ 1
// unsignedOverflow 現(xiàn)在等于 0
變量 unsignedOverflow 使用UInt8 的最大值初始化nt8 (255, 或者 11111111). 然后使用溢出加運算符加1. 這個讓它的二進制表示正好超過UInt8可以保存的最大值,這個導致了溢出, 如下表所示因悲。溢出加之后這個值00000000依然在UInt8的界限內堕汞。
相似的事情會發(fā)生在無符號數(shù)向負數(shù)方向的溢出上。下面是使用了溢出減運算符的例子:
var unsignedOverflow = UInt8.min
// unsignedOverflow 等于 0, 是UInt8可以保存的最小值
unsignedOverflow = unsignedOverflow &- 1
// unsignedOverflow 現(xiàn)在等于 255
UInt8可以保存的最小值是0, 或者二進制 00000000. 如果使用溢出減運算符減1, 這個數(shù)字會溢出變成 11111111, 或者十進制 255 .
溢出也會發(fā)生在有符號整數(shù)晃琳。有符號整數(shù)的加減法以位形式執(zhí)行, 標志位也參與加減讯检。
var signedOverflow = Int8.min
// signedOverflow 等于 -128, 是Int8可以保存的最小值
signedOverflow = signedOverflow &- 1
// signedOverflow 現(xiàn)在等于 127
Int8保存的最小值是 -128, 或者二進制 10000000. 使用溢出減減1,結果是 01111111, 它會切換標志位然后得正數(shù) 127, 它是Int8可以保存的最大正數(shù)值。