Swift 程式語言

Swift 2 初學者指南

Swift 2 初學者指南
Swift 2 初學者指南
In: Swift 程式語言

去年 Apple 帶來了 Swift,一個為針對 iOS 以及 OS 的全新程式語言。當它第一次宣布時,就跟其他開發者一樣。我非常的興奮,因為這宣稱是一個既快且安全的語言。跟預期一樣,這家公司今年在 WWDC 導入了 Swift 2。這篇初學者指南會讓你了解一下它帶來了哪些新的功能。

Swift 2今年一樣快速前進中。我們認為 Swift 是未來20年作為應用程式以及系統的主力程式語言,我們認為它他應該可以在任何地方都能用到且每一個人都能使用它。

– Craig Federighi, Apple的軟體工程資深副總

其中Swift 2一項最大的改變是它將在2015年底變成開源。倘若你錯過了 WWDC 的一些重點,或者你消息落後的話,你沒看錯:Swift準備開源。 這是一件大事。Apple會在 OSI 標準許可證(OSI compliant license)底下公開釋出Swift 原始碼,包含編譯器(compiler)與標準函式庫(library)。Apple也即將讓原始碼能與Linux介接。開發者將能夠在Linux使用Swift撰寫程式,進而對這個程式語言的發展做出貢獻。事實上對於語言的開發是值得鼓勵的。或許未來的某一天,你便能夠使用 Swift 來開發Android App了。

除了這個令人興奮的消息,Swift 2引進一些新的功能,包括了改善錯誤處理、協定擴展(protocol extension)以及可行性檢查(availability check)。

do-while 現在變成 repeat-while

我們先從基本的開始。這個經典的 do-while 迴圈現在重新命名為 repeat-while, 例如:

var i = 0
repeat {
    i++
    print(i)
} while i < 10

如同待會這節所述,這個 do 關鍵字現在也應用了其他語言的功能。 將do改為repeat,讓你能夠容易的辨識這是一個迴圈。

for-in where 子句

另外一個基本功能的導入是針對 for-in 陳述導入where子句。 你現在可以在 for 使用 where 子句。 當你對陣列執行迴圈,譬如對於符合規則的項目才能繼續。

let numbers = [20, 18, 39, 49, 68, 230, 499, 238, 239, 723, 332]
	
for number in numbers where number > 100 {
    print(number)
}

以上的例子中,它只會列印大於 100 的數字。

if-case 模式匹配

當 Swift 第一次釋出時,switch 陳述也做了相當的更新。 不只可以使用 switch 來匹配任何資料型態, switch 也同時支援了範圍(range)與模式(pattern)匹配, 以下是一個範圍匹配的示範:

let examResult = 49
	
switch examResult {
case 0...49: print("Fail!")
case 50...100: print("Pass!")
default: break
}

在 Swift 2, 除了 `switch`外,你可以使用 `if case` 來執行範圍匹配:

if case 0...49 = examResult {
    print("Fail!")
} else if case 50...100 = examResult {
    print("Pass!")
}

if case 也可以應用到模式匹配。這裏我使用元組(tuples)為例:

let userInfo = (id: "petersmith", name: "Peter Smith", age: 18, email: "simon")
	
if case (_, _, 0..<18, _) = userInfo {
    print("You're not allowed to register an account because you're below 18.")
} else if case (_, _, _, let email) = userInfo where email == "" {
    print("Your email is blank. Please fill in your email address.")
} else {
    print("You are good to go!")
}

第一個if case 子句測試使用者的歲數是否超過18。 這下底線表示「這個值我不在乎」, 這裏我們只確認這個使用者的年紀。 這個 else if case 是用來測試郵件是否為空值。 跟 switch 例子一樣,在使用 if case 時你可以將值暫時綁定。 我們綁定值給 email 常數並透過 where 子句執行匹配。

Guard 介紹

Swift導入了 guard 關鍵字,根據 Apple的文件。 guard 的敘述如下:

