2019/06/15

Swift 5 語言新特性簡述

Swift 5 在2019年3月25日正式release,主要最廣為人討論的就是ABI Stability,也就是Swift runtime將被放在作業系統裡而不是包含在App裡,更進一步的說明可以參考ABI Stability and MoreEvolving Swift On Apple Platforms After ABI Stability

此外,Swift 5語言本身也增加了一些新的特性,包含新的語法及標準函式庫的更新。本篇文章將概略說明部分較常被提出的新特性。關於完整的Swift 5更新內容請參考Swift部落格的這篇文章:Swift 5 Released!

Raw Strings SE-0200

在字串前後加上#符號,即可使用raw string
在raw string中,反斜線雙引號都可以直接使用,不再需要額外的跳脫字元

let rawStr = #"Hello, \ My "dear" friend!"#
print(rawStr) 

印出結果:

Hello, \ My "dear" friend!

若依然要使用跳脫字元,使用反斜線加上#符號:

let rawStr = #"Hello,\#nMy friend!"#
print(rawStr)

如此才可印出有換行的結果:

Hello,
My friend!

你可以改在字串前後加上兩個或更多個#符號,使用raw string。如此一來,即可改在字串中直接輸入"#;此外,跳脫字元變也為\##

let rawStr = ##"Hello,\##n Now yout can type "# directly!"##
print(rawStr) 

印出結果:

Hello,
Now yout can type "# directly!

Customizing string Interpolation SE-0228

String Interpolation是swift在字串中放入變數的方式之一,如下:

let name = "myitem"
let weight = 123
print("The item is \(name). Its weight is \(weight).") 
//印出The item is myitem. Its weight is 123.

然而,若是自訂的類別物件,在以往,需要令該類別實作CustomStringConvertibledescription property,才能印出自訂的內容;類似Java的toString。

而現在,在Swift 5中,可以使用extension替String.StringInterpolation增加新的appendInterpolation方法,以處理自訂的類別。

class MyItem {
    let name = "item name"
    let weight = 123 
    init(name:String, weight:Int) {
        self.name = name
        self.weight = weight
    }
}

extension String.StringInterpolation {
    mutating func appendInterpolation(_ value: MyItem) {
        appendInterpolation("The item is \(value.name). Its weight is \(value.weight).")
    }
}

let myitem = MyItem(name:"item name", weight:123)
print("myitem:\(myitem)")
//印出myitem:The item is myitem. Its weight is 123.

此外,在增加新的appendInterpolation方法時,
你也可以使用多個參數,以及使用argument label,就像自訂函數一樣,進一步客製想要的String Interpolation內容:

extension String.StringInterpolation {
    mutating func appendInterpolation(mystring: String, replace oldString:String, with newString:String) {
        let newstr = mystring.replacingOccurrences(of: oldString, with: newString)
        appendInterpolation(newstr)
    }
}

print("test:\(mystring:"aaabbbccc", replace:"bbb", with:"ddd")")
//印出test:aaadddccc

Standard Result type SE-0235

Swift 5 標準函式庫中加入了Result型別,讓程式開發者可以更一致地處理錯誤。

Result是一個enum,有successfailure兩個case,並分別各有一個associated value,分別代表執行成功時欲回傳的資料,以及執行失敗時發生的錯誤。

public enum Result<Success, Failure: Error> {
    case success(Success), failure(Failure)
}

假設我們使用要定義一個取得新聞資料的API,並使用Result表示結果,可以定義如下:

func fetchNews(from urlString: String, completionHandler: @escaping (Result<MyNews, MyNewsError>) -> Void) {
    guard let url = URL(string: urlString) else {
        //發生錯誤時回傳.failure及錯誤內容
        completionHandler(.failure(.networkError))
        return
    }

    var mynews = MyNews()
    // ...
    
    //正確執行時,回傳.success及資料
    completionHandler(.success(mynews))
}

MyNews是某個自訂的類別;MyNewsError則是自訂的enum,繼承自標準函式庫的Error。

class MyNews {
    //...
}

enum MyNewsError: Error {
    case networkError
}

如此一來,在呼叫fetchNews取新聞資料時,可以用switch case以更直覺簡單地分別處理成功與失敗的結果:

fetchNews(from: "https://www.xxx.com") { result in
    switch result {
    case .success(let mynews):
        // ...
    case .failure(let myerror):
        // ...
    }
}

Dynamically callable types SE-0216

dynamicCallable可以讓你定義一個型別,其宣告出來的變數可以如同函式般直接使用,如下:

@dynamicCallable
struct MyMultiplier {
    private num:Int
    init(num:Int) {
        self.num = num;
    }
    func dynamicallyCall(withArguments args: [Int]) -> Int {
        //將陣列中的所有元素乘以10後相加。
        return args.map { $0 * num }.reduce(0,+);
    }
}

建立MyMultiplier型別的物件,並令其當作函式來呼叫:

let m10 = MyMultiplier(num:10)
m10(1,2,3,4,5) //150

如上,該型別必須實作dynamicallyCall方法,而參數可以是withArguments任意型別的陣列,如上例為Int陣列。

此外,也可以改用withKeywordArguments,並把參數改為KeyValuePairs<String, Any>,讓函式可以取得傳入的參數名稱。

