Swift 4 新功能詳盡介紹:Codable, Dictionaries優化, 多行字符串等等


在幾週前的WWDC大會中,Apple公佈Swift 4,它伴隨新版Xcode 9一同現身,它的最終發布日將會是在九月,這個夏季期間仍是beta版本,這是該語言首次可以向下兼容的版本,它為現有的Swift 3功能提供很棒的優化,其中,很多都是開發者非常期待的,讀者將在本文中了解所有內容,所以廢話不多說,讓我們開始進入重點吧!:)

在本文中,我們假設你已經有Swift 3的一些基本知識。如果讀者需要快速了解的Swift 3和Swift 3.1最大亮點和更新重點,請查看我整理出Swift 3Swift 3.1新特性的介紹文章。

我將使用Playground帶領讀者一起暸解本次程式碼的變更,如果你想充分了解Swift 4的新功能/更改,建議打開Xcode 9 beta版並創建一個新的Playground文件。 本文中的一些示例需要使用Foundation框架才能正常作業,因此,如果你想在Playground中操作本文介紹的所有內容,請先import這個框架之後再開始:

import Foundation

JSON的編碼(Encoding)和解碼(Decoding)

讓我開始介紹一下Swift新版本一個很酷的功能。Swift 4簡化了Swift 3中使用的整個JSON壓縮和序列化過程。現在你只需要使自定義類型實現Codable協定 – 它會將EncodableDecodable兩者結合 – 這樣會讓你的工作更便利:

class Tutorial: Codable {
  let title: String
  let author: String
  let editor: String
  let type: String
  let publishDate: Date
  
  init(title: String, author: String, editor: String, type: String, publishDate: Date) {
    self.title = title
    self.author = author
    self.editor = editor
    self.type = type
    self.publishDate = publishDate
  }
}

let tutorial = Tutorial(title: "What's New in Swift 4?", author: "Cosmin Pupăză", editor: "Simon Ng", type: "Swift", publishDate: Date())

你的Tutorial類別遵從了Codable協定,所以讓我們繼續編碼你的tutorial物件:

let encoder = JSONEncoder()
let data = try encoder.encode(tutorial)
let string = String(data: data, encoding: .utf8)

首先,你使用JSONEncoder類別的指定建構器(designated initializer)創建一個code>encoder物件。然後,使用try語句和encoder的encode(_:)方法將tutorial壓縮到data物件中。最後,使用UTF-8編碼將數據轉換為字符串,如果讀者使用Playground跟隨內文一起操作,將看到JSON輸出:

swift-json

現在我們來看看如何拿回初始的tutorial物件:

let decoder = JSONDecoder()
let article = try decoder.decode(Tutorial.self, from: data)
let info = "\(article.title) \(article.author) \(article.editor) \(article.type) \(article.publishDate)"

首先,你使用JSONDecoder類別初始化程序宣告decoder物件。然後,使用像之前那個解碼器的decode(_: from:) 方法,對tryblock內的編碼數據進行解碼。最後且重要的一點是,可以透過字串代換(String interpolation)來使用article物件的屬性,生成自定義字符串,現在解碼JSON數據非常簡單。

swift-json-decode

注意:讀者可以在下列兩個連結閱讀關於此更新的更多訊息,連結一連結二

更智能的Key Paths

Swift 4可以更容易使用key paths訪問物件的屬性,參考下面的類別實作:

class Author {
  let name: String
  let tutorial: Tutorial
  
  init(name: String, tutorial: Tutorial) {
    self.name = name
    self.tutorial = tutorial
  }
}
let author = Author(name: "Cosmin Pupăză", tutorial: tutorial)

你可以使用之前創建的Tutorial類別來定義author物件的某個教程(tutorial),現在來看如何使用key paths獲取作者的名字:

let authorNameKeyPath = \Author.name
let authorName = author[keyPath: authorNameKeyPath]

首先,透過(反斜線符號)創建一個Key Path,然後使用keyPath下標(subscript)獲取author物件的名稱。

除此之外 – 你可以使用Key Paths穿越類別階層,決定tutorial物件的標題,如下所示:

let authorTutorialTitleKeyPath = \Author.tutorial.title
let authorTutorialTitle = author[keyPath: authorTutorialTitleKeyPath]

開發者甚至可以使用key path的appending(path:)方法向已定義的Key Path添加新的Key Path,如下所示:

let authorTutorialKeyPath = \Author.tutorial
let authorTutorialNameKeyPath = authorTutorialKeyPath.appending(path: \.title)
let authorTutorialName = author[keyPath: authorTutorialNameKeyPath]

