Swift 3 新特性

作者: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
}

這個算法的邏輯很簡單:如果兩個數相等,則返回其中一個的值。否則,做大小比較,大的數減去小的數之后,將差值賦值給大的數,然后再將兩個數作比較,為止它們相等為止,最終返回其中一個的值。正如你所看到的,通過將 ab 標記為變量,才能在函數體里對兩個數進行修改。

Swift 3 不在允許開發者這樣來將參數標記為變量了,因為開發者可能會在 varinout 糾結不已。所以最新的 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

讓我們一步一步分析下上面的代碼:

  1. 導入 UIKitXCPlayground 框架——需要創建一個按鈕,并在 playground 的 assistant editor 中進行顯示。

    注意:你需要在 Xcode 菜單欄上的 View -> Assistant Editor -> Show Assistant Editor 來開啟 assistant editor。

  2. 創建點擊的觸發事件,能在用戶點擊按鈕時,觸發綁定的事件——這需要基類為 NSObject,因為 selector 僅對 Objective-C 的方法有效。

  3. 聲明按鈕,并配置相關屬性。

  4. 聲明視圖,給定合適的大小,將按鈕添加到視圖上,最后顯示在 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,然后創建一個繼承自 UIViewView 類,重寫 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。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容