一個 guard 陳述,就像if 陳述一樣,依照一個表達式的布林值來執行陳述(statement)。為了讓guard陳述後的程式被執行,你使用一個 guard 陳述來取得必須為真(true)的條件。

在我繼續解釋 guard陳述前,我們直接進到這個例子:

struct Article {
    var title:String?
    var description:String?
    var author:String?
    var totalWords:Int?
}

func printInfo(article: Article) {
    if let totalWords = article.totalWords where totalWords > 1000 {
        if let title = article.title {
            print("Title: \(title)")
        } else {
            print("Error: Couldn't print the title of the article!")
        }
    } else {
        print("Error: Couldn't print the total word count!")
    }
}

let sampleArticle = Article(title: "Swift Guide", description: "A beginner's guide to Swift 2", author: "Simon Ng", totalWords: 1500)
printInfo(sampleArticle)

這裏我們建立一個printInfo 函數來顯示一篇文章的標題。 不過我們只是要印出一篇超過上千文字的文章資訊。 因為變數是 optional。我們使用 if let 來確認是否optional有包含一個值。倘若這個optional是 nil,我們會顯示一個錯誤訊息。 倘若你在 Playgrounds 執行程式,它會顯示文章的標題。

通常 if-else 陳述依照這個模式:

if some conditions are met {
    // 執行某些動作
if some conditions are met {
    // 執行某些動作
} else {
    // 顯示錯誤或執行其他操作
} else  {
    // 顯示錯誤或執行其他操作
}

你可能會這注意到,倘若你必須測試更多條件,它會嵌入更多條件。這樣的程式沒有什麼錯。但是以可讀性來看,你的程式看起來會很亂,因為有很多嵌套條件。

因此 guard 陳述因應而生。 guard 的語法如下所示:

guard  else {
    // 執行假如條件沒有匹配要做的動作
}

// 繼續執行一般的動作

倘若定義在 guard 陳述內的條件不匹配, else 後的程式便會執行。 換句話說,倘若條件符合,它會略過 else 子句並且繼續執行程式。

倘若你使用 guard 重寫以上的範例程式,會更簡潔:

func printInfo(article: Article) {
    guard let totalWords = article.totalWords where totalWords > 1000 else {
        print("Error: Couldn't print the total word count!")
        return
    }
	
    guard let title = article.title else {
        print("Error: Couldn't print the title of the article!")
        return
    }
	
    print("Title: \(title)")
}

有了guard你就將重點放在處理不想要的條件。甚至,它會強迫你一次處理一個狀況,避免有嵌套條件。如此一來程式便會變得更簡潔易讀。

錯誤處理

在開發一個 App 或者任何程式,不論好壞,你需要處理每一種可能發生的狀況。很清楚地,事情可能會有所出入。譬如說,倘若你開發一個連線到雲端的 App,你的 App 必須處理網路無法連線或者雲端伺服器故障而無法連接的情況。

在目前的 Swift版本,它缺少了適當的處理模式。舉例來說,處理錯誤條件的處理如下:

let request = NSURLRequest(URL: NSURL(string: "http://www.apple.com")!)
var response:NSURLResponse?
var error:NSError?
	
let data = NSURLConnection.sendSynchronousRequest(request, returningResponse: &response, error: &error)
	
if error == nil {
    print(response)
	
    // 解析資料
	
} else {
   // 處理錯誤
}

當呼叫一個方法時,可能會造成失敗,通常是傳遞一個 NSError 物件(像是一個指標)給它。倘若有錯誤,這個物件會設定對應的錯誤。然後你就可以檢查是否錯誤物件為 nil ,並且給予相對的回應。

這是在 Swift 1.2 處理錯誤的的做法。

注意: NSURLConnection.sendSynchronousRequest() 在 iOS 9 已經不推薦使用,但是因為大部分的讀者比較熟悉這個用法,所以在這個例子我們才使用它。

try / throw / catch

在 Swift 2 內建了使用 try / throw / catch keywords關鍵字,像例外(exception)的模式。相同的程式會變成這樣:

let request = NSURLRequest(URL: NSURL(string: "http://www.apple.com")!)
var response:NSURLResponse?
var error:NSError?
	
do {
    let data = try NSURLConnection.sendSynchronousRequest(request, returningResponse: &response)
	
    print(response)
	
    // Parse the data
	
} catch {
    // handle error
    print(error)
}

現在你可以使用 do-catch 陳述來捕捉(catch)錯誤並處理它。你可能有注意到。我們有放一個 try 關鍵字在呼叫方法前面,有了 Swift 2.0新錯誤處理模式的導入,一些方法會丟出錯誤來表示失敗。當我們調用一個 throw 方法,你需要放一個 try 關鍵字在前面。

你要怎麼知道是方法丟出一個錯誤?當你在內建編輯器輸入方法時,這個 throw 方法會標示出一個`throws`關鍵字。

throwing-methods

現在你應該了解要如何呼叫一個 throw 方法並捕捉錯誤,那要如何指示一個可以丟出錯誤的方法或函數呢?

想像你正在規劃一個輕量型的購物車,客戶可以使用這個購物車來短暫儲存並針對購買的貨物做結帳,但是購物車在以下的條件下會丟出錯誤:
* 購物車只能儲存最多五個商品,否則的話會丟出一個 cartIsFull 的錯誤。
* 結帳時在購物車至少要有一項購買商品,否則會丟出 cartIsEmpty的錯誤。

在 Swift,錯誤是由遵循 ErrorType 協定的型態值來呈現。 通常是使用一個列舉(enumeration)來規劃錯誤條件,在這裡,你可以建立一個採用 ErrorType 的列舉,如以下購物車錯誤的例子:

enum ShoppingCartError: ErrorType {
    case cartIsFull
    case emptyCart
}

對於購物車,我們建立一個 LiteShoppingCart 別來規劃它的函數。以下為其程式段:

struct Item {
    var price:Double
    var name:String
}
	
class LiteShoppingCart {
    var items:[Item] = []
	
    func addItem(item:Item) throws {
	guard items.count < 5 else {
            throw ShoppingCartError.cartIsFull
        }
    }
	
    items.append(item)
}
	
func checkout() throws {
    guard items.count > 0 else {
        throw ShoppingCartError.emptyCart
    }
	
    // 繼續結帳
}

倘若你更進一步看一下這個 addItem 方法,你可能會注意到這個 throws 關鍵字。 我們加入 throws 關鍵字在方法宣告處來表示這個方法可以丟出錯誤。在實作中,我們使用 guard 來確保全部商品數是少於5個。否則,我們會丟出 ShoppingCartError.cartIsFull 錯誤。

要丟出一個錯誤,你只要撰寫throw關鍵字,接著是實際錯誤。針對 checkout 方法。我們有相同的實作。 倘若購物車沒有包含任何商品,我們會丟出 ShoppingCartError.emptyCart 錯誤。

現在,我們來看結帳時購物車是空的會發生什麼事。 我建議你啟動 Xcode 並使用 Playgrounds來測試程式。

let shoppingCart = LiteShoppingCart()
do {
    try shoppingCart.checkout()
    print("Successfully checked out the items!")
} catch ShoppingCartError.cartIsFull {
    print("Couldn't add new items because the cart is full")
} catch ShoppingCartError.emptyCart {
    print("The shopping cart is empty!")
} catch {
    print(error)
}

因為checkout 方法會丟出一個錯誤, 我們使用 do-catch 陳述來捕捉錯誤, 倘若你在 Playgrounds 執行以上的程式, 它會捕捉 ShoppingCartError.emptyCart 錯誤並印出相對的錯誤訊息,因為我們沒有加入任何項目。

現在插入以下的程式至 do 子句, 在 checkout 方法前面:

try shoppingCart.addItem(Item(price: 100.0, name: "Product #1"))
try shoppingCart.addItem(Item(price: 100.0, name: "Product #2"))
try shoppingCart.addItem(Item(price: 100.0, name: "Product #3"))
try shoppingCart.addItem(Item(price: 100.0, name: "Product #4"))
try shoppingCart.addItem(Item(price: 100.0, name: "Product #5"))
try shoppingCart.addItem(Item(price: 100.0, name: "Product #6"))

在這裡我們加入全部6 個商品至shoppingCart物件。同樣的,它會丟出錯誤,因為購物車不能存放超過五個商品。

當捕捉到錯誤時,你可以指示一個正確的錯誤( 例如 ShoppingCartError.cartIsFull )來匹配, s因此你就可以提供一個非常具體的錯誤處理。 另外,倘若你沒有在 catch 子句指定一個模式(pattern), Swift會匹配任何錯誤並自動地綁定錯誤至 error 常數。 最棒的做法還是應該要試著去捕捉由throw方法所丟出的特定錯誤。 同時, 你可以寫一個 catch 子句來匹配任何錯誤。這可以確保所有可能的錯誤都有處理到。

defer

倘若你有寫過Java 程式語言,它提供一個相似的例外處理模式,稱作 try-catch-finally. 指定在 finally 子句的程式,不管錯誤為何都會執行。

Swift 2 導入 defer 關鍵字來提供一個相似的功能,不管錯誤為何,定義在 defer 區塊中的程式在目前的範圍完成之前會執行。我們繼續使用購物車為例來說明。譬如說,倘若你想要在結帳時印出購物車內的全部商品數,你可以插入defer 陳述至 checkout 方法,如下所示:

func checkout() throws {
    defer {
	print("Number of items in shopping cart = \(shoppingCart.items.count)")
    }
	
    guard items.count > 0 else {
	throw ShoppingCartError.emptyCart
    }
	
    // 繼續結帳
}

現在當你呼叫 checkout 方法, 不管他是否能正常完成或有錯誤,它會列印「Number of items in shopping cart」的訊息。

倘若你在Playgrounds測試 checkout 方法,如下所示:

let shoppingCart = LiteShoppingCart()
do {
    try shoppingCart.checkout()
	
    print("Successfully checked out the items!")
	
} catch ShoppingCartError.cartIsFull {
    print("Couldn't add new items because the cart is full")
} catch ShoppingCartError.emptyCart {
    print("The shopping cart is empty!")
} catch {
    print(error)
}

你會在主控台(console)見到這兩個訊息:

Number of items in shopping cart = 0
The shopping cart is empty!

defer 陳述對清除操作特別有用:

func aMethod() throws {
    // 打開一些資源,像是打開一個檔案
	
    defer {
        // 釋放資源並執行清除操作 (例如關閉一個檔案描述器)
    }
	
    // 錯誤處理(例如載入檔案失敗)
    // 以資源進行一些事情
	
}

協定擴展 (Protocol Extension)

在 Swift 1.2,你可以使用擴展(extension),在目前的類別(class)、結構(structure)或列舉(enumeration)加入新的功能。譬如說,你可以使用擴展加入新的功能至 String 類別:

extension String {
    func contains(find: String) -> Bool {
        return self.rangeOfString(find) != nil
    }
}

你不只可以幫類別建立擴展。Swift 2 可以讓開發者將擴展應用至協定型態。在Swift 2 之前,協定只包含了方法與屬性宣告。在一個類別在採用這些協定時,你需要提供你自己的實作。倘若你之前使用過UITableView的話,我相信你已經非常熟悉協定的實作。

class MyViewController: UITableViewDataSource, UITableViewDelegate {
    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        // Return the number of rows
    }
	
    .
    .
    .
}