最後來看看最精彩的部分 – 使用Key Paths來更改屬性值:

class JukeBox {
  var song: String
  
  init(song: String) {
    self.song = song
  }
}

let jukeBox = JukeBox(song: "Nothing else matters")
let jukeBoxSongKeyPath = \JukeBox.song
jukeBox[keyPath: jukeBoxSongKeyPath] = "Stairway to heaven”

首先,你定義JukeBox類別,並創建一個可以播放新歌曲的jukeBox物件 然後,你可以為song屬性定義Key Path,並使用它來更改歌曲。

注意:你可以在這裡閱讀更多相關資訊。

Classes與Protocols的協作

在創建常量和變量時,你可以在Swift 3中組合使用協定(protocols)。Swift 4則能夠更進一步,讓開發者使用相同的語法將類別添加到其中,你可以將一個物件限制在一個類別和一個協定中,就像在Objective-C中一樣。

想像一下,你正在編寫一個繪圖和著色的應用程序,使用的主要類別和協定原型如下所示:

protocol Drawable {}
protocol Colourable {}

class Shape {}
class Line {}

你可以為形狀(shapes)和線(lines)創建兩個基類(base classes),以及兩個處理繪圖和著色對應邏輯的協定,現在假設你要為你的應用定義一些特定類型:

class Circle: Shape, Drawable, Colourable {}
class Rectangle: Shape, Drawable, Colourable {}
class Square: Shape, Drawable, Colourable {}

class StraightLine: Line, Drawable {}
class DottedLine: Line, Drawable {}

let circle: Circle
let rectangle: Rectangle
let square: Square

let straightLine: StraightLine
let dottedLine: DottedLine

首先,創建三個實體類別(concrete classes):CircleRectangleSquare,它繼承自Shape基類並遵循DrawableColourable協定。然後你宣告兩個derived classes(衍生類別):StraightLineDottedLine,它們繼承自Line類別,並實現DrawableColourable協定。最後,為之前創建的每個類別定義物件對象。

這種方法的問題是,為應用程序中的每個新類型設置太多的子類,使類別層次結構複雜化,開發者可以透過將兩個協定結合在一起來進行簡化:

let newCircle: Drawable & Colourable
let newRectangle: Drawable & Colourable
let newSquare: Drawable & Colourable

你可以使用&操作符,使你的物件遵守DrawableColourable協定,並可以進一步透過為協定創建類型別名(type alias)來簡化語法:

typealias DrawableColourable = Drawable & Colourable

let anotherCircle: DrawableColourable
let anotherRectangle: DrawableColourable
let anotherSquare: DrawableColourable

你的物件符合code>DrawableColourable複合協定,但這還不夠,因為初始問題仍然未解決。因此,Swift 4提供一個解方 – 將基礎類別(base classes)添加到組合中,如下所示:

let brandNewCircle: Shape & Drawable & Colourable
let brandNewRectangle: Shape & Drawable & Colourable

let brandNewSquare: Shape & Drawable & Colourable
let brandNewStraightLine: Line & Drawable
let brandNewDottedLine: Line & Drawable

你可以創建ShapeLine的物件,並使用&運算符去實現它們相應的DrawableColourable協定,你的類別層次結構現在很簡單,而且為了簡化語法,你也可以為它創建一個類別別名,如下所示:

typealias DrawableColourableShape = Shape & Drawable & Colourable
typealias DrawableLine = Line & Drawable

let anotherNewCircle: DrawableColourableShape
let anotherNewRectangle: DrawableColourableShape

let anotherNewSquare: DrawableColourableShape
let anotherNewStraightLine: DrawableLine
let anotherNewDottedLine: DrawableLine

你自定義DrawableColourableShapeDrawableLine類型別名,並為其宣告相對應的物件,你的繪圖和著色應用程式設計看起來很棒 – 做得好!:)

進一步閱讀:你可以在這裡閱讀更多相關資訊。

One Sided Ranges

在Swift 4推出了One Sided Ranges(可以簡化範圍運算符為單向),並為half open range和half closed range運算符添加前綴(prefix)和後綴(postfix)版本。以下是Swift 3中如何獲取陣列的前半部分:

let array = [1, 5, 2, 8, 4, 10]
let halfIndex = (array.count - 1) / 2
let openFirstHalf = array[0..<halfIndex]
let closedFirstHalf = array[0...halfIndex]