@dynamicCallable
struct MyFunc {
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Any>) {
    //...        
    }
}

let myfunc = MyFunc()
myfunc(name:"test name", count:123)

dynamicCallable可以用於struct, class, enum

Handling Future Enumeration Cases SE-0192

當switch述句中的條件判斷若其值來自非自定義的enum,如:C enums或是來自standard library的enum,則意味著此switch述句的條件判斷值,在未來可能需要處理非預期的內容;而通常在這種情況下,我們一定會用到default來處理。

以下以來自standard library的enum: AVAudioSession.InterruptionType為例。

Standard library中AVAudioSession.InterruptionType的定義如下:

    public enum InterruptionType : UInt {

        case began

        case ended
    }

假設我們有一個switch述句需要針對AVAudioSession.InterruptionType進行處理:

//判斷AudioInterruption的狀態並印出log...
func logAudioInterruptionType(inttType:AVAudioSession.InterruptionType) {
    switch inttType {
    case .began:
        print("began:\(inttType)")
    case .ended:
        print("ended:\(inttType)")
}

由於AVAudioSession.InterruptionType是來自standard library的enum,未來有可能會增加新的值;所以編譯器提出警告:

Switch covers known cases, but 'AVAudioSession.InterruptionType' may have additional unknown values, possibly added in future versions

Handle unknown values using "@unknown default"

使用 @unknown 標註,暗示未來隨著標準函式庫的更新,此switch case有可能會遇到其他未知的enum類型:

func logAudioInterruptionType(inttType:AVAudioSession.InterruptionType) {
    switch inttType {
    case .began:
        print("began:\(inttType)")
    case .ended:
        print("ended:\(inttType)")
        
    @unknown default:
        print("unknown type:\(inttType)")
    }
}

為何不直接寫上default?

如果直接寫個default,雖然也不會出現編譯錯誤,

func logAudioInterruptionType(inttType:AVAudioSession.InterruptionType) {
    switch inttType {
    case .began:
        print("began:\(inttType)")
    case .ended:
        print("ended:\(inttType)")
        
    default:  //不太合理,因為目前看來不可能...
        print("unknown type:\(inttType)")
    }
}

//備註:在Swift語言中,switch裡的每個case執行完就會自動跳出,不需要像C語言一樣使用break來防止繼續往下執行到其他case

但就邏輯上來說,這是令人困惑的,
因為就現況而言,AVAudioSession.InterruptionType確實只有 .began.ended 兩種值;
若是自定義的enum,而你的 switch case 也已明確地處理好所有enum值的話,再加上default,編譯器甚至還會回報Warning,請你將這個累贅的 default陳述句 移除:

Default will never be executed

一般的default應該用於如下情境,switch case中尚有值仍未處理的情況;

public enum MyGreetings {

    case .goodmorning

    case .goodnight
}

func printMyGreetings(greetingType:MyGreetings) {
    switch greetingType {
    case .goodmorning:
        print("Good Morning")
    default:  //合理,因為還有goodnight沒有處理到...
        print("Good night...I think...")
}

而加上 @unknown 則意味著,此處的 default 可能目前不會遇到,但是在往後有可能會遇到的,所以通常會是來自 非自定義的enum 或是C enums

Proposal SE-0192 就是希望能在程式中刻意區分這一點而提出,也才有了用 @unknown 來處理future enumeration的這個變動。

This proposal aims to distinguish between enums that are frozen (meaning they will never get any new cases) and those that are non-frozen, and to ensure that clients handle any future cases when dealing with the latter.

Flatten nested optionals resulting from ‘try?’ SE-0230

因爲try而造成的nested optionals,意即如:String?? 這樣有兩個 ?? 的情況,在swift 5之後也會變為普通的optional。如:

class MyItem {
    init?(itemid:String) {
        //假設init可能會因為不適合的itemid而建立失敗
    }
    
    //回傳String,但可能拋出錯誤
    func getItemInfo() throws -> String {
        ///...
    }
    
    //回傳String?
    func getItemDesc() -> String? {
        ///...
    }
}

//getItemInfo可能拋出錯誤,需使用try處理
let myItemInfo = try? MyItem(itemid:"aaa")?.getItemInfo()

在swift 5 之前,myItemInfo為String??

在swift 5 之後,myItemInfo為String?

這個改變是因為,原本多數的 optional 使用情境,都會避免發生 nested optionals,如下:

//getItemDesc方法不會拋出錯誤,而是回傳Optional String
let myItemDesc = MyItem(itemid:"aaa")?.getItemDesc()

上面程式中的myItemDesc為String?

不會因為 initializer 回傳 optional 型態,而getItemDesc方法也回傳 optional 型態,
進而造成有兩個 ??nested optionals

因此,於Swift 5中,為了使 try? 與其他 optional 的使用情境更一致,而做了此更動。

Reference

https://swift.org/blog/swift-5-released/

What’s new in Swift 5.0 – Hacking with Swift

What’s New in Swift 5? | raywenderlich.com

https://developer.apple.com/documentation/xcode_release_notes/xcode_10_2_release_notes/swift_5_release_notes_for_xcode_10_2

沒有留言:

張貼留言