歡迎加入「精通Swift」系列教程,本文會與過去注重某一個應用的AppCoda文章不太一樣,這回不是要教你如何使用iOS APIs或是特定iOS主題,而是教你如何操作Swift,它是蘋果提供給開發者的新語言,我們將會探索一些使用技巧和技術,讀者可以跟著本篇文章讓你的Swift程式碼更加Swift,這個新語言的設計考慮了安全、清晰和穩定性,我們將使用Swift的幾個關鍵功能來實現這些目標。
讓我們開始吧,啟動Xcode並創建一個Playground文件。讀者不需要特別初始化一個新的專案來遵循本教程。這篇文章只是帶你探索程式碼,並使用Playgrounds測試它。
Enumerations(列舉)
如果你還沒聽過它,enumerations(或稱為enums
)是Swift中一種特殊類型,它允許你表示多個「情況」或可能性。Enums(列舉)類似於Bool
,但Bool
只能是true
或false
,enums可以由開發者自行定義各種情況,列舉值(enum values)則為預設的多種情況之一,讓我們來看看。
假設你已經打開了Xcode的Playgrounds,我們先宣告一個enum:
enum DownloadStatus {
case downloading
case finished
case failed
case cancelled
}
如你所見,宣告一個enum是如此簡單,在我們上方的範例中,宣告了一個名為DownloadStatus
的enum,並且設定downloading
、finished
、failed
和cancelled
等四種情況,使用enum就像String
或Int
等其他任何類型一樣:>
var currentStatus = DownloadStatus.downloading
編者的話
此時,你可能會想,為什麼需要使用enum來定義多個情況,而不選擇宣告一個包含四個情境項目的array,如下圖所示:
let downloadStatus = [“downloading”, “finished”, “failed”, “cancelled”]
let currentStatus = downloadStatus[0]
你可以這樣做沒錯,但是如此一來會有兩個缺點,首先,你可能會不知道downloadStatus[0]代表什麼,除非你引用downloadStatus array,若是比較downloadStatus[0]與DownloadStatus.downloading這兩種表達方式,很明顯的是後者的可讀性比較高。
其次,因為currentStatus是String類型,變量可以被賦予任何字符串值,無法將它限制為 “downloading”, “finished”, “failed” 以及 “cancelled”,除非執行一些額外的驗證。反之,如果使用 enum,我們可以將 myDirection 限制在 .downloading、.finished、.failed或 .cancelled等四種情境之一,不會出現其他意料之外的情況。
當我們宣告enums,它的實際名稱(在本例中為DownloadStatus
)應以大寫字母開頭,並且要是單數的。這意味著將我們的enum命名為downloadStatus
或 DownloadStatuses
在語法上是不正確的。在開發時遵守一致性原則和編程規範是很重要的,因此,它應該要被普遍遵循。
此外,從Swift 3開始,我們的enum內設定的情境字串要以小寫字母開頭。雖然在Swift 3之前,約定是用大寫字母開頭,但是這已經改變。如果你使用Swift 3或更高版本進行開發,請確保您的列舉情境字串以小寫字母開頭。
現在你知道enums是什麼了,接著來探討如何使用它們吧!Swift允許我們以switch
搭配列舉來使用。請看下面範例:
let currentStatus = DownloadStatus.downloading
switch currentStatus {
case .downloading:
print("Downloading...")
case .finished:
print("Just finished the download...")
case .failed:
print("Failed to download the file...")
case .cancelled:
print("The download is cancelled...")
}
它讓我們可以建構一個條件語句,比簡單的if
語句更強大。編譯器將強制我們在switch
語句中處理我們列舉中的每個情境,確保我們不會遺漏任何可能的情況,不會錯過任何東西,如此一來,將有助於提升程式碼的安全性。
直到現在,你可能認為列舉沒有替Swift添加新的功能。當然,它可以使我們的程式碼更安全,但你可以使用String
或幾個Bool
來表示列舉儲存的數據。現在,讓我們來看看Swift列舉中最強大的功能之一:associated values。它允許我們在列舉中儲存額外的數據。我們宣告一個新的列舉,稱為WeatherCondition
,它讓我們在每個天氣條件中可額外挾帶一些資訊:
enum Cloud {
case cirrus
case cumulus
case altocumulus
case stratus
case cumulonimbus
}
enum WeatherCondition {
case sunny(temperature: Float)
case rainy(inchesPerHour: Float)
case cloudy(cloudType: Cloud, windSpeed: Float)
}
在這個例子中,我們實際上宣告了兩個列舉:Cloud
和WeatherCondition
。不要太專注在Cloud
,先請看一下WeatherCondition
的宣告。我們提供三個情境,在enum每個情境中都存有附加資訊。在sunny
情況下,儲存一個參數名稱為temperature
的Float
。在rainy
情況下,附帶一個Float
類型的參數,命名為inchesPerHour
,另外在cloudy
情況下,我們附帶兩個參數,分別為Cloud
類型的cloudType
和Float
類型的windSpeed
。
正如你可能已經觀察到的,associated values讓我們很輕易的在列舉中儲存額外的資訊。讓我們看看如何使用它們:
let currentWeather = WeatherCondition.cloudy(cloudType: .cirrus, windSpeed: 4.2)
就像一般的情況,我們也可以在associated values列舉的情境中使用switch
:
switch currentWeather {
case .sunny(let temperature):
print("It is sunny and the temperature is \(temperature).")
case .rainy(let inchesPerHour):
print("It is raining at a rate of \(inchesPerHour) inches per hour.")
case .cloudy(let cloudType, let windSpeed):
print("It is cloudy; there are \(cloudType) clouds in the sky, and the wind speed is \(windSpeed).")
}
希望讀者可以看到列舉在應用程式中展現的價值。它可以使你的程式碼更安全,更清晰,更簡潔。也許你已經知道列舉,但可能沒注意到assocated values的用法,或如何透過switch
操作它們。無論如何,嘗試將其納入你的應用程式,它們非常有用。
Closures和高階函式
<closure(閉包)也是Swift的其中一項關鍵特性。在Objective C中,閉包被稱為blocks,它們概念是類似的,但是在Swift中變得更強大。簡單地說,閉包是一個沒有名字的函數。Swift的closure是第一類物件,意味著它們可以像任何其他類型一樣分配給變量,Closures也可以傳遞到函數中。>
closure的類型表示為(parameters)-> returnType
。例如,將String
和Float
當成參數並返回Void
的closure呈現方式如下:(String, Float) -> Void
。你可以編寫一個接受closures作為參數的函數:
func myFunction(_ stringParameter: String, closureParameter: (String) -> Void) {
closureParameter(stringParameter)
}
上面的函數接受一個string和一個closure,然後調用這個閉包,並且將string提供為它的參數。這裡有一個使用的例子:
myFunction("Hello, world!", closureParameter: {(string) in
print(string) //prints "Hello, world!"
})
在這種情況下,我們將Closure提供給函數,就像我們提供一個正常的參數一樣。然後,在函數中我們調用這個閉包,就像調用任何其他用func
宣告的函數,這個例子說明了Closure真的只是未命名的函數。事實上,其他語言的Closures有時被稱為匿名函數,因為它們的特性就如其名。
Swift也有一個稱為trailing closure syntax的用法,這是Closure的語法蜜糖(syntax sugar)。當Closure是函數的最後一個參數時,它允許用一個更清晰和優雅的表達方式。這裡有一個例子,我們使用相同的函數:
myFunction("Hello, world!") {(string) in
print(string) //prints "Hello, world!"
}
Trailing closure syntax允許我們消除函數中closure參數周圍的括號,但前提是它為最後一個參數。當編寫函數時,請確保你把closure參數放在最後,這樣可以透過乾淨和簡潔的方式調用你的程式碼。
因為closures在Swift是如此的強大,也讓標準函式庫(Standard Library)提供一系列高階函數(higher order functions)。簡單地說,高階函數是將另一個函數做為參數的函數,它存在於Swift的集合類型上,提供map
、filter
、forEach
、reduce
以及flatMap
等函式,將會在接下來的篇幅介紹它們。
Map
讓我們從map
開始介紹,在Playgrounds中輸入下列程式碼,看看得到什麼結果。
let mapNumbers = [1, 2, 3, 4, 5]
let doubledMapNumbers = mapNumbers.map { $0 * 2 }
print(doubledMapNumbers) //prints [2, 4, 6, 8, 10]
上面的代碼是map
函數的一個範例。如你所見,通過map功能將每個項目在mapNumbers數組中乘以2是如此簡單。雖然也可以使用for
loop替代,但是map
函數為你精簡很多code。
map
函數可以傳入一個closure,它會跑遍集合中所有元素
,對每一個元素執行closure中定義的操作,且返回與元素相同類型的值。在我們的例子中,參數沒有命名,意味著我們沒有明確地提供它的名字。因此,我們可以將其稱為$0
。後續的未命名參數可以被稱為$1
,$2
,$3
,依此類推。
除此之外,你可能會注意到closure內沒有執行返回值的程式碼。對於只有一行的closure,我們不需要使用return
,因為它已經被隱含起來了。最後,我們使用trailing closure syntax,這允許我們進一步減少我們的代碼的長度。這些快捷的語法表達方式都讓我們得以寫出更簡潔的closure表達式。 這裡有與let doubledMapNumbers = mapNumbers.map {$ 0 * 2}
相同的表達式,是看起來尚未經過簡化的程式碼:
let doubledMapNumbers = mapNumbers.map( {(number) in
return number * 2
})
如你所見,Swift為開發者提供很多方法來縮短closure表達式,讓程式碼更乾淨且更容易閱讀。
Filter
讓我們探索更多高階函數,請宣告另外一個array,filterNumbers
。
let filterNumbers = [1, 2, 3, 4, 5]
let filteredNumbers = filterNumbers.filter { $0 > 3 }
print(filteredNumbers) //prints [4, 5]
在這個範例中,我們過濾filteredNumbers
這個array,只保留大於3的項目。這個例子也使用了Swift多個語法優化的方式,以確保我們的程式碼是簡潔的。
forEach
讓我們針對forEachNumbers
這個array嘗試使用forEach
方法:
let forEachNumbers = [1, 2, 3, 4, 5]
forEachNumbers.forEach { print($0) } //prints one item of the array on each line
使用forEach
其實跟for
loop非常相似,我們再次使用多個語法優化代碼,以確保我們擁有乾淨的程式碼。
Reduce
現在,我們宣告一個名為reduceNumbers
的array,嘗試透過它學習使用reduce
函數:
let reduceNumbers = [1, 2, 3, 4 ,5]
let reducedNumber = reduceNumbers.reduce(0) { $0 + $1 }
print(reducedNumber) //prints 15
reduce
用於將集合內元素組合計算為單個值。在上面範例中,我們將reduceNumbers
中的所有數字加到一個值中,並儲存在reducedNumber
裡面。
我們為reduce
函數提供的參數(範例中為0
)是一個初始值。reduce
以我們的初始值開始,對集合中的每個項目執行指定的操作。
0 + 1 + 2 + 3 + 4 + 5 = 15,這就是為何reducedNumbers印出的結果為15。
flatMap
繼續介紹下一個吧!接下來讓我們使用flatMap
方法,宣告一個名為flatMapNumbers
的array。這一次,我們起始的array有點不同於前面的例子,內部元素包含nil
值:
let flatMapNumbers = [1, nil, 2, nil, 3, nil, 4, nil, 5]
let flatMappedNumbers = flatMapNumbers.flatMap { $0 }
print(flatMappedNumbers) //prints [1, 2, 3, 4, 5]
flatMap
跑遍集合內元素,並在closure內指定執行動作操作數值,最終集合僅包含非nil值。你會注意到,最終array不包含nil
元素,flatMap
對於從集合中處理optional values特別有用。
使用組合技
OK,我答應flatMap
將是最後一個範例,但這邊還有一個要介紹給讀者,它實際上是前面幾個方法變換的組合,你可以串連多個高階函數來創建一個強大轉換函式,它是無法透過單一轉換方法來實現的,以下提供一個例子:
let chainNumbers = [1, nil, 2, nil, 3, nil, 4, nil, 5]
let doubledNumbersOver8 = chainNumbers.flatMap { $0 }.filter { $0 > 3 }.map { $0 * 2 }
print(doubledNumbersOver8) //prints [8, 10]
希望上面的例子已經充分介紹Swift中closure和高階函數的威力,你可以在應用中使用這些技術來精簡你的程式碼,使它其更易於維護。祝你好運!
Generics(泛型)
Generics是一個很有趣的觀念,它允許開發者在不同類型中複用你的程式碼,讓我們看一個例子:
func swapInts(_ a: inout Int, _ b: inout Int) {
let temporaryB = b
b = a
a = temporaryB
}
我們的swapInts
函式有兩個Int
類型的參數,並在函式內將兩者數值交換,這就是一個使用的範例:
var num1 = 10
var num2 = 20
swapInts(&num1, &num2)
print(num1) // 20
print(num2) // 10
但是如果我們想互換的參數類型為string(字串)怎麼辦?也許你會寫另一個函數如下:
func swapStrings(_ a: inout String, _ b: inout String) {
let temporaryB = b
b = a
a = temporaryB
}
如你所見,兩個函數除了參數類型不同,內部執行的動作都是相同的。你能寫一個支援多種類型參數的交換函數嗎?我們可以使用Generics編寫更靈活的程式碼,將swap函數轉換為更通用的方法,請參考下列程式碼:
func swapAnything(_ a: inout T, _ b: inout T) {
let temporaryB = b
b = a
a = temporaryB
}
這裡來講解一下前面的範例,剛開始先宣告一個看起來很一般的Swift函數,但是正如你所見,我們將字母T放在尖括號之間,這是代表什麼意思呢?這個尖括號就是Swift的泛型語法,透過將字母T放在尖括號之間,我們告訴Swift正在建立一個稱為T
的generic type,現在可以在我們的函數中引用T
,如你所見,我們使用它來表示我們的參數的類型,函數的其餘部分只是基本宣告動作,現在可以使用我們的swapAnything
函數來互換任何類型的參數。
var string1 = "Happy" var string2 = "New Year" swapAnything(&string1, &string2) print(string1) // New Year print(string2) // Happy var bool1 = false var bool2 = true swapAnything(&bool1, &bool2) print(bool1) // true print(bool2) // false
讓我們來看一個更複雜的泛型例子。但首先讓我們回顧一下。對於熟悉Objective C的人,記得如何把從NSArray
存取的項目當作id
返回嗎? 如果我們這樣寫:
NSArray *myArray = @[1, 2, 3, 4, 5]; int myInt = (int)myArray[2];
我們必須要手動將數組中的元素轉換為它們的實際類型,但這是不安全的,如果你不知道數組中有什麼類型的item怎麼辦?如何知道你要把item轉為哪種類型?如果你忘了將它轉型,並且試圖使用該對象會怎麼樣呢?將會因為undefined selector或相關的問題可能導致應用程式crash。
這時候就是Swift泛型來救援的時刻,讓我們來看看下面的例子:
struct Stack {
private var storage = [Any]()
mutating func push(_ item: Any) {
storage.append(item)
}
mutating func pop() -> Any? {
return storage.removeLast()
}
}
var myStack = Stack()
myStack.push("foo")
myStack.push(5)
myStack.push(4.7)
let element = myStack.pop()
這裡我們宣告了一個stack,你可以push任何項目給它,並且能將一個項目pop拿取出來。
這裡有一個問題。存在stack中的item和我們拿出的元素
屬於Any
類型。這樣要如何知道這些元素
是什麼類型?正確的答案是…我們不知道,它可以是String、Int或Float。
如果我們不知道stack中的項目怎麼辦?我們該怎麼做?每次當你需要使用element
時,可能透過以下方式找尋它的類型:
switch element { case let number as Int: print(number) case let number as Double: print(number) case let text as String: print(text) default: print("Unknown type") }
它似乎不是一個好的解決方案,將items以Any
類型儲存在array裡面不是一個好的做法,更好的方法是將stack限制為單一類型,這種情況下我們將必須分別為String、Int以及String創建一個stack。在這裡,Swift中的泛型將允許你創建一個通用的stack:
struct GenericStack {
private var storage = [Element]()
mutating func push(_ item: Element) {
storage.append(item)
}
mutating func pop() -> Element? {
return storage.removeLast()
}
}
它沒有比我們的原始stack複雜太多,但現在stack為類型安全的狀態,來看看如何實用吧:
var textStack = GenericStack()
textStack.push("foo")
textStack.push("bar")
textStack.push("baz")
let textElement = textStack.pop() // baz
var numStack = GenericStack()
numStack.push(10)
numStack.push(20)
numStack.push(30)
let numElement = numStack.pop() // 30
現在我們使用泛型,pop
函數返回一個String
,而不是Any
,這為我們的程式碼大幅提升類型的安全性,並且消除了不必要的轉換。
希望讀者也可以找到在自己應用程式中使用泛型的方法,它們非常強大,可以優雅的解決很多問題,試著使用看看吧!
Value vs Reference Types
如果你從早期就加入Swift開發行列,你可能已經聽說過標準庫(Standard Library)廣泛使用了value types。如果你想知道value types是什麼,當語言已經漸趨成熟,現在是一個很好的時間點,而要了解value types,需要先了解reference types,讓我們來看一個Objective C的例子:
NSString *myString = @"Hello, world!";
NSString *myOtherString = myString;
myString = @"Guess what? We have a problem.";
NSLog(myOtherString); //prints "Guess what? We have a problem."
哇!發生什麼事?這是使用reference types發生危險的主要案例,你可以看到,當我們將myOtherString
設置為myString
時,編譯器不是複製myString
的值給myOtherString
,而是告訴myOtherString
去參考myString
。當myString
更改時,myOtherString
的值也會一起變動。如果開發人員不小心使用,這種類型的引用會導致很多錯誤,因此Swift語言的設計者選擇使用value types。讓我們看看在Swift中的同樣的例子:
var myString = "Hello, world!"
let myOtherString = myString
myString = "No problems here."
print(myOtherString) //prints "Hello, world!"
很好!這樣就沒有奇怪的參考語義(reference semantics)錯誤。 這使得大多數程式碼更容易為開發人員編寫和維護。在Swift中,struct
是value types,class
是reference types。這種知識在大多數日常開發工作中不是立即可以應用的,但它有助於理解,可以幫助你在class和struct之間做出選擇。
Protocols
Swift有另一個有趣的功能,稱為protocol。Protocols是訪問類別結構和繼承的一種新方法。在一般物件導向環境中,你定義class去描述object及其提供的功能,還有它們具有的屬性。然後,生成subclass去繼承你的類別,繼承所有的功能和屬性,然後,子類別可以提供額外功能和屬性,或者覆寫superclasses的一部分,這些子類可以進一步子類化,下面我們看一個典型的類別層次結構的範例:
class Animal {
func makeSound() {
print("Implement me!")
}
func move() {
print("Implement me!")
}
}
class Dog: Animal {
override func makeSound() {
print("Woof.")
}
override func move() {
print("walk around like a dog")
}
func bite() {
print("bite")
}
}
class Cat: Animal {
override func makeSound() {
print("Meow.")
}
override func move() {
print("walk around like a cat")
}
func scratch() {
print("scratch")
}
}
這個例子說明了一個層次結構,我們定義一個基類別(base class)Animal
,然後將子類別繼承於它。Dog
子類覆蓋Animal
類別的default功能,並且還添加了一個bite()
方法,它讓狗可以做出咬東西的動作。Cat
子類也覆蓋了base class,並添加了一個scratch()
方法,實作貓抓人的動作。>
這個例子讀者有看出什麼問題嗎?雖然結構本身看起來似乎沒有什麼問題,但在現實中,它有很多事情可以改進。如果有人將Animal
子類化但忘記覆寫makeSound()
和/或move()
,在這種情況下,它將只會印出“Implement me!”
class Tiger: Animal { func eat() { print("Eat like a tiger") } } let animal: Animal = Tiger() animal.makeSound() // Implement me! animal.move() // Implement me!
這是一個簡單的範例,但想像如果Animal
有幾十個需要被覆寫的函數和屬性,會發生什麼事情呢?
在Objective C中,這類情況替我們提供了稱為abstract base classes(抽象基類)的東西。抽象基類就如同Animal
,事實上,它們自身沒有提供多少功能,僅有需要被覆寫的事件列表,而非實際提供自己的功能。典型的Objective C的基類可能會丟一個Assertion(斷言),否則如果它直接使用,所在的程序將會crash。有鑑於此,抽象基類會記錄為抽象,告知開發人員不要直接使用它們,而是與它們的子類進行互動。
在Swift中沒有abstract class。然而,Swift(和Objective C)都提供了一種更好的方法來處理這種情況:protocols。雖然protocols存在於Swift和Objective C,但Swift裡面的protocols強大很多。碰到上述同樣的問題,我們可以將Animal
做為一個protocol:
protocol Animal {
func makeSound()
func move()
}
struct Dog: Animal {
func makeSound() {
print("Woof.")
}
func move() {
print("walk around like a dog")
}
func bite() {
print("bite")
}
}
struct Cat: Animal {
func makeSound() {
print("Meow.")
}
func move() {
print("walk around like a cat")
}
func scratch() {
print("scratch")
}
}
現在來看一下有什麼不同?我們仍然有dog和cat,它們也都提供繼承對象所宣告的方法,但是,這一次這些動物都採用共同protocol。每一個class定義了它的物件,而protocol則定義物件要做的事情,使用protocol的方法與我們先前做過的動作類似:
let animal: Animal = Dog() animal.makeSound() animal.move()
Cat和Dog都符合Animal
的protocol,它們必須實現makeSound
和move
方法。當採用一個protocol時,編譯器強制任何class或struct應提供protocol宣告時所指定需要實現的方法。因此,不可能有像這樣的class或struct:
編譯器會顯示一個錯誤訊息,確保開發者已提供protocol需要被滿足的實作項目。
我們將在本教程的延伸內文中進一步討論protocols,展示更多protocols有利於開發工作的應用情境。請記住,繼承類別或protocols都不能解決所有的編程問題,不要因為兩者都非常有用和強大,而被侷限在其中一個上面,有時需要兩者並用。
結尾
希望讀者可以在本篇文章學到很多東西,我們談到了很多重要的Swift主題,可以讓我們在應用程式中做一些非常強大的事情。無論你學習到列舉(enums),閉包(closure),泛型(generics),reference / value types,protocols或這些項目的組合技,希望本文教給讀者的技巧,可以應用在每天的開發工作上。如有任何問題,請隨時發表評論。
FB : https://www.facebook.com/yishen.chen.54
Twitter : https://twitter.com/YeEeEsS
原文:Mastering Swift: Enumerations, Closures, Generics, Protocols and High Order Functions