你可以使用open range和closed range運算符來獲取你所指定的陣列前半部分,因為是從陣列(array)的第一個元素開始,兩個範圍都以index 0開頭,所以在Swift 4中可以將其省略,如下所示:

let openFirstSlice = array[..<halfIndex]
let closedFirstSlice = array[...halfIndex]

你可以刪除範圍的起始index,因為你會從頭開始依序執行陣列中的元素直到對應的halfIndex為止。現在我們來看看如何在Swift 3中返回陣列的後半部分:

let nextIndex = halfIndex + 1
let lastIndex = array.count - 1
let openSecondHalf = array[nextIndex..<lastIndex + 1]
let closedSecondHalf = array[nextIndex...lastIndex]

這裡沒有什麼新的東西 – 你使用half open range和half closed range操作符,從nextIndex依序執行到陣列的最後一個元素,就像在之前的情況一樣,在Swift 4使用closed ranges不需要列出陣列的最後一個元素的index:

let closedSecondSlice = array[nextIndex...]

上面程式碼從nextIndex對應的陣列元素計算到陣列的最後一個元素,因此,這邊可以刪除計算範圍的終點index。

One sided ranges運算符讓你從任何定點開始創建出一個index無限延伸的序列,這是在Swift 3中顯示陣列的index和value的方法:

for (index, value) in array.enumerated() {
  print("\(index + 1): \(value)")
}

你可以使用for in迴圈和陣列的enumerated()方法為每個index及其相應的value打印一個tuple。陣列的第一個index都為0,因此你應該將其增加1,以1當作計算起點,但是在Swift 4不再需要這樣做了:

for (index, value) in zip(1..., array) {
  print("\(index): \(value)")
}

你可以在for in迴圈中使用zip(_: _:)函式,將One sided range的index與陣列的value組合在一起,現在第一個index為1。請注意,儘管index序列是無限的,但是當所有陣列的元素都被打印時,這個迴圈就會停止。這是避免在這種情況下創建無限循環的唯一方法:你應該在某種點去終止它,以使其不會像瘋狂一樣繼續下去。

注意:不能在迴圈內省略One sided range的第一個值,因為你將永遠不知道從哪個index開始進行迭代。

One sided range在switch語句中可以做到很好的搭配工作。只有一個注意事項 – 你必須添加一個default情況,使switch不會出現遺漏的情況,因為現在範圍是無限:

let favouriteNumber = 10
switch favouriteNumber {
  case ..<0:
    print("Your favourite number is a negative one.")
  case 0...:
    print("Your favourite number is a positive one.")
  default:
    break
}

你使用單邊無限範圍(infinite ranges)來將其與0進行比較,測試favouriteNumber是正數還是負數。..<00...模式模型相對應(-∞, 0)[0, +∞)區間。請注意,此處不會設置default情況,因此只需使用break語句。

注意:讀者可以在這裡閱讀有關於此變更的更多資訊。

swap vs swapAt

Swift 3中的swap(_:_:)mutating method可讓陣列的兩個元素進行交換:

var numbers = [1, 5, 2, 8, 4, 10]
swap(&numbers[0], &numbers[1])

這個方式有一個最顯著的缺點:交換的元素作為inout參數傳遞給函數,以便可以直接訪問它們。Swift 4採用完全不同的方法,用swapAt(_:_:)替換方法,它接受兩個元素對應的index,然後像之前那樣進行交換:

numbers.swapAt(0, 1)

swap(_:_:)函式也可以在Swift 3中交換兩個給定的值 – 將值作為inout參數傳遞,做法就像剛才那樣:

var a = 1
var b = 2

swap(&a, &b)

但是,在Swift 4中swap(_:_:)函式將被棄用並完全刪除,你可以通過兩種方式來替換它,第一種方法是使用另一個常量來執行實際的交換:

let aux = a
a = b
b = aux 

第二個解決方案利用Swift內置的tuple特性,只要一行程式碼即可實現以前的算法:

(b, a) = (a, b)
注意:讀者可以在此處閱讀更多相關資訊。

優化Dictionaries和Sets

Dictionaries和Sets是兩個很實用的數據結構,但在Swift 3中一直缺少一些重要特性,所以Swift 4改進了它們,我們先從Dictionaries開始,因為比起Sets的案例,在這裡有更多的變化。

首先,在Swift 4中如何從一個tuples的陣列創建一個dictionary:

