Swift 程式語言

精通Swift:列舉、閉包、泛型、Protocols和高階函數

精通Swift:列舉、閉包、泛型、Protocols和高階函數
精通Swift:列舉、閉包、泛型、Protocols和高階函數
In: Swift 程式語言

歡迎加入「精通Swift」系列教程,本文會與過去注重某一個應用的AppCoda文章不太一樣,這回不是要教你如何使用iOS APIs或是特定iOS主題,而是教你如何操作Swift,它是蘋果提供給開發者的新語言,我們將會探索一些使用技巧和技術,讀者可以跟著本篇文章讓你的Swift程式碼更加Swift,這個新語言的設計考慮了安全、清晰和穩定性,我們將使用Swift的幾個關鍵功能來實現這些目標。

讓我們開始吧,啟動Xcode並創建一個Playground文件。讀者不需要特別初始化一個新的專案來遵循本教程。這篇文章只是帶你探索程式碼,並使用Playgrounds測試它。

Enumerations(列舉)

如果你還沒聽過它,enumerations(或稱為enums)是Swift中一種特殊類型,它允許你表示多個「情況」或可能性。Enums(列舉)類似於Bool,但Bool只能是truefalse,enums可以由開發者自行定義各種情況,列舉值(enum values)則為預設的多種情況之一,讓我們來看看。

假設你已經打開了Xcode的Playgrounds,我們先宣告一個enum:

enum DownloadStatus {
    case downloading
    case finished
    case failed
    case cancelled
}

如你所見,宣告一個enum是如此簡單,在我們上方的範例中,宣告了一個名為DownloadStatus的enum,並且設定downloadingfinishedfailedcancelled等四種情況,使用enum就像StringInt等其他任何類型一樣:>

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)
}

在這個例子中,我們實際上宣告了兩個列舉:CloudWeatherCondition。不要太專注在Cloud,先請看一下WeatherCondition的宣告。我們提供三個情境,在enum每個情境中都存有附加資訊。在sunny情況下,儲存一個參數名稱為temperatureFloat。在rainy情況下,附帶一個Float類型的參數,命名為inchesPerHour,另外在cloudy情況下,我們附帶兩個參數,分別為Cloud類型的cloudTypeFloat類型的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 。例如,將StringFloat當成參數並返回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的集合類型上,提供mapfilterforEachreduce以及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正在建立一個稱為Tgeneric 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拿取出來。

Note: 如果你不熟悉它的運作方式,請參閱維基百科上的stacks。

這裡有一個問題。存在stack中的item和我們拿出的元素屬於Any類型。這樣要如何知道這些元素是什麼類型?正確的答案是…我們不知道,它可以是String、Int或Float。

編輯筆記:Swift提供了一個名為Any的類型,用於處理非特定類型。

如果我們不知道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,它們必須實現makeSoundmove方法。當採用一個protocol時,編譯器強制任何class或struct應提供protocol宣告時所指定需要實現的方法。因此,不可能有像這樣的class或struct:

swift-protocol-error

編譯器會顯示一個錯誤訊息,確保開發者已提供protocol需要被滿足的實作項目。

我們將在本教程的延伸內文中進一步討論protocols,展示更多protocols有利於開發工作的應用情境。請記住,繼承類別或protocols都不能解決所有的編程問題,不要因為兩者都非常有用和強大,而被侷限在其中一個上面,有時需要兩者並用。

結尾

希望讀者可以在本篇文章學到很多東西,我們談到了很多重要的Swift主題,可以讓我們在應用程式中做一些非常強大的事情。無論你學習到列舉(enums),閉包(closure),泛型(generics),reference / value types,protocols或這些項目的組合技,希望本文教給讀者的技巧,可以應用在每天的開發工作上。如有任何問題,請隨時發表評論。

編輯筆記:喜歡這邊教程嗎?你可能也會喜歡我們的Swift & iOS程式書籍
譯者簡介:陳奕先-過去為平面財經記者,專跑產業新聞,2015年起跨進軟體開發世界,希望在不同領域中培養新的視野,於新創學校ALPHA Camp畢業後,積極投入iOS程式開發,目前任職於國內電商公司。聯絡方式:電郵[email protected]

FB : https://www.facebook.com/yishen.chen.54
Twitter : https://twitter.com/YeEeEsS

原文Mastering Swift: Enumerations, Closures, Generics, Protocols and High Order Functions

作者
Pranjal Satija
現時為高中生,喜歡在課餘時間創作App,享受把創作App的經驗與人分享。除此之外還喜歡滑雪、高爾夫球、足球,與朋友在一起。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。