原文:What’s New in Swift 3
作者:COSMIN PUPĂZĂ
译者:kmyhy
在 WWDC 大會上,蘋果在 Xcode 8 beta 中集成了 Swift 3,最後的版本則需要到年末的時候才會放出。這是 Swift 開源以後第一個版本,它將同時支持 Mac OS X 和 Linux。如果你關注過去年 11 月份開始的 Swift Evolution 專案,它甚至已經可以在 IBM sandbox 上運行了,這門語言真的有了天翻地覆的變化。如果你用 Xcode 8 編譯你的專案,你會發現根本無法編譯了。
Swift 3 的改進主要集中在兩個方面:
讓我們先從简单的开始,即 Swift 3 中已刪除的語法特性。這些特性是你在 Xcode 7.3 的中曾經看到過的一些警告。
自增、自減運算是從 C 語言中繼承來的,它們的功能很簡單:在某個變量的基礎上減去或加上 1。
var i = 0 i++ ++i i-- --i
但是,当要选择用这些運算中的哪一個時,往往讓人不知所措。自增和自減運算子都會有兩種使用的方式:前置或後置 ── 它們的實現完全隱藏在底層,返回值對你來說可能有用,也可能無用,幸好我們還有運算子重載。
對於初學者來說,它們太難控制了,因此現在將它們移除了 ── 現在它們完全被 += 和 ﹣= 所替代了:
var i = 0 i += 1 i -= 1
雖然復合賦值運算要簡短一些,但只要你願意,你也可以用 + 和 ﹣ 運算来達到同樣的目的 :
i = i + 1 i = i - 1
[ecko_alert color=”gray”]編後語: 如果你想了解這项改進的最初提議,你可以閱讀Chris Lattner 的關於移除 ++/– 運算子的提議。[/ecko_alert]
對於自增、自減運算用得最多的地方就是 C 語言的經典循環了。當自增、自減運算子被移除后,同時也意味著 C 經典循環的歷史使命被終結了,因為經典循環能做的,用 for-in 循環結合區間的使用同樣能夠做到。
如果你有一定的編程經驗,那麼打印從 1 到 10 的數字你可以用 for 循環來實現:
for (i = 1; i <= 10; i++) {
print(i)
}
在 Swift 3 中,不再允許這樣做了。下面是 Swift 3 的同一功能的實現 ── 注意,封閉區間運算子 … 的使用:
for i in 1...10 { print(i) }
此外,你也可以使用 for each 循環和閉包、快捷參數來實現同樣的目的 ── 關於循環的更多討論,請參考這裡。
(1...10).forEach { print($0) }
[ecko_alert color=”gray”]編後語: 如果你想了解這項改進的最初提議,請閱讀 Erica Sadun 的關於 C 語言風格的 for 循環的提議 。[/ecko_alert]
函數參數一般是當做常量來用的,因為在方法中你通常不會修改它們的值。但是某些情況下,你需要將它們聲明為變量。在 Swift 2 中,你可以用 var 來修飾一個函數參數。一旦參數被 var 所修飾,它會創建一份局部變量的拷貝,因此你可以在函數中修改它的值。
例如,下面的函數判斷兩個數字中的最大公約數 ── 如果你忘記了你的高中數學,請你先看這裡:
func gcd(var a: Int, var b: Int) -> Int { if (a == b) { return a } repeat { if (a > b) { a = a - b } else { b = b - a } } while (a != b) return a }
算法很簡單:如果兩個數字相等,隨便返回其中一個。否則,對二者進行比較,用大的一個減去小的一個并將結果賦給大的一個,直到二者完全相等,最終返回二者之一。如你所見,a 和 b 都用 var 修飾了,因此在函數中我們可以任意改變二者的值。
Swift 3 不再允許這樣做,因為這樣會讓開發者將 var 和 inout 混淆起來。因此新版本中直接不允許在函數參數中使用 var。
因此,gcd 函數需要在 Swift 3 中用其它方式實現。你需要將參數值保存到一個局部變量中:
func gcd(a: Int, b: Int) -> Int { if (a == b) { return a } var c = a var d = b repeat { if (c > d) { c = c - d } else { d = d - c } } while (c != d) return c }
如果你想了解這項改進的最初提議,請閱讀原始提議。
函數的參數列表,底層上實際是以元組(tuple)表示的,因此你可以以元組的方式調用函數,就好像調用一個和函數原型相同的元組。以 gcd() 函數為例,你可以這樣調用它:
gcd(8, b: 12)
也可以這樣調用它:
let number = (8, b: 12) gcd(number)
如你所見,在 Swift 2 中第一個參數的 Label(又叫外部參數名)不是必須的。但是,第二個參數的參數名(以及剩餘的其它參數名)則是必須的。
這種語法也會讓新手茫然,因此這次對參數名的使用進行了規範化。在 Swift 3 中,你必須這樣調用這個函數:
gcd(a: 8, b: 12)
必須顯式地指定第一個參數名。否則,Xcode 8 會提示錯誤。
對此你的第一反應可能是“上帝!這得給我的程式碼帶來多大的修改量!”。你說對了,這個修改量還真不小。因此蘋果提供了一個調用函數時忽略第一個參數名的方法。你可以在第一個參數前面加一個下劃線:
func gcd(_ a: Int, b: Int) -> Int { ... }
這樣,你可以不用修改原來的函數調用方式 ── 即不用書寫第一個參數名。這會簡少將程式碼從 Swift 2 升級到 Swift 3 的工作量。
[ecko_alert color=”gray”]編後語: 要了解這項改進的最初提議,請閱讀這個提議。[/ecko_alert]
讓我們創建一顆按鈕,讓它被觸摸時執行某些動作 ── 假設只能用 playground 而不能用 Interface Builder 實現的話,這段程式碼應當編寫成:
// 1 import UIKit import XCPlayground // 2 class Responder: NSObject { func tap() { print("Button pressed") } } let responder = Responder() // 3 let button = UIButton(type: .System) button.setTitle("Button", forState: .Normal) button.addTarget(responder, action: "tap", forControlEvents: .TouchUpInside) button.sizeToFit() button.center = CGPoint(x: 50, y: 25) // 4 let frame = CGRect(x: 0, y: 0, width: 100, height: 50) let view = UIView(frame: frame) view.addSubview(button) XCPlaygroundPage.currentPage.liveView = view
程式碼有點多,讓我們將它分成幾步來進行說明:
導入 UIKit 和 XCPlayground 框架 ── 這樣我們才能創建按鈕并將它顯示到 playground 的助手編輯器中。
注意: 通過 View -> Assistant Editor -> Show Assistant Editor 菜單,你可以打開助手編輯器并與按鈕進行交互。
定義一個 tap 方法,這個方法會在用戶觸摸按鈕時觸發,并將一個 responder 對象傳遞給按鈕的 target ── responder 必須繼承自 NSObject,因為 selector 對象只對 Objective-C 方法有效。
創建一個按鈕并設置其屬性。
創建一個視圖,初始化時指定其 frame,然後將按鈕加到 view 上,然後在 playground 的助手編輯器上顯示。
注意突出顯示的程式碼。按鈕的 selector 用一個字符串指定。如果你將這個寫錯了,程式碼仍然能夠編譯,但在運行時會因為找不到相應的方法而崩潰。
為了在編譯時解決這種潛伏的問題,Swift 3 使用 #selector()
關鍵字來表示 selector。這樣,如果你寫錯了方法名,編譯器就會提前檢查出問題所在。
button.addTarget(responder, action: #selector(Responder.tap), for: .touchUpInside)
[ecko_alert color=”gray”]編後語: 要了解這項改進的最初提議,請閱讀 Doug Gregor 的提議。[/ecko_alert]
上面就是 Swift 中已刪除的語法特性。現在讓我們看一下讓 Swift 更加具有“先進性”的改進有哪些。
這個特性跟前一個有點類似,但它被用於 kvc(鍵值編碼)和 kvo(鍵值觀察)。
class Person: NSObject { var name: String = "" init(name: String) { self.name = name } } let me = Person(name: "Cosmin") me.valueForKeyPath("name")
你創建了一個 Person 類,它是鍵值編碼兼容的,在指定的初始化方法中傳入了我的名字,然後通過 key-path 來讀取我的名字。同樣,如果你把 key-path 弄錯了,app 會崩潰,我會很生氣!:(
幸運的是,Swift 3 解決了這個問題。key-path 字符串現在需要用 #keyPath()
表達式來替換:
class Person: NSObject { var name: String = "" init(name: String) { self.name = name } } let me = Person(name: "Cosmin") me.value(forKeyPath: #keyPath(Person.name))
[ecko_alert color=”gray”]編後語: 關於這個改進的最初提議,你可以查看 David Hart 的提議。[/ecko_alert]
NS 前綴終於從 Foundation 類型名中移除了 ── 如果你想知道這項改進的最初提議,請看這裡。典型的例子如 JSON 解析:
let file = NSBundle.mainBundle().pathForResource("tutorials", ofType: "json") let url = NSURL(fileURLWithPath: file!) let data = NSData(contentsOfURL: url) let json = try! NSJSONSerialization.JSONObjectWithData(data!, options: []) print(json)
在將 JSON 數據從文件中讀出的過程中,我們使用了幾個 Foundation 類:
NSBundle -> NSURL -> NSData -> NSJSONSerialization
在 Swift 3 中,NS 前綴被移除了,因此上面的解析過程就變成了:
Bundle -> URL -> Data -> JSONSerialization:
let file = Bundle.main().pathForResource("tutorials", ofType: "json") let url = URL(fileURLWithPath: file!) let data = try! Data(contentsOf: url) let json = try! JSONSerialization.jsonObject(with: data) print(json)
[ecko_alert color=”gray”]編後語: 要想知道這項改進的最初提議,請查看 Tony Parker 和 Philippe Hausler 的 這個提議 [/ecko_alert]
讓我們用圓半徑算出圓的周長和面積:
let r = 3.0 let circumference = 2 * M_PI * r let area = M_PI * r * r
在老的 Swift 版本中,M_PI 代表了圓周率。Swift 3 中,則將 pi 放到了 Float, Double 和 CGFloat 類型的內部:
Float.pi Double.pi CGFloat.pi
因此前面的(計算圓周長和面積)的程式碼,用 Swift 3 則可以編寫為:
let r = 3.0 let circumference = 2 * Double.pi * r let area = Double.pi * r * r
由於類型推斷的存在,我們甚至可以忽略類型名,因此代碼可以簡化為:
let r = 3.0 let circumference = 2 * .pi * r let area = .pi * r * r
GCD(Grand Central Dispath) 通常被用於執行網絡操作而不阻塞主線程中的用戶界面。它是用 C 編寫的,因此它的 API 對於初學者來說難於理解,尤其是想在異步隊列中做某些工作的時候:
let queue = dispatch_queue_create("Swift 2.2", nil) dispatch_async(queue) { print("Swift 2.2 queue") }
Swift 3 刪除了這些公式化和繁瑣的語法,改用面向對象的方法來實現:
let queue = DispatchQueue(label: "Swift 3") queue.async { print("Swift 3 queue") }
[ecko_alert color=”gray”]編後語: 要了解這項改進的最初提議,請閱讀 Matt Wright 的這個提議 [/ecko_alert]
Core Graphics 是一個強大的圖形框架,但使用了跟 GCD 一樣 C 風格的 API:
let frame = CGRect(x: 0, y: 0, width: 100, height: 50) class View: UIView { override func drawRect(rect: CGRect) { let context = UIGraphicsGetCurrentContext() let blue = UIColor.blueColor().CGColor CGContextSetFillColorWithColor(context, blue) let red = UIColor.redColor().CGColor CGContextSetStrokeColorWithColor(context, red) CGContextSetLineWidth(context, 10) CGContextAddRect(context, frame) CGContextDrawPath(context, .FillStroke) } } let aView = View(frame: frame)
你需要先指定 view 的 frame,繼承 UIView 類,重寫 drawRect 方法以進行定制的繪圖操作,使 view 呈現不一樣的畫面。
Swift 3 中,使用了一種全新的方法來使用 Core Graphic ── 先獲取前繪圖上下文,將它進行解包,解包到一個 context 對象,然後再通過這個 context 來執行所有的繪圖操作:
let frame = CGRect(x: 0, y: 0, width: 100, height: 50) class View: UIView { override func draw(_ rect: CGRect) { guard let context = UIGraphicsGetCurrentContext() else { return } let blue = UIColor.blue().cgColor context.setFillColor(blue) let red = UIColor.red().cgColor context.setStrokeColor(red) context.setLineWidth(10) context.addRect(frame) context.drawPath(using: .fillStroke) } } let aView = View(frame: frame)
注意: 在 view 調用它的 drawRect 方法之前,圖形上下文為空,因此你需要對其用 guard 語句進行解包操作 ── 更多內容請閱讀這裡。
接下來該講一點語法了!:) Swift 3 將方法分成兩類:有返回值的 ── 使用名詞命名,以及執行某種操作的 ── 使用動詞命名。
下面打印從 10 到 1 的數字:
for i in (1...10).reverse() { print(i) }
reverse() 方法用於將區間反序。在 Swift 3 看來,這個方法應該用名詞,因為它將原來的區間反向排序后返回。因此,它在這個方法後面加了一個 ed 後綴:
for i in (1...10).reversed() { print(i) }
元組最常用的用法之一,就是打印一個數組的內容:
var array = [1, 5, 3, 2, 4] for (index, value) in array.enumerate() { print("\(index + 1) \(value)") }
在 Swift 3 中,這個方法應該用名詞,因為它也返回了一個由當前數組元素的索引和值共同組成的元組。因此它在這個方法後面也加了一個 ed 後綴:
var array = [1, 5, 3, 2, 4] for (index, value) in array.enumerated() { print("\(index + 1) \(value)") }
另一個例子是數組的排序。下面我們將一個數組按照升序進行排序:
var array = [1, 5, 3, 2, 4] let sortedArray = array.sort() print(sortedArray)
在 Swift 3 中,這個方法應該使用名詞,因為它返回了經過排序的數組。因此 sort 方法現在被命名為 sorted 方法:
var array = [1, 5, 3, 2, 4] let sortedArray = array.sorted() print(sortedArray)
讓我們直接在數組上進行操作,去掉中間變量。在 Swift 2 中,你會用這樣的程式碼:
var array = [1, 5, 3, 2, 4] array.sortInPlace() print(array)
你通過 sortInPlace() 方法對一個可變數組排序。Swift 3 中,這個方法名應該用動詞,因為它直接進行實際上的排序操作,而不返回任何東西。只需要用最簡單的單詞就可以描述這種動作。因此 sortInPlace() 應該被 sort() 代替:
var array = [1, 5, 3, 2, 4] array.sort() print(array)
[ecko_alert color=”gray”]編後語: 關於這項命名規範的最初提議,請閱讀 API 設計指南。[/ecko_alert]
Swift 3 使用了一個簡單的哲學來規範其 API ── 刪除無用的單詞,如果某個單詞是多餘的或者是能夠通過上下文推斷出來的,則刪除它:
XCPlaygroundPage.currentPage
改為 PlaygroundPage.current
button.setTitle(forState)
改為 button.setTitle(for)
button.addTarget(action, forControlEvents)
改為 button.addTarget(action, for)
NSBundle.mainBundle()
改為 Bundle.main()
NSData(contentsOfURL)
改為 URL(contentsOf)
NSJSONSerialization.JSONObjectWithData()
改為 JSONSerialization.jsonObject(with)
UIColor.blueColor()
改為 UIColor.blue()
UIColor.redColor()
改為 UIColor.red()
Swift 3 將枚舉值看成屬性,因此使用“小駝峰法”而不是“大駝峰法”進行命名:
.System
改為 .system
.TouchUpInside
改為 .touchUpInside
.FillStroke
改為 .fillStroke
.CGColor
改為 .cgColor
在 Swift 3 中,如果你不使用方法或函數的返回值,Xcode 會顯示警告,例如:
在上面的程式碼中,printMessage 方法返回一個字符串。但是,這個返回值在我們調用方法的時候沒有用到,這可能引發潛在的問題,因此 Swift 3 編譯器會發出警告。
但是在某些情況下,我們實在是沒有必要保留返回值,因此你可以用一個 @discardableResult 表明方法不需要這種警告:
override func viewDidLoad() { super.viewDidLoad() printMessage(message: "Hello Swift 3!") } @discardableResult func printMessage(message: String) -> String { let outputMessage = "Output : \(message)" print(outputMessage) return outputMessage }
關於 Swift 3 就介紹到這裡。新的 Swift 版本的發佈,讓這門語言越來越好用啦。在它包含了大量的功能改進的同時,也會對你現在的代碼產生非常大的影響。我希望這篇教程能讓你更好地理解這些改進,同時節省你升級 Swift 專案的時間成本。
本教程中的所有程式碼都可以在 這裡 下載。我已經在 Xcode 8 beta 中通過了測試。請確保你在 Xcode 8 中運行這個專案。
如果你有任何問題和建議,請告訴我。祝你 coding 快樂! :)
楊宏焱,男,中國大陸籍人士,CSDN 博客專家(個人博客 http://blog.csdn.net/kmyhy)。2009 年開始學習蘋果 iOS 開發,精通 O-C/Swift 和 Cocoa Touch 框架,開發有多個商店應用和企業 App。熱愛寫作,著有和翻譯有多本技術專著,包括:《企業級 iOS 應用實戰》、《iPhone & iPad 企業移動應用開發秘笈》、《iOS8 Swift 編程指南》,《寫給大忙人看的 Swift》(合作翻譯)、《iOS Swift game Development cookbook》等。