let tupleArray = [("Monday", 30),  ("Tuesday", 25),  ("Wednesday", 27),  ("Thursday", 20),  ("Friday", 24),  ("Saturday", 22),  ("Sunday", 26)]
let dictionary = Dictionary(uniqueKeysWithValues: tupleArray)

使用Dictionaries的init(uniqueKeysWithValues:)初始化程序從tuple陣列創建一個全新的Dictionary。每個tuple表示一周某一天的平均溫度,因此Dictionary的key是星期幾,value是對應的溫度。

注意: 溫度以攝氏度(Celsius degrees)為單位,一週的第一天是星期一,你可以根據自身需求,選擇使用華氏度(Fahrenheit degrees),或是將星期天設為一週的的一天。

如果你已分別擁有用來建構Dictionary中key和value的陣列,可以透過下列方法完成構建作業:

let keys = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
let values = [30, 25, 27, 20, 24, 22, 26]
let newDictionary = Dictionary(uniqueKeysWithValues: zip(keys, values))

zip(_:_:)函式能從對應的陣列創建出剛才的tuple陣列。但是,如果你只有values的陣列,沒有keys的陣列呢?One sided range又再次提供一個救援方法:

let anotherDictionary = Dictionary(uniqueKeysWithValues: zip(1..., values))

創建一個以1開頭的無限序列,並將其與zip(_:_:)函式中Dictionary的值進行組合:key從1(代表星期一)開始計算到7(代表星期天) – good job!:)

但是如果出現重複的情況呢?Swift 4相當的聰明,它會提供參數讓你自己選擇處理Dictionary重複值的方式,如下所示:

let duplicatesArray = [("Monday", 30),  ("Tuesday", 25),  ("Wednesday", 27), ("Thursday", 20),  ("Friday",   24),  ("Saturday", 22),  ("Sunday", 26),  ("Monday", 28)]
let noDuplicatesDictionary = Dictionary(duplicatesArray, uniquingKeysWith: min) 

透過Dictionary的init(_: uniquingKeysWith:)初始化方法,創建一個僅有唯一key值的Dictionary。它使用特定的算法為Dictionary中重複的key選擇對應的值。在上述範例中,如果在一周的某一天有兩種不同的溫度,程式會選擇比較小的值。

但是如果要將一些重複項合併到現有的Dictionary怎麼辦?Swift 4也提供了解決方法:

let duplicateTuples = [("Monday", 28),  ("Tuesday", 27)]
var mutatingDictionary = Dictionary(uniqueKeysWithValues: tupleArray)
mutatingDictionary.merge(duplicateTuples, uniquingKeysWith: min)
let updatedDictionary = mutatingDictionary.merging(duplicateTuples, uniquingKeysWith: min)

添加元素到Dictionary有兩種風格,可以使用mutating的merge(_: uniquingKeysWith:)方法修改原始Dictionary,或者使用non-mutatingmerging(_: uniquingKeysWith:)函式從頭開始創建一個全新的Dictionary,過濾重複的算法與以前的情況一樣 – 你可以隨意嘗試其他動作,看看會發生什麼事。

在Swift 3當中Dictionary下標(subscripts)返回對應key的value是一個optional,因此你可以使用nil coalescing operator(空值聚合運算子)設置沒有對應key的預設值:

var seasons = ["Spring" : 20, "Summer" : 30, "Autumn" : 10]
let winterTemperature = seasons["Winter"] ?? 0

請求的季節(season)不在Dictionary中,因此你設置了預設的溫度。Swift 4替Dictionary中不存在key的對應值添加了全新的subscript,因此在這種情況下不再需要nil coalescing operator:

let winterTemperature = seasons["Winter", default: 0]

自定義subscript可以讓你輕鬆更新現有keys的值。若是在沒有它的情況下,你在Swift 3中執行應該會執行下面的動作:

if let autumnTemperature = seasons["Autumn"] {
  seasons["Autumn"] = autumnTemperature + 5
}

首先,必須使用if let語句拆解季節名稱對應的值,如果不是nil則更新它。現在,我們看看它是如何在Swift 4中使用預設值的subscript來完成:

seasons["Autumn", default: 0] += 5

你將新的subscript與加法賦值運算符相結合,以便在一行代碼中解決任務:)

Swift 4重新設計Dictionary的map(_:)filter(_:),因為上述函式在Swift 3會返回陣列而不是Dictionary。看下面例子,如何在Swift 3中透過map設定Dictionary的value:

let mappedArrayValues = seasons.map{$0.value * 2}