有了協定擴展,你可以在目前的協定中加入方法或屬性。當你想要擴展協定的功能,這個功能非常強大。除了這個,你可以在協定的這些方法中以擴展來提供預設的實作。

我們來參考以下範例:

protocol Container {
    var items:[String] {get set}
    func numberOfItems() -> Int
}

class ToolBox: Container {
    var items:[String] = ["Glue Stick", "Scissors", "Hammer", "Level", "Screwdriver", "Jigsaw"]
    
    func numberOfItems() -> Int {
        return items.count
    }
}

class PaperBag: Container {
    var items:[String] = ["Bagel", "Baguette", "Black bread"]
    
    func numberOfItems() -> Int {
        return items.count
    }
}

class Basket: Container {
    var items:[String] = ["Orange", "Apple", "Honeydew", "Watermelon", "Pineapple"]
    
    func numberOfItems() -> Int {
        return items.count
    }
}

var container:Container = ToolBox()
print(container.numberOfItems())

我們宣告一個協定稱作 Container。在協定中,它有宣告一個稱作 numberOfItems的方法。對於任何採用 Container協定的類別,它必須實作這個方法來回傳容器中全部的項目數。

上的例子中,我們有三個類別模型,一個工具箱(ToolBox)、一個是紙袋(PaperBag)以及一個籃子(Basket)。每一個類別都採用Container協定,以回傳儲存在特定容器中的項目數。倘若你在Playgrounds,它會印出儲存在工具箱的項目數(也就是 6)。透過協定的使用,你可以很容易地以其他採用 Container 的協定的物件來指定 container 變數:

var container:Container = ToolBox()
container = Basket()
print(container.numberOfItems())

你可能會注意到,在這些類別中 numberOfItems 方法的實作是相同的。在 Swift 2 之前,你不能在協定中提供預設的實作。但是現在你可以透過協定擴展來達成:

extension Container {
    func numberOfItems() -> Int {
        return items.count
    }
}

有了預設的實作,這三個類別可以簡化如下:

class ToolBox: Container {
    var items:[String] = ["Glue Stick", "Scissors", "Hammer", "Level", "Screwdriver", "Jigsaw"]
}

class PaperBag: Container {
    var items:[String] = ["Bagel", "Baguette", "Black bread"]
}

class Basket: Container {
    var items:[String] = ["Orange", "Apple", "Honeydew", "Watermelon", "Pineapple"]
}

他們都具備了 numberOfItems 方法的預設實作。倘若預設實作不符合你的需求,你還是可以覆寫它。以下面這個例子為例:

class Basket: Container {
    var items:[String] = ["Orange", "Apple", "Honeydew", "Watermelon", "Pineapple"]
    var bonusItems:[String] = ["Blueberry", "Blackcurrant", "Durian"]
	
    func numberOfItems() -> Int {
        return items.count + bonusItems.count
    }
}

假設你想要加入一個稱作 randomItem的新方法,可以隨機回傳一個項目至協定中。在 Swift 1.2,你需要加入這方法至Container 協定。

