原文: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 2.2 中聲明的“已拋棄”特性
- 讓語言更加“先進”
讓我們先從简单的开始,即 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 語言風格的 for 循環成為歷史
對於自增、自減運算用得最多的地方就是 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]
函數參數中的 var 被移除
函數參數一般是當做常量來用的,因為在方法中你通常不會修改它們的值。但是某些情況下,你需要將它們聲明為變量。在 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]
不能用字符串表示 Selector
讓我們創建一顆按鈕,讓它被觸摸時執行某些動作 ── 假設只能用 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 更加具有“先進性”的改進有哪些。
Key-paths 不再使用字符串
這個特性跟前一個有點類似,但它被用於 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]
Foundation 類型名不再需要 NS 前綴
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]
M_PI 和 .pi
讓我們用圓半徑算出圓的周長和面積:
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
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 變得更加“Swift 化”
Core Graphics 是一個強大的圖形框架,但使用了跟 GCD 一樣 C 風格的 API:
let frame = CGRect(x: 0,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,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,value) in array.enumerated() {
print("\(index + 1) \(value)")
}
另一個例子是數組的排序。下面我們將一個數組按照升序進行排序:
var array = [1,4]
let sortedArray = array.sort()
print(sortedArray)
在 Swift 3 中,這個方法應該使用名詞,因為它返回了經過排序的數組。因此 sort 方法現在被命名為 sorted 方法:
var array = [1,4]
let sortedArray = array.sorted()
print(sortedArray)
讓我們直接在數組上進行操作,去掉中間變量。在 Swift 2 中,你會用這樣的程式碼:
var array = [1,4]
array.sortInPlace()
print(array)
你通過 sortInPlace() 方法對一個可變數組排序。Swift 3 中,這個方法名應該用動詞,因為它直接進行實際上的排序操作,而不返回任何東西。只需要用最簡單的單詞就可以描述這種動作。因此 sortInPlace() 應該被 sort() 代替:
var array = [1,4]
array.sort()
print(array)
[ecko_alert color=”gray”]編後語: 關於這項命名規範的最初提議,請閱讀 API 設計指南。[/ecko_alert]
API 更加“swift 化”
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
@discardableResult
在 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》等。