在這裡你使用縮略參數(shorthand arguments)將seasons這個Dictionary的溫度從攝氏度映射到華氏溫度。如果你也想映射key值,在Swift 3會這樣做:

let mappedArray = seasons.map{key, value in (key, value * 2)}

這種方法使用Dictionary內的元素創建了的一組tuples陣列,但它不能在Swift 4中運行,因為已經不再能夠對閉包參數進行tuple解構(更多關於文章另一部分),第一個解決方法是使用整個tuple作為map(_:)函數的參數:

let mappedArray = seasons.map{season in (season.key, season.value * 2)}

第二個解決方案使用縮略語法(shorthand syntax)代替:

let mappedArrayShorthandVersion = seasons.map{($0.0, $0.1 * 2)}

這兩個解決方案都可以使用,但是畢竟你仍堅持使用陣列而不是Dictionary,所以讓我們繼續解決這個問題:

let mappedDictionary = seasons.mapValues{$0 * 2}

你透過mapValues(_:)函式將原本的Dictionary透過map映射一個具有相同結構的全新Dictionary – 多酷?:)

若是使用Swift 3開發,你可以使用缩略語法過濾(filter)Dictionary的值,如下所示:

let filteredArray = seasons.filter{$0.value > 15}

只有溫度15度以上才能進入filteredArray。但是,如果我們想要過濾之後產生的是一個Dictionary呢?Swift 4再次救援:

let filteredDictionary = seasons.filter{$0.value > 15}

程式碼與以前完全相同,但是現在你有一個與原來結構相同的已過濾Dictionary,太好了!:)

注意:你可以使用MARKDOWN_HASH05f152eac01279a44873041576143a65MARKDOWN_HASH函式來決定映射(map)和過濾(filter)的陣列和字典的實際類型。如果想了解相關功能編程技術的更多資訊,請查看我的map教程filter教程

Swift 4讓開發者可以將陣列分解重組為一個按數值分組的Dictionary,如下圖:

let scores = [7, 20, 5, 30, 100, 40, 200]
let groupedDictionary = Dictionary(grouping: scores, by:{String($0).count})

Dictionary的init(grouping: by:)初始化方法可以通過特定的key將Dictionary的value進行分類組合。在這種情況下,你可以將scores陣列以數字的位數加以分類,如下所示:

[ 1: [7, 5], 2: [20, 30, 40], 3: [100, 200] ]

你還可以使用尾隨閉包(Trailing Closures)語法重寫該段程式碼,如下所示:

let groupedDictionaryTrailingClosure = Dictionary(grouping: scores) {String($0).count}

你可以從初始化函式中將字典裡表達key值的閉包(closure)提取出來,因為在這種情況下,閉包(closure)是初始化方法的最後一個參數 – 真的很酷!:)

Swift 3允許創建一個有預設初始容量的Dictionary,如下所示:

let ratings: Dictionary = Dictionary(minimumCapacity: 10)

透過Dictionary的init(minimumCapacity:)初始化方法,在ratings這個Dictionary中可儲存至少十個ratings,但無法檢查當前容量或預留更多儲存空間。在此,Swift 4再次幫我們省了不少事:

seasons.capacity
seasons.reserveCapacity(4)

你可以使用capacity屬性獲取Dictionary的當前儲存空間,並創建內存,以便使用reserveCapacity(_:)方法儲存更多元素。

以上就是關於Dictionary的全部介紹。事實上,Sets和Dictionaries都是屬於集合,所以讓我們看看Dictionary的變更適用於Sets的地方。

Sets是一種特別的陣列,因為它們不能存放相同的項目。儘管如此,當你在Swift 3中過濾一個Set時,將獲得一個陣列,而不是一個Set:

var categories: Set = ["Swift", "iOS", "macOS", "watchOS", "tvOS"]
let filteredCategories = categories.filter{$0.hasSuffix("OS")}

你過濾categories的Set以便只獲取與操作系統(OS)相關的Apple技術,Swift 4會返回一個過濾完成的Set:

let filteredCategories = categories.filter{$0.hasSuffix("OS")}

程式碼與Swift 3中的代碼相同,但是這次操作系統會儲存在一個Set中,而不是在陣列裡。

你可以在Swift 3中創建一個具有預設容量的Set,如下所示:

let movies: Set = Set(minimumCapacity: 10)

這個moviesSet至少儲存十部電影,Swift 4能夠宣告Set的容量並可為存放項目定義額外的空間,將內存容量提升到一個新的水平:

categories.capacity
categories.reserveCapacity(10)

