Swift 5.0 & 5.1 更新

今天浏览了HackingWithSwift上关于Swift5.0和5.1更新内容的文章,这里记录一下感兴趣的部分。

New in Swift 5.0

Raw Strings

可以使用#-#生成不需要使用 \ 进行转译的字符串,如

let msg = #"this is a messge"#

可以在引号前添加任意多个 #,只要首尾的#数量相同,如:

let hashes = ####"this is a \"test""####

当需要在 raw string 中使用 interpolation时,需要:

let title = "John"
let msg = #"Hi, Mr \#(title)!"#

如果经常需要写正则表达式,那么这个功能无疑是一福音。

标准类型: Result

顾名思义, Result是一个用于表示返回结果的类型,主要运用在异步的API中。它是一个 enum类型,包含 success 和 failure。两者都是使用范型定义的,但是 failure 需要实现 Error 协议, 即

public enum Result<Success, Failure> where Failure : Error

它的使用也很简单,看个例子就能明白:

enum NetworkError: Error {
    case badURL
}
func fetchUnreadCount1(from urlString: String, completionHandler: @escaping (Result<Int, NetworkError>) -> Void)  {
    guard let url = URL(string: urlString) else {
        completionHandler(.failure(.badURL))
        return
    }

    // complicated networking code here
    print("Fetching \(url.absoluteString)...")
    completionHandler(.success(5))
}
fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    switch result {
    case .success(let count):
        print("\(count) unread messages.")
    case .failure(let error):
        print(error.localizedDescription)
    }
}

通过Result的定义,还可以发现:

  1. get() 方法要么返回success值,要么 throws,因此需要 try? result.get()
  2. 可以使用一个带throws闭包来初始化 Result
  3. failure可以使用库中的 Error

Result 还有一系列方法:map/flatMap/mapError/flatMapError,其中 flatMap和map的区别是当传递给map的闭包返回一个result时, flatMap会直接返回那个result;而map会把该result包裹进一个result再返回。

String Interpolation

协议ExpressibleByStringInterpolation 在3.0 被废弃,在5.0又被引入,这个功能使得字符串的插值化更灵活更方便。为了更好地理解这个特性,笔者特地查看了 SE-0228 的说明。先来看下当前的字符串插值是如何进行:编译器会把字符串解析成一系列片段,每个片段要么包含纯的字符串(含转译字符串),要么是一个等待插入值的表达式,形如:

// Semantic expression for: "hello \(name)!"
String(stringInterpolation:
  String(stringInterpolationSegment: "hello "),
  String(stringInterpolationSegment: name),
  String(stringInterpolationSegment: "!"))

这种设计在效率和灵活性上都有不足,比如:

  1. 内存管理低下:现有实现依赖String(stringInterpolationSegment:),这会导致复制内存段,极端情况下,会占有过多的内存。而对于每个segment,其位置和长度都是知道的,并不需要生成临时变量以占用额外内存
  2. 插值方式单一导致的灵活性不够:插值表达式没有额外的参数;segment可以是任何参数从而导致缺少参数约束;丢失segment的语义,可以从字符串解析生成各个segment但反过来却不行,这样就无法得出segment是由字符串还是表达式而来

SE-0228列举了几种适用情形:

//formatting
/// Use printf-style format strings:
"The price is $\(cost, format: "%.2f")"

//logging
log("Processing \(public: tagName) tag containing \(private: contents)")

//attributed strings
"\([.link: supportURL])Click here\([.link: nil]) to visit our support site"

//localization
// Builds a LocalizableString(key: "The document “%@” could not be saved.", arguments: [name])
let message: LocalizableString = "The document “\(name)” could not be saved."
alert.messageText = String(localized: message)

接下来,我们跟随hackingWithSwift看一下这个更新带来的便利。
首先就是灵活的格式化了,这也是最基本的用法。

extension String.StringInterpolation {
    mutating func appendInterpolation(_ number: Int, style: NumberFormatter.Style) {
        let formatter = NumberFormatter()
        formatter.numberStyle = style

        if let result = formatter.string(from: number as NSNumber) {
            appendLiteral(result)
        }
    }
}

