作者:COSMIN PUP?Z?,原文鏈接,原文日期:2016/06/29
譯者:saitjr;校對:Cee;定稿:CMB
Apple 在 WWDC 上已將 Swift 3 整合進了 Xcode 8 beta 中,并會在今年晚些時候發布 Swift 3 的正式版。這是 Swift 在開源和支持 Mac OS X 與 Linux 之后的首個版本。如果你在去年 11 月關注了 Swift 進化史 和已經啟動的 IBM 沙盒 項目,那你應該知道 Swift 確實改動很多。甚至可以確定你在 Xcode 8 上根本無法編譯既有項目。
Swift 3 的改動歸結下來主要有兩點:
- 移除了在 Swift 2.2 就已經棄用的特性
- 語言現代化問題
讓我們從移除特性講起,畢竟這點能容易理解,而且在 Xcode 7.3 的時候我們遇到了相關警告。
++
與 --
操作符
自增自減是來源于 C 的操作符,作用是對變量直接進行 +1
或 -1
的操作:
var i = 0
i++
++i
i--
--i
然而,在我們要選擇使用哪一種操作符進行運算的時候,事情就變得復雜起來。無論是自增還是自減,都對應著兩種寫法:寫在在變量之前,還是在變量之后。它們的底層實現其實都是有返回值的函數,是否使用返回值取決于對運算符的重載。
這可能會嚇跑初學者,所以蘋果移除了該特性——取而代之的是復合加法運算(+=
)與減法運算(-=
):
var i = 0
i += 1
i -= 1
當然,你也可以使用普通的加法運算(+
)與減法運算(-
),雖然復合式運算符寫起來要短一點:
i = i + 1
i = i - 1
延伸閱讀:如果你想要了解更多該變更背后的故事,請閱讀 Chris Lattner 對移除
++
與--
的看法。
C 風格的 for 循環已成歷史
其實自增自減運算符用得最多的地方,還是在 for 循環部分。移除該運算符意味著 for 循環的特性也隨之遠去了,因為在 for-in 的世界中,循環控制語句與范圍限制用不上該操作符。
如果你有一定編程背景,那么輸出 1 到 100 的數,你可能會這樣寫:
for (i = 1; i <= 10; i++) {
print(i)
}
在 Swift 3 中,已經不允許這種寫法了,而應該寫為(注意閉區間范圍的寫法):
for i in 1...10 {
print(i)
}
或者,你也可以使用 for-each 加閉包的寫法(更多循環相關信息請看這):
(1...10).forEach {
print($0)
}
延伸閱讀:如果你想要了解更多該變更背后的故事,請閱讀 Erica Sadun 對移除 C 風格循環的看法。
移除函數參數的 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
標記為變量,才能在函數體里對兩個數進行修改。
Swift 3 不在允許開發者這樣來將參數標記為變量了,因為開發者可能會在 var
和 inout
糾結不已。所以最新的 Swift 版本中,就干脆移除了函數參數標記 var
的特性。
如此,想要用 Swift 3 來寫上面的 gcd
函數,就要另辟蹊徑了。你需要在函數內部創建臨時變量來存儲參數:
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
}
延伸閱讀:如果你想要了解更多該變更背后的故事,請閱讀決定移除
var
的想法。
函數參數標簽的一致性
函數的參數列表底層實現其實是元組,所以只要元組結構和函數參數列表相同,你可以直接用元組來代替參數列表。就拿剛才的 gcd()
函數來說,你可以這樣調用:
gcd(8, b: 12)
你也可以這樣調用:
let number = (8, b: 12)
gcd(number)
正如你所看到的,在 Swift 2 中,第一個參數無需帶標簽,而從第二個參數開始,就必須要帶標簽了。
這個語法對初學者來說可能會造成困惑,所以,要進行統一標簽設計。在 Swift 3 中,函數的調用要像下面這樣:
gcd(a: 8, b: 12)
即使是第一個參數,也必須帶上標簽。如果不帶,Xcode 8 會直接報錯。
你對這修改的第一個反應可能是:「我嗶!那我代碼改動得多大??!」是的,這簡直是成噸的傷害。所以蘋果又給出了一種不用給第一個參數帶標簽的解決方案。在第一個參數前面加上一個下劃線:
func gcd(_ a: Int, b: Int) -> Int {
...
}
但是這樣做,事情又仿佛回到了原點——第一個參數不用帶標簽了。使用這種方式,應該能一定程度上降低 Swift 2 遷移到 Swift 3 上的痛苦。
延伸閱讀:如果你想要了解更多該變更背后的故事,請閱讀函數標簽一致性的一些想法。
Selector 不再允許使用 String
讓我們來創建一個按鈕,并給它添加一個點擊事件(不需要界面支持,直接使用 playground 就行):
// 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 的 assistant editor 中進行顯示。注意:你需要在 Xcode 菜單欄上的 View -> Assistant Editor -> Show Assistant Editor 來開啟 assistant editor。
創建點擊的觸發事件,能在用戶點擊按鈕時,觸發綁定的事件——這需要基類為
NSObject
,因為 selector 僅對 Objective-C 的方法有效。聲明按鈕,并配置相關屬性。
聲明視圖,給定合適的大小,將按鈕添加到視圖上,最后顯示在 playground 的 assistant editor 中。
讓我們來看下給按鈕添加事件的代碼:
button.addTarget(responder, action: "tap", forControlEvents: .TouchUpInside)
這里按鈕的 selector 還是寫的字符串。如果字符串拼寫錯了,那程序會在運行時因找不到相關方法而崩潰。
為了解決編譯期間的潛在問題,Swift 3 將字符串 selector 的寫法改為了 #selecor()
。這將允許編譯器提前檢查方法名的拼寫問題,而不用等到運行時。
button.addTarget(responder, action: #selector(Responder.tap), for: .touchUpInside)
延伸閱讀:如果你想要了解更多該變更背后的故事,請閱讀 Doug Gregor 的觀點。
以上就是關于移除特性的全部內容。接下來,讓我們來看看語言現代化的一些亮點。
不再是 String 的 key-path 寫法
這個特性和上一個很相似,但是這是用在鍵值編碼(KVC)與鍵值觀察(KVO)上的:
class Person: NSObject {
var name: String = ""
init(name: String) {
self.name = name
}
}
let me = Person(name: "Cosmin")
me.valueForKeyPath("name")
首先創建了 Person
類,這是 KVC 的首要條件。然后用指定的構造器初始化一個 me
,最后通過 KVC 來修改 name
。同樣,如果 KVC 中的鍵拼寫錯誤,這一切就白瞎了 ??。
幸運的是,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))
延伸閱讀:如果你想要了解更多該變更背后的故事,請閱讀 David Hart 的觀點。
Foundation 去掉 NS
前綴
我們先來看看有 NS
前綴時的寫法,下面是一個典型的 JSON 解析例子(如果對 NS
前綴的前世今生感興趣,請移步):
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)
以上代碼使用了 Foundation 相關類來對文件中的 JSON 數據進行解析: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)
延伸閱讀:關于命名約定的變化,你可以查看 Tony Parker 與 Philippe Hausler 的觀點。
M_PI
還是 .pi
下面是一個已知半徑求圓周長的例子:
let r = 3.0
let circumference = 2 * M_PI * r
let area = M_PI * r * r
在舊版本的 Swift 中,我們使用 M_PI
常量來表示 π。而在 Swift 3 中,π 整合為了 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 Dispatch(GCD)多用于解決網絡請求時,阻塞主線程的 UI 刷新問題。這是用 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")
}
延伸閱讀:更多相關信息,請查看 Matt Wright 的觀點。
更 Swift 范的 Core Graphics
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
的 View
類,重寫 drawRect()
方法來重繪 view 的內容。
在 Swift 3 中,有不同的實現方式——對當前畫布上下文解包,之后的所有繪制操作就都基于解包對象了:
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()
方法之前,上下文均為 nil
,所以使用 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 中,同樣對相關的 enumerate()
方法名做出了名詞性的修改——同樣加上了 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)
延伸閱讀:更多關于命名約定的信息,請查看 API 設計手冊。
更 Swift 范的 API
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 項目版本遷移方面能幫到你。
文章的所有代碼我都放在了這個 Playground 中,我已經在 Xcode 8 beta 版本中進行了測試。所以,請確保使用 Xcode 8 來進行編譯。
有任何問題,歡迎告知。Happy coding!??
本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 http://swift.gg。