你可以在Set中存更多categories並隨時讀取其容量。這就是Sets!:)

注意: 你可以在此閱讀更多相關變動資訊。

Private vs Fileprivate in Extensions

Swift 3會使用fileprivate訪問控制修飾符,某個類別的重要數據可以在該類別之外的任何地方使用,只要在相同的文件中存取即可,來看看在extension的情況下是如何使用的:

class Person {
  fileprivate let name: String
  fileprivate let age: Int
  
  init(name: String, age: Int) {
    self.name = name
    self.age = age
  }
}

extension Person {
  func info() -> String {
    return "\(self.name) \(self.age)"
  }
}

let me = Person(name: "Cosmin", age: 31)
me.info()

你可以為Person類別創建一個extension,並使用字串代換(string interpolation)來訪問其info()方法中的私有屬性,這是Swift中常見的模式,現在可以在Swift 4中使用private取代fileprivate,以便在extension中訪問類別屬性,就如同你在該類別本身所做:

class Person {
  private let name: String
  private let age: Int
  // original code
}
注意:你可以在此處閱讀更多相關資訊。

NSNumber優化

Swift 3沒有正確地將NSNumber類型轉換為UInt8

let x = NSNumber(value: 1000)
let y = x as? UInt8

在這種情況下,y的正確值應為nil,因為Uint8類型以0開頭,最多只能255。Swift 3採用完全不同的方法來計算餘數:1000%256 = 232。這個奇怪的行為已經在Swift 4中被修復了,所以現在ynil,正如你原先期待的一樣。

注意:讀者可以在此處閱讀有關此更改的更多訊息。

字符的純量(Unicode Scalars)

你不能直接在Swift 3中訪問某個字符的unicode scalars – 必須先將其轉換為字符串,如下所示:

let character: Character = "A"
let string = String(character)
let unicodeScalars = string.unicodeScalars
let startIndex = unicodeScalars.startIndex
let asciiCode = unicodeScalars[startIndex].value

你可以從對應字符串的unicode scalars中決定字符的ASCII代碼,Swift 4替字符添加了一個unicodeScalars屬性,因此可以直接訪問它:

let unicodeScalars = character.unicodeScalars
// original code
注意:讀者可以在此處獲取更多相關知識。

優化表情符號字串

Swift 3不能正確地判斷表情符串中的字符數:

let emojiString = "👨‍👩‍👧‍👦"
let characterCountSwift3 = emojiString.characters.count

字符串的字符數應為1,因為在那裡只有一個表情符號,但在這種情況下,Swift 3會返回4。在Swift 4修復了這個問題:

let characterCountSwift4 = emojiString.count

可以看到,你能夠直接訪問字符串本身的count屬性,因為字符串在Swift 4中被視為集合 – 更多內容在文章的另一部分。

多行字符串

在Swift 3你可以為字符的每一行添加\n符號,來宣告一個多行的字符串,並可以在text裡面跳脫(escaping)所有雙引號,如下所示:

let multipleLinesSwift3 = "You can \"escape\" all of the double quotes within the text \n by formatting it with a \"\\\" backslash before each and every single double quote that appears out there \n and insert inline \\n symbols before each and every new line of text \n in order to display the string on multiple lines in Swift 3.”

Swift 4針對多行字串採用不同的做法,透過三重引號來取代原有方法,所以不必再跳脫(escaping)雙引號:

let multiLineStringSwift4 = """
You can display strings
on multiple lines by placing a
\""" delimiter on a separate line
both right at the beginning
and exactly at the very
end of it
and don't have to "escape" double quotes
"" in Swift 4.
"""
注意:你可以在此處閱讀更多本次更新資訊。

Generic Subscripts(泛型下標)

Swift 4可以創建一個下標(Subscripts)的泛型:下標的參數和返回類型都可以是泛型,下面則是一個非泛型的Swift 3下標,用於判斷給定陣列中的最大值和最小值:

// 1
extension Array where Element: Comparable {
  // 2
  subscript(minValue: Element, maxValue: Element) -> [Element] {
    // 3
    var array: [Element] = []
    // 4
    if let minimum = self.min(), minimum == minValue {
      // 5
      array.append(minValue)
    }
    // 6 
    if let maximum = self.max(), maximum == maxValue {
      array.append(maxValue)
    }
    // 7
    return array
  }
}