protocol Container {
    var items:[String] {get set}
    func numberOfItems() -> Int
    func randomItem() -> String
}

在這裏,所有採用Container 協定的類別都需要變更並提供 randomItem 方法的實作。

有了協定擴展的導入,你可以輕易的加入方法至擴展中,而所有遵循這協定的類別可以自由實作它。

extension Container {
    func numberOfItems() -> Int {
        return items.count
    }
	
    func randomItem() -> String {
        let randomIndex = Int(arc4random_uniform(UInt32(items.count)))
        return items[randomIndex]
    }
}

是不是很棒?Swift 2大方的擴展了協定的範圍。這裏我只說明了協定擴展的基礎。你可以進一步利用協定導向程式( protocol-oriented programming )的力量,我不準備在這篇文章介紹。倘若你有興趣,你可以看一下這部很棒的 WWDC 影片.

可行性檢查 (Availability Check)

倘若所有的使用者被強迫更新到最新版的iOS版本,這會讓開發者更輕鬆些,但事實沒這麼理想,你的App必須應付不同iOS 的版本(例如 iOS 7 與 iOS8 )。倘若你只在你的App使用最新的版本的API,這樣一來,在其他較舊版本的iOS會造成錯誤。當使用只能在最新的iOS版本才能使用的API,你必須要在使用這個類別或呼叫這個方法前做一些驗證。

