標簽(空格分隔): 未分類
基礎(相關概念)
1.元祖
元組(tuples)把多個值組合成一個復合值。元組內的值可以是任意類型,并不要求是相同類型
let http404Error = (404, "Not Found")
// http404Error 的類型是 (Int, String),值是 (404, "Not Found")
2.可選類型(optionals)
使用可選類型(optionals)來處理值可能缺失的情況。可選類型表示:
有值,等于 x;或者 沒有值
C 和 Objective-C 中并沒有可選類型這個概念。最接近的是 Objective-C 中的一個特性,一個方法要不返回一個對象要不返回nil,nil表示“缺少一個合法的對象”。然而,這只對對象起作用——對于結構體,基本的 C 類型或者枚舉類型不起作用。對于這些類型,Objective-C 方法一般會返回一個特殊值(比如NSNotFound)來暗示值缺失。這種方法假設方法的調用者知道并記得對特殊值進行判斷。然而,Swift 的可選類型可以讓你暗示任意類型的值缺失,并不需要一個特殊值。
//1.變量聲明
var surveyAnswer: String?
// surveyAnswer 被自動設置為 nil
//2.自動類型推斷
let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber 被推測為類型 "Int?", 或者類型 "optional Int"
//3.可選類型賦值(兩種狀態)
var serverResponseCode: Int? = 404
// serverResponseCode 包含一個可選的 Int 值 404
serverResponseCode = nil
// serverResponseCode 現在不包含值
if 語句以及強制解析
你可以使用 if 語句和 nil比較來判斷一個可選值是否包含值。你可以使用“相等”(==)或“不等”(!=)來執行比較。如果可選類型有值,它將不等于 nil:
if convertedNumber != nil {
print("convertedNumber contains some integer value.")
}
// 輸出 "convertedNumber contains some integer value."
當你確定可選類型確實包含值之后,你可以在可選的名字后面加一個感嘆號(!)來獲取值。這個驚嘆號表示“我知道這個可選有值,請使用它。”這被稱為可選值的強制解析(forced unwrapping):
if convertedNumber != nil {
print("convertedNumber has an integer value of \(convertedNumber!).")
}
// 輸出 "convertedNumber has an integer value of 123."
可選綁定
使用可選綁定(optional binding)來判斷可選類型是否包含值,如果包含就把值賦給一個臨時常量或者變量。可選綁定可以用在 if 和 while 語句中,這條語句不僅可以用來判斷可選類型中是否有值,同時可以將可選類型中的值賦給一個常量或者變量。
if let constantName = someOptional {
statements
}
3.錯誤處理
swift是類型安全的語言,對于錯誤中斷必須處理,而OC在這方面沒有如此嚴格的要求
//OC不帶error處理
[NSFileManager.defaultManager copyItemAtURL:[NSURL fileURLWithPath:@""] toURL:[NSURL fileURLWithPath:@""] error:nil];
//OC帶error處理(非強制)
NSError *error;
[NSFileManager.defaultManager copyItemAtURL:[NSURL fileURLWithPath:@""] toURL:[NSURL fileURLWithPath:@""] error:&error];
//swift
do{
try FileManager.default.copyItem(at: URL.init(fileURLWithPath: ""), to:URL.init(fileUrlWithPath:"")
}catch{
//error handle
}
集合類型
1.數組(Array)
創建數組
//1.創建空數組
var someInts = [Int]()
//2.創建帶有默認值或指定長度的數組
var threeDoubles = Array(repeating: 0.0, count: 3)
//3.創建一個含有數據的數組()
var shoppingList: [String] = ["Eggs", "Milk"]
var shoppingList = ["Eggs", "Milk"]
注:兩者等價,自動類型推斷,類型為[String]
訪問和修改數組
var shoppingList = [String][]
//1.拼接元素
shoppingList.append("egg")
//2.拼接數組
shoppingList = shoppingList + ["tomato","milk"]
//3.修改元素
shoppingList[0] = "orange juice"
shoppingList[0...2] = ["fish","noodle","pork","onion"]
注:可將其中的某幾項替換成某幾項(數量可不對等)
//4.插入元素
shoppingList.insert("Maple Syrup", at: 0)
//5.移除元素
shoppingList.remove(at:2)
shoppingList.removeLast()
shoppingList.removeFirst()
//6.清除所有元素
shoppingList = []
數組遍歷
for item in shoppingList {
print(item)
}
//帶索引的遍歷
for (index, value) in shoppingList. enumerated() {
print("Item \(String(index + 1)): \(value)")
}
2.集合(Set)
集合創建
var letters = Set<String>()
var favoriteGenres: Set<String> = ["Rock", "Classical", "Hip hop"]
var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"] //自動推斷集合元素數據類型
集合賦值、遍歷
favoriteGenres.insert("Jazz")
favoriteGenres.remove("Jazz")
for genre in favoriteGenres {
print("\(genre)")
}
集合操作
- 使用intersection(_:)方法根據兩個集合中都包含的值創建的一個新的集合。
- 使用symmetricDifference(_:)方法根據在一個集合中但不在兩個集合中的值創建一個新的集合。
- 使用union(_:)方法根據兩個集合的值創建一個新的集合。
- 使用subtracting(_:)方法根據不在該集合中的值創建一個新的集合。
[圖片上傳失敗...(image-899197-1524110403362)]
let oddDigits: Set = [1, 3, 5, 7, 9]
let evenDigits: Set = [0, 2, 4, 6, 8]
let singleDigitPrimeNumbers: Set = [2, 3, 5, 7]
oddDigits.union(evenDigits).sorted()
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
oddDigits. intersection(evenDigits).sorted()
// []
oddDigits.subtracting(singleDigitPrimeNumbers).sorted()
// [1, 9]
oddDigits. symmetricDifference(singleDigitPrimeNumbers).sorted()
// [1, 2, 9]
3.字典(Dictionary)
創建字典
var namesOfIntegers = [Int: String]()
var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
//類型自動推導為[String: String]
訪問和修改字典
airports["LHR"] = "London" //賦值
airports["APL"] = nil //移除鍵值對
airports.removeValue(forKey: "DUB")//移除鍵值對
airports.count //字典鍵值對數量
airports.isEmpty //字典是否為空
注:我們也可以使用下標語法來在字典中檢索特定鍵對應的值。因為有可能請求的鍵沒有對應的值存在,字典的下標訪問會返回對應值的類型的可選值。如果這個字典包含請求鍵所對應的值,下標會返回一個包含這個存在值的可選值,否則將返回nil:
//做可選判斷是非常必要的
if let airportName = airports["DUB"] {
print("The name of the airport is \(airportName).")
} else {
print("That airport is not in the airports dictionary.")
}
字典遍歷
for (airportCode, airportName) in airports {
print("\(airportCode): \(airportName)")
}
函數
1.函數的定義與調用
函數由 func + 函數名 + 函數參數(0-n個) + 返回參數(可為Void,返回參數可為元祖) 構成
func greet(person: String) -> String {
let greeting = "Hello, " + person + "!"
return greeting
}
指定參數標簽:你可以在參數名稱前指定它的參數標簽,中間以空格分隔:
func someFunction(argumentLabel parameterName: Int) {
// 在函數體內,parameterName 代表參數值,argumentLabele為參數標簽
}
忽略參數標簽:如果你不希望為某個參數添加一個標簽,可以使用一個下劃線(_)來代替一個明確的參數標簽
func someFunction(_ firstParameterName: Int, secondParameterName: Int) {
// 在函數體內,firstParameterName 和 secondParameterName 代表參數中的第一個和第二個參數值
}
someFunction(1, secondParameterName: 2)
默認參數值:你可以在函數體中通過給參數賦值來為任意一個參數定義默認值(Deafult Value)。當默認值被定義后,調用這個函數時可以忽略這個參數
func someFunction(parameterWithoutDefault: Int, parameterWithDefault: Int = 12) {
// 如果你在調用時候不傳第二個參數,parameterWithDefault 會值為 12 傳入到函數體中。
}
someFunction(parameterWithoutDefault: 3, parameterWithDefault: 6) // parameterWithDefault = 6
someFunction(parameterWithoutDefault: 4) // parameterWithDefault = 12
可變參數:一個可變參數(variadic parameter)可以接受零個或多個值。函數調用時,你可以用可變參數來指定函數參數可以被傳入不確定數量的輸入值。通過在變量類型名后面加入(...)的方式來定義可變參數
func arithmeticMean(_ numbers: Double...) -> Double {
var total: Double = 0
for number in numbers {
total += number
}
return total / Double(numbers.count)
}
arithmeticMean(1, 2, 3, 4, 5)
// 返回 3.0, 是這 5 個數的平均數。
arithmeticMean(3, 8.25, 18.75)
// 返回 10.0, 是這 3 個數的平均數。
函數的使用
你可以定義一個類型為函數的常量或變量
var mathFunction: (Int, Int) -> Int = addTwoInts
函數類型作為參數類型
func printMathResult(_ mathFunction: (Int, Int) -> Int, _ a: Int, _ b: Int) {
print("Result: \(mathFunction(a, b))")
}
函數類型作為返回類型
func stepForward(_ input: Int) -> Int {
return input + 1
}
func stepBackward(_ input: Int) -> Int {
return input - 1
}
func chooseStepFunction(backward: Bool) -> (Int) -> Int {
return backward ? stepBackward : stepForward
}
閉包
閉包是自包含的函數代碼塊,可以在代碼中被傳遞和使用。Swift 中的閉包與 C 和 Objective-C 中的代碼塊(blocks)以及其他一些編程語言中的匿名函數比較相似。
閉包可以捕獲和存儲其所在上下文中任意常量和變量的引用。被稱為包裹常量和變量。 Swift 會為你管理在捕獲過程中涉及到的所有內存操作。
尾隨閉包
如果你需要將一個很長的閉包表達式作為最后一個參數傳遞給函數,可以使用尾隨閉包來增強函數的可讀性。尾隨閉包是一個書寫在函數括號之后的閉包表達式,函數支持將其作為最后一個參數調用。在使用尾隨閉包時,你不用寫出它的參數標簽:
func someFunctionThatTakesAClosure(closure: () -> Void) {
// 函數體部分
}
// 以下是不使用尾隨閉包進行函數調用
someFunctionThatTakesAClosure(closure: {
// 閉包主體部分
})
// 以下是使用尾隨閉包進行函數調用
someFunctionThatTakesAClosure() {
// 閉包主體部分
}
值捕獲
閉包可以在其被定義的上下文中捕獲常量或變量。即使定義這些常量和變量的原作用域已經不存在,閉包仍然可以在閉包函數體內引用和修改這些值。
Swift 中,可以捕獲值的閉包的最簡單形式是嵌套函數,也就是定義在其他函數的函數體內的函數。嵌套函數可以捕獲其外部函數所有的參數以及定義的常量和變量。
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
注:incrementer() 函數并沒有任何參數,但是在函數體內訪問了 runningTotal 和 amount 變量。這是因為它從外圍函數捕獲了 runningTotal 和 amount 變量的引用。捕獲引用保證了 runningTotal 和 amount 變量在調用完 makeIncrementer 后不會消失,并且保證了在下一次執行 incrementer 函數時,runningTotal 依舊存在。
逃逸閉包
當一個閉包作為參數傳到一個函數中,但是這個閉包在函數返回之后才被執行,我們稱該閉包從函數中逃逸。當你定義接受閉包作為參數的函數時,你可以在參數名之前標注 @escaping,用來指明這個閉包是允許“逃逸”出這個函數的。
一種能使閉包“逃逸”出函數的方法是,將這個閉包保存在一個函數外部定義的變量中。舉個例子,很多啟動異步操作的函數接受一個閉包參數作為 completion handler。這類函數會在異步操作開始之后立刻返回,但是閉包直到異步操作結束后才會被調用。在這種情況下,閉包需要“逃逸”出函數,因為閉包需要在函數返回之后被調用。例如:
//逃逸閉包
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
//非逃逸閉包
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
將一個閉包標記為 @escaping 意味著你必須在閉包中顯式地引用 self。比如說,在下面的代碼中,傳遞到 someFunctionWithEscapingClosure(:) 中的閉包是一個逃逸閉包,這意味著它需要顯式地引用 self。相對的,傳遞到 someFunctionWithNonescapingClosure(:) 中的閉包是一個非逃逸閉包,這意味著它可以隱式引用 self。
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// 打印出 "200"
completionHandlers.first?()
print(instance.x)
// 打印出 "100"
自動閉包
自動閉包是一種自動創建的閉包,用于包裝傳遞給函數作為參數的表達式。這種閉包不接受任何參數,當它被調用的時候,會返回被包裝在其中的表達式的值。這種便利語法讓你能夠省略閉包的花括號,用一個普通的表達式來代替顯式的閉包。
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// 打印出 "5"
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// 打印出 "4"
閉包作為函數參數:
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
//顯式閉包傳參
serve(customer: { customersInLine.remove(at: 0) } )
// 打印出 "Now serving Alex!"
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
//自動閉包傳參
serve(customer: customersInLine.remove(at: 0))
// 打印 "Now serving Ewa!"
枚舉
枚舉為一組相關的值定義了一個共同的類型,使你可以在你的代碼中以類型安全的方式來使用這些值。
enum CompassPoint {
case north
case south
case east
case west
}
enum CompassPoint {
case north,south,east,west
}
使用 Switch 語句匹配枚舉值
你可以使用switch語句匹配單個枚舉值:
directionToHead = CompassPoint.south
switch directionToHead {
case .north:
print("Lots of planets have a north")
case .south:
print("Watch out for penguins")
case .east:
print("Where the sun rises")
case .west:
print("Where the skies are blue")
}
// 打印 "Watch out for penguins”
遞歸枚舉
遞歸枚舉是一種枚舉類型,它有一個或多個枚舉成員使用該枚舉類型的實例作為關聯值。使用遞歸枚舉時,編譯器會插入一個間接層。你可以在枚舉成員前加上indirect來表示該成員可遞歸。
enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}
//也可以在枚舉類型開頭加上indirect關鍵字來表明它的所有成員都是可遞歸的:
indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}
例子:
let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))
屬性
屬性將值跟特定的類、結構或枚舉關聯。存儲屬性存儲常量或變量作為實例的一部分,而計算屬性計算(不是存儲)一個值。計算屬性可以用于類、結構體和枚舉,存儲屬性只能用于類和結構體。
存儲屬性
簡單來說,一個存儲屬性就是存儲在特定類或結構體實例里的一個常量或變量。存儲屬性可以是變量存儲屬性(用關鍵字 var 定義),也可以是常量存儲屬性(用關鍵字 let 定義)。
struct FixedLengthRange {
var firstValue: Int
let length: Int
}
延遲存儲屬性(OC的懶加載)
延遲存儲屬性是指當第一次被調用的時候才會計算其初始值的屬性。在屬性聲明前使用 lazy 來標示一個延遲存儲屬性。
注意:必須將延遲存儲屬性聲明成變量(使用 var 關鍵字),因為屬性的初始值可能在實例構造完成之后才會得到。而常量屬性在構造過程完成之前必須要有初始值,因此無法聲明成延遲屬性。
延遲屬性很有用,當屬性的值依賴于在實例的構造過程結束后才會知道影響值的外部因素時,或者當獲得屬性的初始值需要復雜或大量計算時,可以只在需要的時候計算它。
class DataImporter {
/*
DataImporter 是一個負責將外部文件中的數據導入的類。
這個類的初始化會消耗不少時間。
*/
var fileName = "data.txt"
// 這里會提供數據導入功能
}
class DataManager {
lazy var importer = DataImporter()
var data = [String]()
// 這里會提供數據管理功能
}
計算屬性
除存儲屬性外,類、結構體和枚舉可以定義計算屬性。計算屬性不直接存儲值,而是提供一個 getter 和一個可選的 setter,來間接獲取和設置其他屬性或變量的值。
struct Rect {
var origin = Point()
var size = Size()
var center: Point {
get {
let centerX = origin.x + (size.width / 2)
let centerY = origin.y + (size.height / 2)
return Point(x: centerX, y: centerY)
}
set(newCenter) {
origin.x = newCenter.x - (size.width / 2)
origin.y = newCenter.y - (size.height / 2)
}
}
}
只讀計算屬性
只有 getter 沒有 setter 的計算屬性就是只讀計算屬性。只讀計算屬性總是返回一個值,可以通過點運算符訪問,但不能設置新的值。
struct Cuboid {
var width = 0.0, height = 0.0, depth = 0.0
var volume: Double {
return width * height * depth
}
}
屬性觀察器
屬性觀察器監控和響應屬性值的變化,每次屬性被設置值的時候都會調用屬性觀察器,即使新值和當前值相同的時候也不例外。
可以為除了延遲存儲屬性之外的其他存儲屬性添加屬性觀察器,也可以通過重寫屬性的方式為繼承的屬性(包括存儲屬性和計算屬性)添加屬性觀察器。你不必為非重寫的計算屬性添加屬性觀察器,因為可以通過它的 setter 直接監控和響應值的變化。
- willSet 在新的值被設置之前調用
- didSet 在新的值被設置之后立即調用
class StepCounter {
var totalSteps: Int = 0 {
willSet(newTotalSteps) {
print("About to set totalSteps to \(newTotalSteps)")
}
didSet {
if totalSteps > oldValue {
print("Added \(totalSteps - oldValue) steps")
}
}
}
}
類型屬性
實例屬性屬于一個特定類型的實例,每創建一個實例,實例都擁有屬于自己的一套屬性值,實例之間的屬性相互獨立。
也可以為類型本身定義屬性,無論創建了多少個該類型的實例,這些屬性都只有唯一一份。這種屬性就是類型屬性。
類型屬性用于定義某個類型所有實例共享的數據,比如所有實例都能用的一個常量(就像 C 語言中的靜態常量),或者所有實例都能訪問的一個變量(就像 C 語言中的靜態變量)。
存儲型類型屬性可以是變量或常量,計算型類型屬性跟實例的計算型屬性一樣只能定義成變量屬性。
方法
實例方法 (Instance Methods)
實例方法是屬于某個特定類、結構體或者枚舉類型實例的方法。實例方法提供訪問和修改實例屬性的方法或提供與實例目的相關的功能,并以此來支撐實例的功能。
class Counter {
var count = 0
func increment() {
count += 1
}
類型方法
實例方法是被某個類型的實例調用的方法。你也可以定義在類型本身上調用的方法,這種方法就叫做類型方法。在方法的func關鍵字之前加上關鍵字static,來指定類型方法。類還可以用關鍵字class來允許子類重寫父類的方法實現。
class SomeClass {
class func someTypeMethod() {
// 在這里實現類型方法
}
}
SomeClass.someTypeMethod()
繼承
一個類可以繼承另一個類的方法,屬性和其它特性。當一個類繼承其它類時,繼承類叫子類,被繼承類叫超類(或父類)。在 Swift 中,繼承是區分「類」與其它類型的一個基本特征。
在 Swift 中,類可以調用和訪問超類的方法、屬性和下標,并且可以重寫這些方法,屬性和下標來優化或修改它們的行為。Swift 會檢查你的重寫定義在超類中是否有匹配的定義,以此確保你的重寫行為是正確的。
可以為類中繼承來的屬性添加屬性觀察器,這樣一來,當屬性值改變時,類就會被通知到。可以為任何屬性添加屬性觀察器,無論它原本被定義為存儲型屬性還是計算型屬性。
子類
class SomeClass: SomeSuperclass {
// 這里是子類的定義
}
class Bicycle: Vehicle {
var hasBasket = false
}
重寫
子類可以為繼承來的實例方法,類方法,實例屬性,或下標提供自己定制的實現。我們把這種行為叫重寫。
如果要重寫某個特性,你需要在重寫定義的前面加上override關鍵字。這么做,你就表明了你是想提供一個重寫版本,而非錯誤地提供了一個相同的定義。意外的重寫行為可能會導致不可預知的錯誤,任何缺少override關鍵字的重寫都會在編譯時被診斷為錯誤。
override關鍵字會提醒 Swift 編譯器去檢查該類的超類(或其中一個父類)是否有匹配重寫版本的聲明。這個檢查可以確保你的重寫定義是正確的。
訪問超類的方法,屬性及下標
當你在子類中重寫超類的方法,屬性或下標時,有時在你的重寫版本中使用已經存在的超類實現會大有裨益。比如,你可以完善已有實現的行為,或在一個繼承來的變量中存儲一個修改過的值。
在合適的地方,你可以通過使用super前綴來訪問超類版本的方法,屬性或下標:
- 在方法someMethod()的重寫實現中,可以通過super.someMethod()來調用超類版本的someMethod()方法。
- 在屬性someProperty的 getter 或 setter 的重寫實現中,可以通過super.someProperty來訪問超類版本的someProperty屬性。
- 在下標的重寫實現中,可以通過super[someIndex]來訪問超類版本中的相同下標。
重寫方法
在子類中,你可以重寫繼承來的實例方法或類方法,提供一個定制或替代的方法實現。
class Train: Vehicle {
override func makeNoise() {
print("Choo Choo")
}
}
重寫屬性
你可以重寫繼承來的實例屬性或類型屬性,提供自己定制的 getter 和 setter,或添加屬性觀察器使重寫的屬性可以觀察屬性值什么時候發生改變。
重寫屬性的 Getters 和 Setters
你可以提供定制的 getter(或 setter)來重寫任意繼承來的屬性,無論繼承來的屬性是存儲型的還是計算型的屬性。子類并不知道繼承來的屬性是存儲型的還是計算型的,它只知道繼承來的屬性會有一個名字和類型。你在重寫一個屬性時,必需將它的名字和類型都寫出來。這樣才能使編譯器去檢查你重寫的屬性是與超類中同名同類型的屬性相匹配的。
你可以將一個繼承來的只讀屬性重寫為一個讀寫屬性,只需要在重寫版本的屬性里提供 getter 和 setter 即可。但是,你不可以將一個繼承來的讀寫屬性重寫為一個只讀屬性。
注意 如果你在重寫屬性中提供了 setter,那么你也一定要提供 getter。如果你不想在重寫版本中的 getter 里修改繼承來的屬性值,你可以直接通過super.someProperty來返回繼承來的值,其中someProperty是你要重寫的屬性的名字。
class Car: Vehicle {
var gear = 1
override var description: String {
return super.description + " in gear \(gear)"
}
}
重寫屬性觀察器
你可以通過重寫屬性為一個繼承來的屬性添加屬性觀察器。這樣一來,當繼承來的屬性值發生改變時,你就會被通知到,無論那個屬性原本是如何實現的。關于屬性觀察器的更多內容,請看屬性觀察器。
注意 你不可以為繼承來的常量存儲型屬性或繼承來的只讀計算型屬性添加屬性觀察器。這些屬性的值是不可以被設置的,所以,為它們提供willSet或didSet實現是不恰當。 此外還要注意,你不可以同時提供重寫的 setter 和重寫的屬性觀察器。如果你想觀察屬性值的變化,并且你已經為那個屬性提供了定制的 setter,那么你在 setter 中就可以觀察到任何值變化了。
class AutomaticCar: Car {
override var currentSpeed: Double {
didSet {
gear = Int(currentSpeed / 10.0) + 1
}
}
}
防止重寫
你可以通過把方法,屬性或下標標記為final來防止它們被重寫,只需要在聲明關鍵字前加上final修飾符即可(例如:final var,final func,final class func,以及final subscript)。
如果你重寫了帶有final標記的方法、屬性或下標,在編譯時會報錯。在類擴展中的方法,屬性或下標也可以在擴展的定義里標記為 final 的。
你可以通過在關鍵字class前添加final修飾符(final class)來將整個類標記為 final 的。這樣的類是不可被繼承的,試圖繼承這樣的類會導致編譯報錯。
構造過程
你可以在構造器中為存儲型屬性賦初值,也可以在定義屬性時為其設置默認值。
構造過程是使用類、結構體或枚舉類型的實例之前的準備過程。在新實例可用前必須執行這個過程,具體操作包括設置實例中每個存儲型屬性的初始值和執行其他必須的設置或初始化工作。
通過定義構造器來實現構造過程,就像用來創建特定類型新實例的特殊方法。與 Objective-C 中的構造器不同,Swift 的構造器無需返回值,它們的主要任務是保證新實例在第一次使用前完成正確的初始化。
注:類和結構體在創建實例時,必須為所有存儲型屬性設置合適的初始值。存儲型屬性的值不能處于一個未知的狀態。
你可以在構造器中為存儲型屬性賦初值,也可以在定義屬性時為其設置默認值。
構造參數
自定義構造過程時,可以在定義中提供構造參數,指定參數值的類型和名字。構造參數的功能和語法跟函數和方法的參數相同。
struct Celsius {
var temperatureInCelsius: Double
init(fromFahrenheit fahrenheit: Double) {
temperatureInCelsius = (fahrenheit - 32.0) / 1.8
}
init(fromKelvin kelvin: Double) {
temperatureInCelsius = kelvin - 273.15
}
//不帶外部名的構造器參數
init(_ celsius: Double){
temperatureInCelsius = celsius
}
}
可選屬性類型
如果你定制的類型包含一個邏輯上允許取值為空的存儲型屬性——無論是因為它無法在初始化時賦值,還是因為它在之后某個時間點可以賦值為空——你都需要將它定義為可選類型。可選類型的屬性將自動初始化為 nil,表示這個屬性是有意在初始化時設置為空的。
class SurveyQuestion {
var text: String
var response: String?
init(text: String) {
self.text = text
}
}
默認構造器
如果結構體或類的所有屬性都有默認值,同時沒有自定義的構造器,那么 Swift 會給這些結構體或類提供一個默認構造器(default initializers)。這個默認構造器將簡單地創建一個所有屬性值都設置為默認值的實例。
class ShoppingListItem {
var name: String?
var quantity = 1
var purchased = false
}
var item = ShoppingListItem()
結構體的逐一成員構造器:除了上面提到的默認構造器,如果結構體沒有提供自定義的構造器,它們將自動獲得一個逐一成員構造器,即使結構體的存儲型屬性沒有默認值。
逐一成員構造器是用來初始化結構體新實例里成員屬性的快捷方法。我們在調用逐一成員構造器時,通過與成員屬性名相同的參數名進行傳值來完成對成員屬性的初始賦值。
struct Size {
var width = 0.0, height = 0.0
}
let twoByTwo = Size(width: 2.0, height: 2.0)
指定構造器和便利構造器
類里面的所有存儲型屬性——包括所有繼承自父類的屬性——都必須在構造過程中設置初始值Swift 為類類型提供了兩種構造器來確保實例中所有存儲型屬性都能獲得初始值,它們分別是指定構造器和便利構造器。
指定構造器是類中最主要的構造器。一個指定構造器將初始化類中提供的所有屬性,并根據父類鏈往上調用父類合適的構造器來實現父類的初始化。
類傾向于擁有少量指定構造器,普遍的是一個類擁有一個指定構造器。指定構造器在初始化的地方通過“管道”將初始化過程持續到父類鏈。
每一個類都必須至少擁有一個指定構造器。在某些情況下,許多類通過繼承了父類中的指定構造器而滿足了這個條件。具體內容請參考后續章節構造器的自動繼承。
便利構造器是類中比較次要的、輔助型的構造器。你可以定義便利構造器來調用同一個類中的指定構造器,并為其參數提供默認值。你也可以定義便利構造器來創建一個特殊用途或特定輸入值的實例。
你應當只在必要的時候為類提供便利構造器,比方說某種情況下通過使用便利構造器來快捷調用某個指定構造器,能夠節省更多開發時間并讓類的構造過程更清晰明了。
類的構造器代理規則
為了簡化指定構造器和便利構造器之間的調用關系,Swift 采用以下三條規則來限制構造器之間的代理調用:
- 指定構造器必須調用其直接父類的的指定構造器。
- 便利構造器必須調用同類中定義的其它構造器。
- 便利構造器最后必須調用指定構造器。
一個更方便記憶的方法是:
- 指定構造器必須總是向上代理
- 便利構造器必須總是橫向代理
[圖片上傳失敗...(image-6e68cd-1524110403362)]
如圖所示,父類中包含一個指定構造器和兩個便利構造器。其中一個便利構造器調用了另外一個便利構造器,而后者又調用了唯一的指定構造器。這滿足了上面提到的規則 2 和 3。這個父類沒有自己的父類,所以規則 1 沒有用到。
子類中包含兩個指定構造器和一個便利構造器。便利構造器必須調用兩個指定構造器中的任意一個,因為它只能調用同一個類里的其他構造器。這滿足了上面提到的規則 2 和 3。而兩個指定構造器必須調用父類中唯一的指定構造器,這滿足了規則 1。
[圖片上傳失敗...(image-8256f2-1524110403362)]
兩段式構造過程
Swift 中類的構造過程包含兩個階段。第一個階段,類中的每個存儲型屬性賦一個初始值。當每個存儲型屬性的初始值被賦值后,第二階段開始,它給每個類一次機會,在新實例準備使用之前進一步定制它們的存儲型屬性。
兩段式構造過程的使用讓構造過程更安全,同時在整個類層級結構中給予了每個類完全的靈活性。兩段式構造過程可以防止屬性值在初始化之前被訪問,也可以防止屬性被另外一個構造器意外地賦予不同的值。
Swift 編譯器將執行 4 種有效的安全檢查,以確保兩段式構造過程不出錯地完成:
- 安全檢查 1
指定構造器必須保證它所在類的所有屬性都必須先初始化完成,之后才能將其它構造任務向上代理給父類中的構造器。
如上所述,一個對象的內存只有在其所有存儲型屬性確定之后才能完全初始化。為了滿足這一規則,指定構造器必須保證它所在類的屬性在它往上代理之前先完成初始化。
- 安全檢查 2
指定構造器必須在為繼承的屬性設置新值之前向上代理調用父類構造器,如果沒這么做,指定構造器賦予的新值將被父類中的構造器所覆蓋。
- 安全檢查 3
便利構造器必須為任意屬性(包括同類中定義的)賦新值之前代理調用同一類中的其它構造器,如果沒這么做,便利構造器賦予的新值將被同一類中其它指定構造器所覆蓋。
- 安全檢查 4
構造器在第一階段構造完成之前,不能調用任何實例方法,不能讀取任何實例屬性的值,不能引用self作為一個值。
類實例在第一階段結束以前并不是完全有效的。只有第一階段完成后,該實例才會成為有效實例,才能訪問屬性和調用方法。
構造器的繼承和重寫
跟 Objective-C 中的子類不同,Swift 中的子類默認情況下不會繼承父類的構造器。Swift 的這種機制可以防止一個父類的簡單構造器被一個更精細的子類繼承,并被錯誤地用來創建子類的實例。
假如你希望自定義的子類中能提供一個或多個跟父類相同的構造器,你可以在子類中提供這些構造器的自定義實現。
當你在編寫一個和父類中指定構造器相匹配的子類構造器時,你實際上是在重寫父類的這個指定構造器。因此,你必須在定義子類構造器時帶上 override 修飾符。即使你重寫的是系統自動提供的默認構造器,也需要帶上 override 修飾符。
正如重寫屬性,方法或者是下標,override 修飾符會讓編譯器去檢查父類中是否有相匹配的指定構造器,并驗證構造器參數是否正確。
構造器的自動繼承
如上所述,子類在默認情況下不會繼承父類的構造器。但是如果滿足特定條件,父類構造器是可以被自動繼承的。事實上,這意味著對于許多常見場景你不必重寫父類的構造器,并且可以在安全的情況下以最小的代價繼承父類的構造器。
假設你為子類中引入的所有新屬性都提供了默認值,以下 2 個規則適用:
- 規則 1
如果子類沒有定義任何指定構造器,它將自動繼承父類所有的指定構造器。
- 規則 2
如果子類提供了所有父類指定構造器的實現——無論是通過規則 1 繼承過來的,還是提供了自定義實現(例如:類本身的便利構造器和父類的指定構造器重名)——它將自動繼承父類所有的便利構造器。
即使你在子類中添加了更多的便利構造器,這兩條規則仍然適用。
class Food {
var name: String
init(name: String) {
self.name = name
}
convenience init() {
self.init(name: "[Unnamed]")
}
}
class RecipeIngredient: Food {
var quantity: Int
init(name: String, quantity: Int) {
self.quantity = quantity
super.init(name: name)
}
override convenience init(name: String) {
self.init(name: name, quantity: 1)
}
}
class ShoppingListItem: RecipeIngredient {
var purchased = false
var description: String {
var output = "\(quantity) x \(name)"
output += purchased ? " ?" : " ?"
return output
}
}
[圖片上傳失敗...(image-f6c4fd-1524110403362)]
可失敗構造器
如果一個類、結構體或枚舉類型的對象,在構造過程中有可能失敗,則為其定義一個可失敗構造器是很有用的。這里所指的“失敗” 指的是,如給構造器傳入無效的參數值,或缺少某種所需的外部資源,又或是不滿足某種必要的條件等。
為了妥善處理這種構造過程中可能會失敗的情況。你可以在一個類,結構體或是枚舉類型的定義中,添加一個或多個可失敗構造器。其語法為在 init 關鍵字后面添加問號 (init?)。
可失敗構造器會創建一個類型為自身類型的可選類型的對象。你通過 return nil 語句來表明可失敗構造器在何種情況下應該 “失敗”。
struct Animal {
let species: String
init?(species: String) {
if species.isEmpty {
return nil
}
self.species = species
}
}
let someCreature = Animal(species: "Giraffe")
// someCreature 的類型是 Animal? 而不是 Animal
枚舉類型的可失敗構造器
你可以通過一個帶一個或多個參數的可失敗構造器來獲取枚舉類型中特定的枚舉成員。如果提供的參數無法匹配任何枚舉成員,則構造失敗。
enum TemperatureUnit {
case Kelvin, Celsius, Fahrenheit
init?(symbol: Character) {
switch symbol {
case "K":
self = .Kelvin
case "C":
self = .Celsius
case "F":
self = .Fahrenheit
default:
return nil
}
}
}
帶原始值的枚舉類型的可失敗構造器
帶原始值的枚舉類型會自帶一個可失敗構造器 init?(rawValue:),該可失敗構造器有一個名為 rawValue 的參數,其類型和枚舉類型的原始值類型一致,如果該參數的值能夠和某個枚舉成員的原始值匹配,則該構造器會構造相應的枚舉成員,否則構造失敗。
enum TemperatureUnit: Character {
case Kelvin = "K", Celsius = "C", Fahrenheit = "F"
}
let fahrenheitUnit = TemperatureUnit(rawValue: "F")
if fahrenheitUnit != nil {
print("This is a defined temperature unit, so initialization succeeded.")
}
構造失敗的傳遞
類,結構體,枚舉的可失敗構造器可以橫向代理到同類型中的其他可失敗構造器。類似的,子類的可失敗構造器也能向上代理到父類的可失敗構造器。
無論是向上代理還是橫向代理,如果你代理到的其他可失敗構造器觸發構造失敗,整個構造過程將立即終止,接下來的任何構造代碼不會再被執行。
class Product {
let name: String
init?(name: String) {
if name.isEmpty { return nil }
self.name = name
}
}
class CartItem: Product {
let quantity: Int
init?(name: String, quantity: Int) {
if quantity < 1 { return nil }
self.quantity = quantity
super.init(name: name)
}
}
必要構造器
在類的構造器前添加 required 修飾符表明所有該類的子類都必須實現該構造器:
class SomeClass {
required init() {
// 構造器的實現代碼
}
}
在子類重寫父類的必要構造器時,必須在子類的構造器前也添加 required 修飾符,表明該構造器要求也應用于繼承鏈后面的子類。在重寫父類中必要的指定構造器時,不需要添加 override 修飾符:
class SomeSubclass: SomeClass {
required init() {
// 構造器的實現代碼
}
}
通過閉包或函數設置屬性的默認值
如果某個存儲型屬性的默認值需要一些定制或設置,你可以使用閉包或全局函數為其提供定制的默認值。每當某個屬性所在類型的新實例被創建時,對應的閉包或函數會被調用,而它們的返回值會當做默認值賦值給這個屬性。
這種類型的閉包或函數通常會創建一個跟屬性類型相同的臨時變量,然后修改它的值以滿足預期的初始狀態,最后返回這個臨時變量,作為屬性的默認值。
class SomeClass {
let someProperty: SomeType = {
// 在這個閉包中給 someProperty 創建一個默認值
// someValue 必須和 SomeType 類型相同
return someValue
}()
}
相對于計算屬性每次返回相同一樣的值,閉包只是一次性賦值,后續可更改
可選鏈式調用
可選鏈式調用是一種可以在當前值可能為nil的可選值上請求和調用屬性、方法及下標的方法。如果可選值有值,那么調用就會成功;如果可選值是nil,那么調用將返回nil。多個調用可以連接在一起形成一個調用鏈,如果其中任何一個節點為nil,整個調用鏈都會失敗,即返回nil。
Swift 的可選鏈式調用和 Objective-C 中向nil發送消息有些相像,但是 Swift 的可選鏈式調用可以應用于任意類型,并且能檢查調用是否成功。
使用可選鏈式調用代替強制展開
通過在想調用的屬性、方法、或下標的可選值后面放一個問號(?),可以定義一個可選鏈。這一點很像在可選值后面放一個嘆號(!)來強制展開它的值。它們的主要區別在于當可選值為空時可選鏈式調用只會調用失敗,然而強制展開將會觸發運行時錯誤。
可選鏈式調用的返回結果與原本的返回結果具有相同的類型,但是被包裝成了一個可選值。例如,使用可選鏈式調用訪問屬性,當可選鏈式調用成功時,如果屬性原本的返回結果是Int類型,則會變為Int?類型。
通過可選鏈式調用訪問屬性
正如使用可選鏈式調用代替強制展開中所述,可以通過可選鏈式調用在一個可選值上訪問它的屬性,并判斷訪問是否成功。通過可選鏈式調用調用方法
可以通過可選鏈式調用來調用方法,并判斷是否調用成功,即使這個方法沒有返回值。
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
這個方法沒有返回值。然而,沒有返回值的方法具有隱式的返回類型Void,如無返回值函數中所述。這意味著沒有返回值的方法也會返回(),或者說空的元組。
如果在可選值上通過可選鏈式調用來調用這個方法,該方法的返回類型會是Void?,而不是Void,因為通過可選鏈式調用得到的返回值都是可選的。這樣我們就可以使用if語句來判斷能否成功調用printNumberOfRooms()方法,即使方法本身沒有定義返回值。通過判斷返回值是否為nil可以判斷調用是否成功:
- 通過可選鏈式調用訪問下標
通過可選鏈式調用,我們可以在一個可選值上訪問下標,并且判斷下標調用是否成功。
連接多層可選鏈式調用
可以通過連接多個可選鏈式調用在更深的模型層級中訪問屬性、方法以及下標。然而,多層可選鏈式調用不會增加返回值的可選層級。
也就是說:
- 如果你訪問的值不是可選的,可選鏈式調用將會返回可選值。
- 如果你訪問的值就是可選的,可選鏈式調用不會讓可選返回值變得“更可選”。
因此: - 通過可選鏈式調用訪問一個Int值,將會返回Int?,無論使用了多少層可選鏈式調用。
- 類似的,通過可選鏈式調用訪問Int?值,依舊會返回Int?值,并不會返回Int??。
在方法的可選返回值上進行可選鏈式調用
我們可以在一個可選值上通過可選鏈式調用來調用方法,并且可以根據需要繼續在方法的可選返回值上進行可選鏈式調用
if let beginsWithThe =
john.residence?.address?.buildingIdentifier()?.hasPrefix("The") {
if beginsWithThe {
print("John's building identifier begins with \"The\".")
} else {
print("John's building identifier does not begin with \"The\".")
}
}
// 打印 “John's building identifier begins with "The".”
錯誤處理
錯誤處理(Error handling)是響應錯誤以及從錯誤中恢復的過程。Swift 提供了在運行時對可恢復錯誤的拋出、捕獲、傳遞和操作的一等公民支持。
表示并拋出錯誤
在 Swift 中,錯誤用符合Error協議的類型的值來表示。這個空協議表明該類型可以用于錯誤處理。
Swift 的枚舉類型尤為適合構建一組相關的錯誤狀態,枚舉的關聯值還可以提供錯誤狀態的額外信息。例如,你可以這樣表示在一個游戲中操作自動販賣機時可能會出現的錯誤狀態:
enum VendingMachineError: Error {
case invalidSelection //選擇無效
case insufficientFunds(coinsNeeded: Int) //金額不足
case outOfStock //缺貨
}
拋出一個錯誤可以讓你表明有意外情況發生,導致正常的執行流程無法繼續執行。拋出錯誤使用throw關鍵字。例如,下面的代碼拋出一個錯誤,提示販賣機還需要5個硬幣:
throw VendingMachineError. insufficientFunds(coinsNeeded: 5)
用 throwing 函數傳遞錯誤
為了表示一個函數、方法或構造器可以拋出錯誤,在函數聲明的參數列表之后加上throws關鍵字。一個標有throws關鍵字的函數被稱作throwing 函數。如果這個函數指明了返回值類型,throws關鍵詞需要寫在箭頭(->)的前面。
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
用 Do-Catch 處理錯誤
可以使用一個do-catch語句運行一段閉包代碼來處理錯誤。如果在do子句中的代碼拋出了一個錯誤,這個錯誤會與catch子句做匹配,從而決定哪條子句能處理它。
do {
try expression
statements
} catch pattern 1 {
statements
} catch pattern 2 where condition {
statements
}
指定清理操作
可以使用defer語句在即將離開當前代碼塊時執行一系列語句。該語句讓你能執行一些必要的清理工作,不管是以何種方式離開當前代碼塊的——無論是由于拋出錯誤而離開,或是由于諸如return、break的語句。例如,你可以用defer語句來確保文件描述符得以關閉,以及手動分配的內存得以釋放。
defer語句將代碼的執行延遲到當前的作用域退出之前。該語句由defer關鍵字和要被延遲執行的語句組成。延遲執行的語句不能包含任何控制轉移語句,例如break、return語句,或是拋出一個錯誤。延遲執行的操作會按照它們聲明的順序從后往前執行——也就是說,第一條defer語句中的代碼最后才執行,第二條defer語句中的代碼倒數第二個執行,以此類推。最后一條語句會第一個執行
func processFile(filename: String) throws {
if exists(filename) {
let file = open(filename)
defer {
close(file)
}
while let line = try file.readline() {
// 處理文件。
}
// close(file) 會在這里被調用,即作用域的最后。
}
}
類型轉換
類型轉換 可以判斷實例的類型,也可以將實例看做是其父類或者子類的實例。
類型轉換在 Swift 中使用 is 和 as 操作符實現。這兩個操作符提供了一種簡單達意的方式去檢查值的類型或者轉換它的類型。
檢查類型
用類型檢查操作符(is)來檢查一個實例是否屬于特定子類型。若實例屬于那個子類型,類型檢查操作符返回 true,否則返回 false。
var movieCount = 0
var songCount = 0
for item in library {
if item is Movie {
movieCount += 1
} else if item is Song {
songCount += 1
}
}
print("Media library contains \(movieCount) movies and \(songCount) songs")
// 打印 “Media library contains 2 movies and 3 songs”
向下轉型
某類型的一個常量或變量可能在幕后實際上屬于一個子類。當確定是這種情況時,你可以嘗試向下轉到它的子類型,用類型轉換操作符(as? 或 as!)。
因為向下轉型可能會失敗,類型轉型操作符帶有兩種不同形式。條件形式as? 返回一個你試圖向下轉成的類型的可選值。強制形式 as! 把試圖向下轉型和強制解包轉換結果結合為一個操作。
當你不確定向下轉型可以成功時,用類型轉換的條件形式(as?)。條件形式的類型轉換總是返回一個可選值,并且若下轉是不可能的,可選值將是 nil。這使你能夠檢查向下轉型是否成功。
只有你可以確定向下轉型一定會成功時,才使用強制形式(as!)。當你試圖向下轉型為一個不正確的類型時,強制形式的類型轉換會觸發一個運行時錯誤
for item in library {
if let movie = item as? Movie {
print("Movie: '\(movie.name)', dir. \(movie.director)")
} else if let song = item as? Song {
print("Song: '\(song.name)', by \(song.artist)")
}
}
// Movie: 'Casablanca', dir. Michael Curtiz
// Song: 'Blue Suede Shoes', by Elvis Presley
// Movie: 'Citizen Kane', dir. Orson Welles
// Song: 'The One And Only', by Chesney Hawkes
// Song: 'Never Gonna Give You Up', by Rick Astley
Any 和 AnyObject 的類型轉換
var things = [Any]()
things.append(0)
things.append(0.0)
things.append(42)
things.append(3.14159)
things.append("hello")
things.append((3.0, 5.0))
things.append(Movie(name: "Ghostbusters", director: "Ivan Reitman"))
things.append({ (name: String) -> String in "Hello, \(name)" })
things 數組包含兩個 Int 值,兩個 Double 值,一個 String 值,一個元組 (Double, Double),一個Movie實例“Ghostbusters”,以及一個接受 String 值并返回另一個 String 值的閉包表達式。
你可以在 switch 表達式的 case 中使用 is 和 as 操作符來找出只知道是 Any 或 AnyObject 類型的常量或變量的具體類型。下面的示例迭代 things 數組中的每一項,并用 switch 語句查找每一項的類型。有幾個 switch 語句的 case 綁定它們匹配到的值到一個指定類型的常量,從而可以打印這些值:
for thing in things {
switch thing {
case 0 as Int:
print("zero as an Int")
case 0 as Double:
print("zero as a Double")
case let someInt as Int:
print("an integer value of \(someInt)")
case let someDouble as Double where someDouble > 0:
print("a positive double value of \(someDouble)")
case is Double:
print("some other double value that I don't want to print")
case let someString as String:
print("a string value of \"\(someString)\"")
case let (x, y) as (Double, Double):
print("an (x, y) point at \(x), \(y)")
case let movie as Movie:
print("a movie called '\(movie.name)', dir. \(movie.director)")
case let stringConverter as String -> String:
print(stringConverter("Michael"))
default:
print("something else")
}
}
// zero as an Int
// zero as a Double
// an integer value of 42
// a positive double value of 3.14159
// a string value of "hello"
// an (x, y) point at 3.0, 5.0
// a movie called 'Ghostbusters', dir. Ivan Reitman
// Hello, Michael
擴展(Extensions)
擴展 就是為一個已有的類、結構體、枚舉類型或者協議類型添加新功能。這包括在沒有權限獲取原始源代碼的情況下擴展類型的能力(即 逆向建模 )。擴展和 Objective-C 中的分類類似。(與 Objective-C 不同的是,Swift 的擴展沒有名字。)
Swift 中的擴展可以:
- 添加計算型屬性和計算型類型屬性
- 定義實例方法和類型方法
- 提供新的構造器
- 定義下標
- 定義和使用新的嵌套類型
- 使一個已有類型符合某個協議
擴展語法
使用關鍵字 extension 來聲明擴展:
extension SomeType {
// 為 SomeType 添加的新功能寫到這里
}
extension SomeType: SomeProtocol, AnotherProctocol {
// 協議實現寫到這里
}
可變實例方法
通過擴展添加的實例方法也可以修改該實例本身。結構體和枚舉類型中修改 self 或其屬性的方法必須將該實例方法標注為 mutating,正如來自原始實現的可變方法一樣。
extension Int {
mutating func square() {
self = self * self
}
}
var someInt = 3
someInt.square()
// someInt 的值現在是 9
協議
協議 定義了一個藍圖,規定了用來實現某一特定任務或者功能的方法、屬性,以及其他需要的東西。類、結構體或枚舉都可以遵循協議,并為協議定義的這些要求提供具體實現。某個類型能夠滿足某個協議的要求,就可以說該類型遵循這個協議。
協議語法
協議的定義方式與類、結構體和枚舉的定義非常相似:
protocol SomeProtocol {
// 這里是協議的定義部分
}
struct SomeStructure: FirstProtocol, AnotherProtocol {
// 這里是結構體的定義部分
}
擁有父類的類在遵循協議時,應該將父類名放在協議名之前,以逗號分隔:
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
// 這里是類的定義部分
}
屬性要求
協議可以要求遵循協議的類型提供特定名稱和類型的實例屬性或類型屬性。協議不指定屬性是存儲型屬性還是計算型屬性,它只指定屬性的名稱和類型。此外,協議還指定屬性是可讀的還是可讀可寫的。
如果協議要求屬性是可讀可寫的,那么該屬性不能是常量屬性或只讀的計算型屬性。如果協議只要求屬性是可讀的,那么該屬性不僅可以是可讀的,如果代碼需要的話,還可以是可寫的。
協議總是用 var 關鍵字來聲明變量屬性,在類型聲明后加上 { set get } 來表示屬性是可讀可寫的,可讀屬性則用 { get } 來表示:
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
在協議中定義類型屬性時,總是使用 static 關鍵字作為前綴。當類類型遵循協議時,除了 static 關鍵字,還可以使用 class 關鍵字來聲明類型屬性:
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
方法要求
協議可以要求遵循協議的類型實現某些指定的實例方法或類方法。這些方法作為協議的一部分,像普通方法一樣放在協議的定義中,但是不需要大括號和方法體。可以在協議中定義具有可變參數的方法,和普通方法的定義方式相同。但是,不支持為協議中的方法的參數提供默認值。
正如屬性要求中所述,在協議中定義類方法的時候,總是使用 static 關鍵字作為前綴。當類類型遵循協議時,除了 static 關鍵字,還可以使用 class 關鍵字作為前綴:
protocol SomeProtocol {
static func someTypeMethod()
}
實例方法:
protocol RandomNumberGenerator {
func random() -> Double
}
Mutating 方法要求
有時需要在方法中改變方法所屬的實例。例如,在值類型(即結構體和枚舉)的實例方法中,將 mutating 關鍵字作為方法的前綴,寫在 func 關鍵字之前,表示可以在該方法中修改它所屬的實例以及實例的任意屬性的值。這一過程在在實例方法中修改值類型章節中有詳細描述。
如果你在協議中定義了一個實例方法,該方法會改變遵循該協議的類型的實例,那么在定義協議時需要在方法前加 mutating 關鍵字。這使得結構體和枚舉能夠遵循此協議并滿足此方法要求。
enum OnOffSwitch: Togglable {
case off, on
mutating func toggle() {
switch self {
case .off:
self = .on
case .on:
self = .off
}
}
}
構造器要求
協議可以要求遵循協議的類型實現指定的構造器。你可以像編寫普通構造器那樣,在協議的定義里寫下構造器的聲明,但不需要寫花括號和構造器的實體:
protocol SomeProtocol {
init(someParameter: Int)
}
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// 這里是構造器的實現部分
}
}
如果一個子類重寫了父類的指定構造器,并且該構造器滿足了某個協議的要求,那么該構造器的實現需要同時標注 required 和 override 修飾符:
protocol SomeProtocol {
init()
}
class SomeSuperClass {
init() {
// 這里是構造器的實現部分
}
}
class SomeSubClass: SomeSuperClass, SomeProtocol {
// 因為遵循協議,需要加上 required
// 因為繼承自父類,需要加上 override
required override init() {
// 這里是構造器的實現部分
}
}
協議作為類型
盡管協議本身并未實現任何功能,但是協議可以被當做一個成熟的類型來使用。
協議可以像其他普通類型一樣使用,使用場景如下:
- 作為函數、方法或構造器中的參數類型或返回值類型
- 作為常量、變量或屬性的類型
- 作為數組、字典或其他容器中的元素類型
class LinearCongruentialGenerator : RandomNumberGenerator{
}
class Dice {
let sides: Int
let generator: RandomNumberGenerator
init(sides: Int, generator: RandomNumberGenerator) {
self.sides = sides
self.generator = generator
}
func roll() -> RandomNumberGenerator {
return LinearCongruentialGenerator()
}
}
委托(代理)模式
委托是一種設計模式,它允許類或結構體將一些需要它們負責的功能委托給其他類型的實例。委托模式的實現很簡單:定義協議來封裝那些需要被委托的功能,這樣就能確保遵循協議的類型能提供這些功能。委托模式可以用來響應特定的動作,或者接收外部數據源提供的數據,而無需關心外部數據源的類型。
protocol DiceGame {
var dice: Dice { get }
func play()
}
protocol DiceGameDelegate {
func gameDidStart(_ game: DiceGame)
func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
func gameDidEnd(_ game: DiceGame)
}
通過擴展添加協議一致性
即便無法修改源代碼,依然可以通過擴展令已有類型遵循并符合協議。擴展可以為已有類型添加屬性、方法、下標以及構造器,因此可以符合協議中的相應要求。
protocol TextRepresentable {
var textualDescription: String { get }
}
extension Dice: TextRepresentable {
var textualDescription: String {
return "A \(sides)-sided dice"
}
}
通過擴展遵循協議
當一個類型已經符合了某個協議中的所有要求,卻還沒有聲明遵循該協議時,可以通過空擴展體的擴展來遵循該協議:
struct Hamster {
var name: String
var textualDescription: String {
return "A hamster named \(name)"
}
}
extension Hamster: TextRepresentable {}
//從現在起,Hamster 的實例可以作為 TextRepresentable 類型使用:
協議類型的集合
協議類型可以在數組或者字典這樣的集合中使用,在協議類型提到了這樣的用法。下面的例子創建了一個元素類型為 TextRepresentable 的數組:
let things: [TextRepresentable] = [game, d12, simonTheHamster]
協議的繼承
協議能夠繼承一個或多個其他協議,可以在繼承的協議的基礎上增加新的要求。協議的繼承語法與類的繼承相似,多個被繼承的協議間用逗號分隔:
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// 這里是協議的定義部分
}
類類型專屬協議
你可以在協議的繼承列表中,通過添加 class 關鍵字來限制協議只能被類類型遵循,而結構體或枚舉不能遵循該協議。class 關鍵字必須第一個出現在協議的繼承列表中,在其他繼承的協議之前:
protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol {
// 這里是類類型專屬協議的定義部分
}
協議合成
有時候需要同時遵循多個協議,你可以將多個協議采用 SomeProtocol & AnotherProtocol 這樣的格式進行組合,稱為 協議合成(protocol composition)。你可以羅列任意多個你想要遵循的協議,以與符號(&)分隔。
下面的例子中,將 Named 和 Aged 兩個協議按照上述語法組合成一個協議,作為函數參數的類型:
protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
struct Person: Named, Aged {
var name: String
var age: Int
}
func wishHappyBirthday(to celebrator: Named & Aged) {
print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// 打印 “Happy birthday Malcolm - you're 21!”
檢查協議一致性
你可以使用類型轉換中描述的 is 和 as 操作符來檢查協議一致性,即是否符合某協議,并且可以轉換到指定的協議類型。檢查和轉換到某個協議類型在語法上和類型的檢查和轉換完全相同:
- is 用來檢查實例是否符合某個協議,若符合則返回 true,否則返回 false。
- as? 返回一個可選值,當實例符合某個協議時,返回類型為協議類型的可選值,否則返回 nil。
- as! 將實例強制向下轉換到某個協議類型,如果強轉失敗,會引發運行時錯誤。
可選的協議要求
協議可以定義可選要求,遵循協議的類型可以選擇是否實現這些要求。在協議中使用 optional 關鍵字作為前綴來定義可選要求。可選要求用在你需要和 Objective-C 打交道的代碼中。協議和可選要求都必須帶上@objc屬性。標記 @objc 特性的協議只能被繼承自 Objective-C 類的類或者 @objc 類遵循,其他類以及結構體和枚舉均不能遵循這種協議。
使用可選要求時(例如,可選的方法或者屬性),它們的類型會自動變成可選的。比如,一個類型為 (Int) -> String 的方法會變成 ((Int) -> String)?。需要注意的是整個函數類型是可選的,而不是函數的返回值。
協議中的可選要求可通過可選鏈式調用來使用,因為遵循協議的類型可能沒有實現這些可選要求。類似 someOptionalMethod?(someArgument) 這樣,你可以在可選方法名稱后加上 ? 來調用可選方法。
協議擴展
協議可以通過擴展來為遵循協議的類型提供屬性、方法以及下標的實現。通過這種方式,你可以基于協議本身來實現這些功能,而無需在每個遵循協議的類型中都重復同樣的實現,也無需使用全局函數。
extension RandomNumberGenerator {
func randomBool() -> Bool {
return random() > 0.5
}
}
提供默認實現
可以通過協議擴展來為協議要求的屬性、方法以及下標提供默認的實現。如果遵循協議的類型為這些要求提供了自己的實現,那么這些自定義實現將會替代擴展中的默認實現被使用。
通過協議擴展為協議要求提供的默認實現和可選的協議要求不同。雖然在這兩種情況下,遵循協議的類型都無需自己實現這些要求,但是通過擴展提供的默認實現可以直接調用,而無需使用可選鏈式調用。
extension PrettyTextRepresentable {
var prettyTextualDescription: String {
return textualDescription
}
}
為協議擴展添加限制條件
在擴展協議的時候,可以指定一些限制條件,只有遵循協議的類型滿足這些限制條件時,才能獲得協議擴展提供的默認實現。這些限制條件寫在協議名之后,使用 where 子句來描述,正如Where子句中所描述的。
extension Collection where Iterator.Element: TextRepresentable {
var textualDescription: String {
let itemsAsText = self.map { $0.textualDescription }
return "[" + itemsAsText.joined(separator: ", ") + "]"
}
}
如果多個協議擴展都為同一個協議要求提供了默認實現,而遵循協議的類型又同時滿足這些協議擴展的限制條件,那么將會使用限制條件最多的那個協議擴展提供的默認實現。