這邊一步一步的介紹上面的動作:

  1. 你可以在Array類別中創建一個extension,並使用where約束其元素來實現Comparable協定。
  2. 下標採用與陣列的元素相同類型的兩個參數,並返回從其對應參數創建的數組。
  3. 你定義一個陣列,它最後將從subscript這個函式返回 – 開始時是空的。
  4. 你在if let裡拆解原始陣列的最小值,並檢查它是否等於subscript的第一個參數。
  5. 如果通過判斷式,則將最小值添加到剛剛前面建立的陣列中。
  6. 參考步驟4和5尋找原始陣列的最大值和subscript的第二個參數。
  7. 你將陣列返回,完成了

下面是數字和字符串陣列的subscript如何運行:

let primeNumbers = [3, 7, 5, 19, 11, 13]

let noMinOrMaxNumber = primeNumbers[5, 11] // []
let onlyMinNumber = primeNumbers[3, 13] // [3]
let justMaxNumber = primeNumbers[7, 19] // [19]
let bothMinAndMaxNumbers = primeNumbers[3, 19] // [3, 19]

let greetings = ["Hello", "Hey", "Hi", "Goodbye", "Bye"]

let noFirstOrLastGreeting = greetings["Hello", "Goodbye"] // []
let onlyFirstGreeting = greetings["Bye", "Hey"] // ["Bye"]
let onlyLastGreeting = greetings["Goodbye", "Hi"] // ["Hi"]
let bothFirstAndLastGreeting = greetings["Bye", "Hi"] // ["Bye", "Hi"]

看起來很酷,但你也可以使其在Swift 4中的Sequences運行,如下所示:

extension Array where Element: Comparable {
  // 1
  subscript(type: String, sequence: T) -> [T.Element] where T.Element: Equatable {
    // 2
    var array: [T.Element] = []
    // 3
    if let minimum = self.min(), let genericMinimum = minimum as? T.Element, sequence.contains(genericMinimum) {
      array.append(genericMinimum)
    }
    if let maximum = self.max(), let genericMaximum = maximum as? T.Element, sequence.contains(genericMaximum) {
      array.append(genericMaximum)
    }
    
   return array
  }
}

我們來看看與剛才的實作有何差異:

  1. subscript現在是泛型,它帶有兩個參數:一個泛型的Sequence和String類型的type,並且會返回一個泛型陣列:其元素必須符合Equatable協定。
  2. 在這個例子中,同樣定義一個泛型陣列。
  3. 你設定一個替陣列過濾最大值與最小值的泛型條件式,並檢查Sequence是否包含它們。
    <

現在,subscript真的很強大 – 下面來示範如何使用特定的Sequences,如陣列和數字、字符串集合:

let noMinOrMaxArray = [5, 11, 23]
let numberFirstArray = primeNumbers["array", noMinOrMaxArray] // []

let onlyMinNumberSet: Set = [3, 13, 2, 7]
let numberFirstSet = primeNumbers["set", onlyMinNumberSet] // [3]

let justMaxNumberArray = [7, 19, 29, 10]
let numberSecondArray = primeNumbers["array", justMaxNumberArray] // [19]

let bothMinAndMaxSet: Set = [3, 17, 19]
let numberSecondSet = primeNumbers["set", bothMinAndMaxSet] // [3, 19]

let noFirstOrLastArray = ["Hello", "Goodbye"]
let stringFirstArray = greetings["array", noFirstOrLastArray] // []

let onlyFirstSet: Set = ["Bye", "Hey", "See you"]
let stringFirstSet = greetings["set", onlyFirstSet] // ["Bye"]

let onlyLastArray = ["Goodbye", "Hi", "What's up?"]
let stringSecondArray = greetings["array", onlyLastArray] // ["Hi"]

let bothFirstAndLastSet: Set = ["Bye", "Hi"]
let stringSecondSet = greetings["set", bothFirstAndLastSet] // ["Bye", "Hi"]
注意:你可以在此處閱讀更多相關資訊。

字符串作為集合

Swift 4中字符串是集合,因此現在可以將其視為陣列或序列,並簡化某些任務,例如,下面範例是你如何在Swift 3中過濾給定的字符串:

let swift3String = "Swift 3"
var filteredSwift3String = ""

for character in swift3String.characters {
  let string = String(character)
  let number = Int(string)
  
  if number == nil {
    filteredSwift3String.append(character)
  }
}

你在code>for in迴圈來逐個處理字符串的字符,每個字符首先被轉換成一個字符串,然後你檢查該字符串是否實際上不是一個數字。最後,如果通過判斷式,就將對應的字符添加到過濾的字符串中。