let number = Int.random(in: 0...100)
let lucky = "The lucky number this week is \(number, style: .spellOut)."

除此之外,对于需要构造正则表达式,html/xml字符串或者sqlite statement语句的情形,ExpressibleByStringInterpolation 可以帮助你便捷地实现。HTMLComponent 是一个很好的构建html字符串的例子,这里就不做搬运。值得一提的是,对于 ExpressibleByStringInterpolation 协议,如果需要实现自定义的插值,必须构建一个实现 StringInterpolationProtocol 的类型。如果没有这种类型的话,系统会自动使用内建的 DefaultStringInterpolation,但其通常是无法满足自定义的插值情形的。对于语句 let text: HTMLComponent = "You should follow me on Twitter \(twitter: "twostraws"), or you can email me at \(email: "paul@hackingwithswift.com").",其执行顺序:

init(literalCapacity: Int, interpolationCount: Int)
appendLiteral(_ literal: String)
appendInterpolation(twitter: String)
appendLiteral(_ literal: String)
appendInterpolation(email: String)
appendLiteral(_ literal: String)

dynamically callable types

这个更新是一种语法糖,先来看一下例子:

@dynamicCallable
struct RandomNumberGenerator1 {
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double {
        let numberOfZeroes = Double(args.first?.value ?? 0)
        let maximum = pow(10, numberOfZeroes)
        return Double.random(in: 0...maximum)
    }
}
let random1 = RandomNumberGenerator1()
let result1 = random1(number: 0)
let result2 = random1(1, 2)

首先要对model声明 dynamicCallable,然后实现函数 dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>)->T。于是,对于 result1会调用dynamicallyCall,输入参数是一组键值对。对于result2,输入参数也是一组键值对,只不过其中每个key是空字符串。
如果同时实现了函数: dynamicallyCall(withArguments args: [Int]) -> T,result2 就会调用该函数。
因此,从技术上而言,要实现 dynamically call,需要实现以上两个函数。
这个更新,提供的便利性在于可以通过实例调用一个函数,从而实现了一个算子的功能。而从[SE-0216]的描述来看,这个更新想把Swift与C/ObjC之优越的交互拓展到类如Python的语言。

handle future enum cases

这是一个实用的功能。在实际中,随着业务或者功能的拓展,往往会对enum类型进行扩展,这就会对已有代码带来影响:如果之前枚举了每一个类型,那么编译器会报错;如果之前添加了 default,那么就缺失对新增类型的处理。
Swift5.0对这种情形的处理是在 default 分支前添加 @unknown。当有新类型添加的时候,编译器不会报错,而是会给出提示,由你决定如何处理。

flatten nested optional

简单地讲,这个更新就是把嵌套的optional展开成普通的optional,形如:

optional(optional(type)) -> optional(type)

整数因子判断

添加了 'isMultiple',增加语句的可读性。

compactMapValues

对于Dictionary,compactMap 的 结果形如:

(key: "Paul", value: Optional(38)

使用 compactMapValues 则可以:

paul:38

如果value是一个nil的话, compactMapValues 会直接将该key移除。所以该函数的操作流程:

  1. 变形
  2. 解包
  3. 移除值为nil的dictionary

在实际中,可以免去了对nil的判断,提高执行效率。

New in Swift 5.1

成员初始化

先看例子:

struct User {
    var name: String
    var loginCount: Int = 0
}

let piper = User(name: "Piper Chapman", loginCount: 0)
let suzanne = User(name: "Suzanne Warren")

在5.1中,suzanne 也是合法的。
这个更新,私以为不是很妥当。设想Swift中设计了 designated 和 convenience initialize,来确保每个成员变量能得到初始化。因此在设计初始化函数的时候,需要很好地设计接口。5.1中引入的这种增强,势必会破坏这种习惯。

implicit return

对于只含有一个表达式的函数,如果有返回值的话,可以省略 return

func double(_ number: Int) -> Int {
    number * 2
}

Self

设想一组父子类:

class NetworkManager {
    class var maximumActiveRequests: Int {
        return 4
    }

    func printDebugData() {
        print("Maximum network requests: \(NetworkManager.maximumActiveRequests).")
    }
}
class ThrottledNetworkManager: NetworkManager {
    override class var maximumActiveRequests: Int {
        return 1
    }
}

let manager = ThrottledNetworkManager()
manager.printDebugData()

对于子类实例 manager 其 maximumActiveRequests 为1,但打印的时候却为4。Self就是为了解决这个问题:在class中,Self 就是动态地代表当前的类,主要用于静态成员变量。Self 简化了 self.dynamicType 的写法,使得书写更通用和一致。

opaque type

在 SwiftUI 中,经常可以看到 some view。使用 some 可以使得一个类型变成 opaque type,考虑如下代码:

protocol Fighter { }
struct XWing: Fighter { }

func launchFighter() -> Fighter {
    return XWing()
}
func launchOpaqueFighter() -> some Fighter {
    return XWing()
}

launchFighter 返回一个 Flight,但是如果想要判断这个 Flight具体是哪个时(是XWing或YWing),就无能无力了。想要实现这个功能,你能想到的解决方案是让 Flighter 遵循 Equatable,但是马上你就会遇到问题:

Protocol 'Fighter' can only be used as a generic constraint because it has Self or associated type requirements

错误原因:对于 Equatable 编译器需要知道左右两边的类型是否相同。
于是 some 登场了,launchOpaqueFighter 返回的就是一个 opaque 类型的 Flighter,在这种情况下对于调用方得到的还是一个 Flighter 类型,但是编译器却知道该类型是 XWing。因此,和范型不一样的地方在于:

1.范型是由调用方决定最终的类型,opaque type 是函数内部自己决定最终的类型 2.范型返回的是一组容器,效率上存在问题 3.在编译阶段,opaque type 是确定的

好了,我们接着看下面的内容:

func makeInt() -> some Equatable {
    Int.random(in: 1...10)
}

func makeString() -> some Equatable {
    "Red"
}

makeInt() == makeString() //build error

虽然上述两个函数都返回 some Equatable,但编译器知道一个返回 Int 另一个返回String,两者是无法比较的。
另外,some 除了可以用在返回类型,还可以用在属性和下标上。

static and class subscripts

这是一种语法的改进,可以使用以下的方式访问类静态变量:

public enum NewSettings {
    private static var values = [String: String]()
    public static subscript(_ name: String) -> String? {
        get {
            return values[name]
        }
        set {
            print("Adjusting \(name) to \(newValue ?? "nil")")
            values[name] = newValue
        }
    }
}

NewSettings["Captain"] = "Gary"
NewSettings["Friend"] = "Mooncake"
print(NewSettings["Captain"] ?? "Unknown")

warnings for ambiguous none

Swift的optional类型是一个 enum,包含 nonesome。因此,自定义的enum如果使用了 none 关键字,就会产生歧义。5.1会改善对此类问题的警告。

enum optional

对于optional enum类型,在switch的时候, case中也要使用optional。在5.1中,可以不需要使用optional。

ordered collection diffing

在实际刷新数据的时候,经常会遇到数据源发生变化而需要刷新UI的情形,这时往往要求对移除和添加的数据按序实现并伴随动画。为了实现这个需求,需要知道哪些数据是要被移除,哪些是被添加的,5.1增加了 difference:

let diff = scores2.difference(from: scores1)

for change in diff {
    switch change {
    case .remove(let offset, _, _):
        scores1.remove(at: offset)
    case .insert(let offset, let element, _):
        scores1.insert(element, at: offset)
    }
}
print(scores1)
//or 
let result = scores1.applying(diff) ?? []

creating uninitialized array

顾名思义,就是创建array的时候,允许只分配内存但不初始化, 比如:

let randomNumbers = Array<Int>(unsafeUninitializedCapacity: 10) { buffer, initializedCount in
    for x in 0..<5 {
        buffer[x] = Int.random(in: 0...10)
    }
    initializedCount = 5//must set
}

这里,要求分配10个Int的Array,但在初始化的闭包中初始化了5个。
这个改进主要用于内置库中高效算法的实现,比如需要根据array计算新array,这时就不需要分配并初始化内存来存放新array。

References

SE-0228
SE-0244