Swift 3 新特性

做者:COSMIN PUPĂZĂ,原文連接,原文日期:2016/06/29
譯者:saitjr;校對:Cee;定稿:CMBhtml

Apple 在 WWDC 上已將 Swift 3 整合進了 Xcode 8 beta 中,並會在今年晚些時候發佈 Swift 3 的正式版。這是 Swift 在開源和支持 Mac OS X 與 Linux 以後的首個版本。若是你在去年 11 月關注了 Swift 進化史 和已經啓動的 IBM 沙盒 項目,那你應該知道 Swift 確實改動不少。甚至能夠肯定你在 Xcode 8 上根本沒法編譯既有項目。ios

Swift 3 的改動歸結下來主要有兩點:git

  • 移除了在 Swift 2.2 就已經棄用的特性github

  • 語言現代化問題算法

讓咱們從移除特性講起,畢竟這點能容易理解,並且在 Xcode 7.3 的時候咱們遇到了相關警告。編程

++-- 操做符

自增自減是來源於 C 的操做符,做用是對變量直接進行 +1-1 的操做:json

var i = 0
i++
++i
i--
--i

然而,在咱們要選擇使用哪種操做符進行運算的時候,事情就變得複雜起來。不管是自增仍是自減,都對應着兩種寫法:寫在在變量以前,仍是在變量以後。它們的底層實現其實都是有返回值的函數,是否使用返回值取決於對運算符的重載。swift

這可能會嚇跑初學者,因此蘋果移除了該特性——取而代之的是複合加法運算(+=)與減法運算(-=):api

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

相關文章
相關標籤/搜索