Swift 4可以直接在字符串本身使用函數編程技術來直接完成這些操作:

let swift4String = "Swift 4”
let filteredSwift4String = swift4String.filter{Int(String($0)) == nil}

當從一個給定的字符串中定義子串時,Swift 3會返回一個字符串:

let swift3SpaceIndex = swift3String.characters.index(of: " ")
let swift3Substring = swift3String.substring(to: swift3SpaceIndex!)

你可以使用字符串的substring(to:)方法獲取對應的子字符串,Swift 4則採用了完全不同的方法,它添加一個全新的Substring類型:

let swift4SpaceIndex = swift4String.index(of: " ")
let swift4Substring = swift4String[..<swift4SpaceIndex!]

在這種情況下,你可以使用one sided ranges來決定子字符串 – 此類型為Substring,而不是StringStringSubstring類型都遵守StringProtocol協定,因此它們的行為完全相同。

注意:你可以在此處獲取本次更新的更多資訊。

優化Objective-C Inference

Swift 3會自動推斷@objc註釋,推薦使用Objective-C中可用的屬性或方法的selectors和key paths。

class ObjectiveCClass: NSObject {
   let objectiveCProperty: NSObject
   func objectiveCMethod() {}
  
  init(objectiveCProperty: NSObject) {
    self.objectiveCProperty = objectiveCProperty
  }
}

let selector = #selector(ObjectiveCClass.objectiveCMethod)
let keyPath = #keyPath(ObjectiveCClass.objectiveCProperty)

你創建一個繼承自NSObject的自定義類別,並為其屬性和方法定義selectors和key paths。 Swift 4需要你自己手動添加@objc註釋,以便所有內容仍然可以正常工作:

class ObjectiveCClass: NSObject {
 @objc let objectiveCProperty: NSObject
 @objc func objectiveCMethod() {}
 // original code
}
注意:你可以在此處閱讀更多相關資訊。

閉包中的Tuple解構

在Swift 3讓你可以直接使用tuple的組件,只要它們做為一個給定閉包中的參數,以下是實現此功能的方法,使用函數式編程技術映射一組tuple陣列:

let players = [(name: "Cristiano Ronaldo", team: "Real Madrid"), (name: "Lionel Messi", team: "FC Barcelona")]
let tupleDestructuring = players.map{name, team in (name.uppercased(), team.uppercased())}

你可以使用閉包的參數,將plater的name和team轉為大寫,這在Swift 4中是不再通用,但是有兩個變通方法,第一是使用整個tuple作為閉包的參數,如下所示:

let tuple = players.map{player in (player.name.uppercased(), player.team.uppercased())}

第二個解決方案是使用縮略參數(shorthand arguments):

let shorthandArguments = players.map{($0.0.uppercased(), $0.1.uppercased())}
注意: 這個改變在Swift社群引起了很多爭議,將來很可能會被恢復,所以我們就停留在這裡,你可以在此處閱讀更多相關資訊。

在協定中約束Associated Types

在Swift 4中,你可以約束協定內的Associated types(關聯類型):

protocol SequenceProtocol {
  associatedtype SpecialSequence: Sequence where SpecialSequence.Element: Equatable
}

首先,你創建一個自定義的協定,並添加符合Sequence協定的關聯類型。然後,使用where子句來限制Sequence的元素:它們都應該實現Equatable協定。

注意: 你可以在此處閱讀更多相關資訊。

結論

這就是所有關於Swift 4的內容,希望你喜歡這個教程,並享受Swift 4的所有變化。同時,如果您有任何問題或疑問,請通知我。 Happy Swifting!:)

譯者簡介:陳奕先-過去為平面財經記者,專跑產業新聞,2015年起跨進軟體開發世界,希望在不同領域中培養新的視野,於新創學校ALPHA Camp畢業後,積極投入iOS程式開發,目前任職於國內電商公司。聯絡方式:電郵[email protected]

FB : https://www.facebook.com/yishen.chen.54
Twitter : https://twitter.com/YeEeEsS

原文What’s New in Swift 4 by Example


Cosmic Pupăză 在部落格 cosminpupaza.wordpress.com 分享有關 Swift 和 iOS 開發文章,以及參與了 raywenderlich.com 和 appcoda.com 兩個平台的 Swift 教學團隊。平日喜歡玩結他和研究二戰歷史。可以透過電郵:[email protected],在Facebook,Twitter 及 Google+ 找到 Cosmic。

blog comments powered by Disqus
Shares
Share This