去年 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`關鍵字。
現在你應該了解要如何呼叫一個 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會告訴你以下的錯誤:
不再使用 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 來開發了。
原文:A Beginner's Guide to Swift 2