在Swift 2之前,沒有可行性檢查(availability check)的標準。例如,NSURLQueryItem 類別只能在iOS 8使用。倘若你在其他的iOS版本使用,你會得到錯誤並且可能造成 App 閃退。要避免這樣的錯誤,你可以像以下這樣執行可行性檢查:

if NSClassFromString("NSURLQueryItem") != nil {
    // iOS 8 or up
} else{
    // Earlier iOS versions
}

這是檢查類別是否存在的一個方式。從Swift 2開始,它內建了 API 可行性的檢查。你可以輕易地定義一個可行性條件,因此這段程式將只會在某些 iOS 版本執行。如下面這個例子:

if #available(iOS 8, *) {
    // iOS 8 or up
    let queryItem = NSURLQueryItem()
} else {
    // Earlier iOS versions
}

這邊在一個if陳述中使用 #available 關鍵字。 在這個可行性條件,你指定了要確認的OS版本(例如 iOS 8、OSX 10.10)。星號(*)是必要的,並指示了 if 子句所執行的最低部署目標以及其他OS的版本。 以上面例子來說, if 的主體將會在 iOS 8 或以上版本執行,以及其他平台,像是 watchOS。

相同地,你可以使用 guard 代替 if 來檢查 API 可行性,如下面這個例子:

guard #available(iOS 8.4, OS X 10.10, *) else {
    // what to do if it doesn't meet the minimum OS requirement
    return
}

那麼如果你想要開發一個類別或方法,可以讓某些OS的版本使用呢?Swift 2 讓你在類別/方法/函數 應用 @available 屬性來指定你的目標平台與OS 版本。 舉例來說,你正在開發一個類別稱作 SuperFancy,而它只能適用於iOS9或之後的版本,你可以像這樣應用 @available

@available(iOS 9, *)
class SuperFancy {
    // implementation
}

倘若你試著在Xcode 專案使用這個類別來支援多種iOS裝置,Xcode會告訴你以下的錯誤:

availability-check-warning

不再使用 println()

在Swift 2以前,是使用 println() 函數來列印訊息至主控台或log檔。 而最新版本的 Swift,我們只能使用 print() 來輸出所寫的訊息。 Apple 已經結合了 println()print() 函數為一個。 print() 函數印出你的訊息。倘若你不輸出某些東西不想要加上換行,你可以設定appendNewline 參數為 false,如以下例子:

print("Engine started", appendNewline: false)

總結

我希望你喜歡這篇Swift 2 初學者指南。有一些功能我還沒有研究。你可以進一步參考這部 WWDC影片 來學習更多 Swift 2的內容。至今許多公司還是使用 Objective C 作為iOS主要程式語言。或許你也是還在使用 Objective C。我堅定相信 Swift 的學習是正確,你將會見到越來越多公司開始雇用 Swift 程式設計師。事實上,所有的在2015 WWDC 的範例都是以 Swift 來撰寫。所以倘若你想要開始新的專案,是時候以 Swift 來開發了。

譯者簡介:王豪勳 -渥合數位服務創辦人,畢業於台灣大學應用力學研究所,曾在半導體產業服務多年,近年來專注於協助客戶進行App軟體以及網站開發,平常致力於研究各式最軟硬體技術,擁有多本譯作。
原文A Beginner's Guide to Swift 2
作者
Simon Ng
軟體工程師,AppCoda 創辦人。著有《iOS 16 App 程式設計實戰心法》、《iOS 16 App程式設計進階攻略》以及《精通SwiftUI》。曾任職於HSBC, FedEx等跨國企業,專責軟體開發、系統設計。2012年創立AppCoda技術部落格,定期發表iOS程式教學文章。現時專注發展AppCoda業務,致力於iOS程式教學、產品設計及開發。你可以到推特與我聯絡。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。