[译] Swift 5.0 新特性

Swift 5.0 是 Swift 的下一个主要的 release,随之而来的是 ABI 的稳定性,同时还实现了几个关键的新功能,包括 raw string,将来的枚举 case,Result 类型,检查整数倍数等等。前端

  • 你能够亲自尝试一下:我建立了一个 Xcode Playground 来展现 Swift 5.0 的新特性,里面有一些你能够参考的例子。

标准 Result 类型

SE-0235 在标准库中引入了全新的 Result 类型,它让咱们可以更加方便清晰地在复杂的代码中处理 error,例如异步 API。python

Swift 的 Result 类型是用枚举实现的,其中包含了 successfailure。它们二者都使用泛型,所以你能够为它们指定任意类型。可是 failure 必须遵循 Swift 的 Error 协议。android

为了进一步演示 Result,咱们能够写一个网络请求函数来计算用户有多少未读消息。在此示例代码中,咱们将只有一个可能的错误,即请求的字符串不是有效的 URL:ios

enum NetworkError: Error {
    case badURL
}
复制代码

fetch 函数将接受 URL 字符串做为其第一个参数,并将 completion 闭包做为其第二个参数。该 completion 闭包自己将接受一个 Result,其中 success 将存储一个整数,failure 将是某种 NetworkError。咱们实际上并无在这里链接到服务器,但使用 completion 闭包可让咱们模拟异步代码。git

代码以下:github

import Foundation

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

    // 此处省略复杂的网络请求
    print("Fetching \(url.absoluteString)...")
    completionHandler(.success(5))
}
复制代码

要调用此函数,咱们须要检查 Result 中的值来看看咱们的请求是成功仍是失败,代码以下:正则表达式

fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    switch result {
    case .success(let count):
        print("\(count) 个未读信息。")
    case .failure(let error):
        print(error.localizedDescription)
    }
}
复制代码

在开始在本身的代码中使用 Result 以前,你还有三件事应该知道。express

首先,Result 有一个 get() 方法,若是存在则返回成功值,不然抛出错误。这容许你将 Result 转换为常规会抛出错误的函数调用,以下所示:swift

fetchUnreadCount1(from: "https://www.hackingwithswift.com") { result in
    if let count = try? result.get() {
        print("\(count) 个未读信息。")
    }
}
复制代码

其次,Result 还有一个接受抛出错误闭包的初始化器:若是闭包返回一个成功的值,用于 success 的状况,不然抛出的错误则被传入 failure后端

举例:

let result = Result { try String(contentsOfFile: someFile) }
复制代码

第三,你可使用通用的 Error 协议而不是你建立的特定错误的枚举。实际上,Swift Evolution 提议说道“预计 Result 的大部分用法都会使用 Swift.Error 做为 Error 类型参数。”

所以你要用 Result <Int,Error> 而非 Result<Int, NetworkError>。这虽然意味着你失去了可抛出错误类型的安全性,但你能够抛出各类不一样的错误枚举,其实这取决于你的代码风格。

Raw string

SE-0200 添加了建立原始字符串(raw string)的功能,其中反斜杠和井号是被做为标点符号而不是转义字符或字符串终止符。这使得许多用法变得更容易,特别是正则表达式。

要使用原始字符串,请在字符串前放置一个或多个 #,以下所示:

let rain = #"西班牙"下的"雨"主要落在西班牙人的身上。"#
复制代码

字符串开头和结尾的 # 成为字符串分隔符的一部分,所以 Swift 明白 "雨" 和 "西班牙" 两边独立引号应该被视为标点符号而不是终止符。

原始字符串也容许你使用反斜杠:

let keypaths = #"诸如 \Person.name 之类的 Swift keyPath 包含对属性未调用的引用。"#
复制代码

这将反斜杠视为字符串中的文字字符而不是转义字符。否则则意味着字符串插值的工做方式不一样:

let answer = 42
let dontpanic = #"生命、宇宙及万事万物的终极答案都是 \#(answer)."#
复制代码

请注意我是如何使用 \#(answer) 来调用字符串插值的,通常 \(answer) 将被解释为 answer 字符串中的字符,因此当你想要在原始字符串中进行引用字符串插值时你必须添加额外的

Swift 原始字符串的一个有趣特性是在开头和结尾使用井号,由于你通常不会一下使用多个井号。这里很难提供一个很好的例子,由于它真的应该很是罕见,但请考虑这个字符串:个人狗叫了一下 "汪"#好狗狗。由于在井号以前没有空格,Swift 看到 "# 会当即把它做为字符串终止符。在这种状况下,咱们须要将分隔符从 #" 改成 ##",以下所示:

let str = ##"个人狗叫了一下 ""#乖狗狗"##
复制代码

注意末尾的井号数必须与开头的一致。

原始字符串与 Swift 的多行字符串系统彻底兼容,只需使用 #""" 开始,而后以 """# 结束,以下所示:

let multiline = #"""
生命、
宇宙,
以及众生的答案都是 \#(answer).
"""#
复制代码

能在正则表达式中再也不大量使用反斜杠足以证实这颇有用。例如编写一个简单的正则表达式来查询关键路径,例如 \Person.name,看起来像这样:

let regex1 = "\\\\[A-Z]+[A-Za-z]+\\.[a-z]+"
复制代码

多亏了原始字符串,咱们能够只用原来一半的反斜杠就能够编写相同的内容:

let regex2 = #"\\[A-Z]+[A-Za-z]+\.[a-z]+"#
复制代码

咱们仍然须要 一些 反斜杠,由于正则表达式也使用它们。

Customizing string interpolation

SE-0228 大幅改进了 Swift 的字符串插值系统,使其更高效、灵活,并创造了之前不可能实现的全新功能。

在最基本的形式中,新的字符串插值系统让咱们能够控制对象在字符串中的显示方式。Swift 具备有助于调试的结构体的默认行为,它打印结构体名称后跟其全部属性。可是若是你使用类的话就没有这种行为,或者想要格式化该输出以使其面向用户,那么你可使用新的字符串插值系统。

例如,若是咱们有这样的结构体:

struct User {
    var name: String
    var age: Int
}
复制代码

若是咱们想为它添加一个特殊的字符串插值,以便咱们整齐地打印用户信息,咱们将使用一个新的 appendInterpolation() 方法为 String.StringInterpolation 添加一个 extension。Swift 已经内置了几个,而且用户插值 类型,在这种状况下须要 User 来肯定要调用哪一个方法。

在这种状况下,咱们将添加一个实现,将用户的名称和年龄放入一个字符串中,而后调用其中一个内置的 appendInterpolation() 方法将其添加到咱们的字符串中,以下所示:

extension String.StringInterpolation {
    mutating func appendInterpolation(_ value: User) {
        appendInterpolation("我叫\(value.name)\(value.age)岁")
    }
}
复制代码

如今咱们能够建立一个用户并打印出他们的数据:

let user = User(name: "Guybrush Threepwood", age: 33)
print("用户信息:\(user)")
复制代码

这将打印 用户信息:我叫 Guybrush Threepwood,33 岁,而使用自定义字符串插值它将打印 用户信息:User(name: "Guybrush Threepwood", age: 33) 。固然,该功能与仅实现CustomStringConvertible 协议没有什么不一样,因此让咱们继续使用更高级的用法。

你的自定义插值方法能够根据须要使用任意数量的参数,标记的和未标记的。例如,咱们可使用各类样式添加插值来打印数字,以下所示:

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)
        }
    }
}
复制代码

NumberFormatter 类有许多样式,包括货币形式(489.00 元),序数形式(第一,第十二)和朗读形式(五, 四十三)。 所以,咱们能够建立一个随机数,并将其拼写成以下字符串:

let number = Int.random(in: 0...100)
let lucky = "这周的幸运数是 \(number, style: .spellOut)."
print(lucky)
复制代码

你能够根据须要屡次调用 appendLiteral(),若是须要的话甚至能够不调用。例如咱们能够添加一个字符串插值来屡次重复一个字符串,以下所示:

extension String.StringInterpolation {
    mutating func appendInterpolation(repeat str: String, _ count: Int) {
        for _ in 0 ..< count {
            appendLiteral(str)
        }
    }
}

print("Baby shark \(repeat: "doo ", 6)")
复制代码

因为这些只是常规方法,你可使用 Swift 的所有功能。例如,咱们可能会添加一个将字符串数组链接在一块儿的插值,但若是该数组为空,则执行一个返回字符串的闭包:

extension String.StringInterpolation {
    mutating func appendInterpolation(_ values: [String], empty defaultValue: @autoclosure () -> String) {
        if values.count == 0 {
            appendLiteral(defaultValue())
        } else {
            appendLiteral(values.joined(separator: ", "))
        }
    }
}

let names = ["Harry", "Ron", "Hermione"]
print("学生姓名:\(names, empty: "空").")
复制代码

使用 @autoclosure 意味着咱们可使用简单值或调用复杂函数做为默认值,但除非 values.count 为零,不然不会作任何事。

经过结合使用 ExpressibleByStringLiteralExpressibleByStringInterpolation 协议,咱们如今可使用字符串插值建立整个类型,若是咱们添加 CustomStringConvertible,只要咱们想要的话,甚至能够将这些类型打印为字符串。

为了让它生效,咱们须要知足一些特定的标准:

  • 咱们建立的类型应该遵循 ExpressibleByStringLiteralExpressibleByStringInterpolationCustomStringConvertible。只有在你想要自定义打印类型的方式时才须要遵循最后一个协议。
  • 在你的类型 内部 须要是一个名为 StringInterpolation 并遵循 StringInterpolationProtocol 的嵌套结构体。
  • 嵌套结构体须要有一个初始化器,它接受两个整数,告诉咱们大概预期的数据量。
  • 它还须要实现一个 appendLiteral() 方法,以及一个或多个 appendInterpolation() 方法。
  • 你的主类型须要有两个初始化器,容许从字符串文字和字符串插值建立它。

咱们能够将全部这些放在一个能够从各类常见元素构造 HTML 的示例类型中。嵌套 StringInterpolation 结构体中的 “暂存器” 将是一个字符串:每次添加新的文字或插值时,咱们都会将其追加到字符串的末尾。为了让你确切了解其中发生了什么,我在各类追加方法中添加了一些 print() 来打印。

如下是代码:

struct HTMLComponent: ExpressibleByStringLiteral, ExpressibleByStringInterpolation, CustomStringConvertible {
    struct StringInterpolation: StringInterpolationProtocol {
        // 以空字符串开始
        var output = ""

        // 分配足够的空间来容纳双倍文字的文本
        init(literalCapacity: Int, interpolationCount: Int) {
            output.reserveCapacity(literalCapacity * 2)
        }

        // 一段硬编码的文本,只需添加它就能够
        mutating func appendLiteral(_ literal: String) {
            print("追加 ‘\(literal)’")
            output.append(literal)
        }

        // Twitter 用户名,将其添加为连接
        mutating func appendInterpolation(twitter: String) {
            print("追加 ‘\(twitter)’")
            output.append("<a href=\"https://twitter/\(twitter)\">@\(twitter)</a>")
        }

        // 电子邮件地址,使用 mailto 添加
        mutating func appendInterpolation(email: String) {
            print("追加 ‘\(email)’")
            output.append("<a href=\"mailto:\(email)\">\(email)</a>")
        }
    }

    // 整个组件的完整文本
    let description: String

    // 从文字字符串建立实例
    init(stringLiteral value: String) {
        description = value
    }

    // 从插值字符串建立实例
    init(stringInterpolation: StringInterpolation) {
        description = stringInterpolation.output
    }
}
复制代码

咱们如今可使用字符串插值建立和使用 HTMLComponent 的实例,以下所示:

let text: HTMLComponent = "你应该在 Twitter 上关注我 \(twitter: "twostraws"),或者你能够发送电子邮件给我 \(email: "paul@hackingwithswift.com")。"
print(text)
复制代码

多亏了分散在里面的 print(),你会看到字符串插值功能的准确做用:“追加 ‘你应该在 Twitter 上关注我’”,“追加 ’twostraws’”,“追加 ’,或者你能够发送电子邮件给我 ’”,“追加 ’paul@hackingwithswift.com’”,最后 “追加 ’。’”,每一个部分触发一个方法调用,并添加到咱们的字符串中。

动态可调用(dynamicCallable)类型

SE-0216 为 Swift 添加了一个新的 @dynamicCallable 注解,它让一个类型能被直接调用。它是语法糖,而不是任何类型的编译器的魔法,它把如下这段代码:

let result = random(numberOfZeroes: 3)
复制代码

转换为:

let result = random.dynamicallyCall(withKeywordArguments: ["numberOfZeroes": 3])
复制代码

以前有一篇关于 Swift 中的动态特性 的文章里有提到了动态查找成员(@dynamicMemberLookup)。@dynamicCallable@dynamicMemberLookup 的天然扩展,它能使 Swift 代码更容易与 Python 和 JavaScript 等动态语言一块儿工做。

要将此功能添加到你本身的类型,你须要添加 @dynamicCallable 注解以及这些方法中的一个或两个:

func dynamicallyCall(withArguments args: [Int]) -> Double

func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double
复制代码

第一个用于调用不带参数标签的类型(例如 a(b, c)),第二个用于你 提供标签 时(例如 a(b: cat, c: dog))。

@dynamicCallable 对于其方法接受和返回的数据类型很是灵活,它使你能够从 Swift 的全部类型安全性中受益,同时具备一些高级用法。所以,对于第一种方法(无参数标签),你可使用遵循了 ExpressibleByArrayLiteral 的任何东西,例如数组、数组切片和集合,对于第二种方法(使用参数标签),你可使用遵循 ExpressibleByDictionaryLiteral 的任何东西。例如字典和键值对。

除了接受各类输入外,你还能够为各类输出提供多个重载,可能返回一个字符串、整数等等。只要 Swift 能推出使用哪个,你就能够混合搭配你想要的一切。

咱们来看一个例子。首先,这是一个 RandomNumberGenerator 结构体,它根据传入的输入生成介于 0 和某个最大值之间的数字:

struct RandomNumberGenerator {
    func generate(numberOfZeroes: Int) -> Double {
        let maximum = pow(10, Double(numberOfZeroes))
        return Double.random(in: 0...maximum)
    }
}
复制代码

要把它切换到 @dynamicCallable,咱们会写这样的代码:

@dynamicCallable
struct RandomNumberGenerator {
    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)
    }
}
复制代码

你能够传任意数量的参数甚至不传参数来调用该方法,所以咱们当心读取第一个值并结合是否为 nil 的判断来确保存在合理的默认值。

咱们如今能够建立一个 RandomNumberGenerator 实例并像函数同样调用它:

let random = RandomNumberGenerator()
let result = random(numberOfZeroes: 0)
复制代码

若是你曾经使用过 dynamicallyCall(withArguments:),或者同时使用,由于你可让它们都是单一类型,就能够写如下代码:

@dynamicCallable
struct RandomNumberGenerator {
    func dynamicallyCall(withArguments args: [Int]) -> Double {
        let numberOfZeroes = Double(args[0])
        let maximum = pow(10, numberOfZeroes)
        return Double.random(in: 0...maximum)
    }
}

let random = RandomNumberGenerator()
let result = random(0)
复制代码

使用 @dynamicCallable 时须要注意一些重要的规则:

  • 你能够将其应用于结构体、枚举、类和协议。
  • 若是你实现了 withKeywordArguments: 而且没有实现 withArguments:,你的类型仍然能够在没有参数标签的状况下调用,你只须要为键得到空字符串。
  • 若是 withKeywordArguments:withArguments: 的实现被标记为 throw,则调用该类型也将可抛出。
  • 你不能把 @dynamicCallable 添加到 extension 里,只可在类的主体里面添加。
  • 你仍然能够为你的类型添加其余方法和属性,并照常使用它们。

也许更重要的是,不支持方法决议,这意味着咱们必须直接调用类型(例如 random(numberOfZeroes: 5))而不是调用类型上的特定方法(例如 random.generate(numberOfZeroes: 5))。已经有一些关于使用方法签名添加后者的讨论,例如:

func dynamicallyCallMethod(named: String, withKeywordArguments: KeyValuePairs<String, Int>)
复制代码

若是那在将来的 Swift 版本中可能实现,它可能会为 test mock 创造出一些很是有趣的可能性。

与此同时 @dynamicCallable 不太可能广受欢迎,但对于但愿与 Python,JavaScript 和其余语言交互的少数人来讲,它很是重要。

面向将来的枚举 case

SE-0192 增长了在固定的枚举和可能将被改变的枚举间的区分度。

Swift 的一个安全特性是它要求全部 switch 语句都是详尽的,它们必须覆盖全部状况。虽然这从安全角度来看效果很好,可是在未来添加新案例时会致使兼容性问题:系统框架可能会发送你未提供的不一样内容,或者你依赖的代码可能会添加新案例并致使你的编译中断,由于你的 switch 再也不详尽。

使用 @unknown 注解,咱们如今能够区分两个略有不一样的场景:“这个默认状况应该针对全部其余状况运行,由于我不想单独处理它们” 和 “我想单独处理全部状况,但若是未来出现任何问题,请使用此而非报错。”

如下是一个枚举示例:

enum PasswordError: Error {
    case short
    case obvious
    case simple
}
复制代码

咱们可使用 switch 编写代码来处理每一个案例:

func showOld(error: PasswordError) {
    switch error {
    case .short:
        print("Your password was too short.")
    case .obvious:
        print("Your password was too obvious.")
    default:
        print("Your password was too simple.")
    }
}
复制代码

对于短密码和弱强度密码,它使用两个 case,但将第三种状况将会到 default 中处理。

如今若是未来咱们在 enum 中添加了一个名为 old 的新 case,对于之前使用过的密码,咱们的 default case 会被自动调用,即便它的消息没有意义。

Swift 没法向咱们发出有关此代码的警告,由于它在语法上没有问题,所以很容易错过这个错误。幸运的是,新的 @unknown 注解完美地修复了它,它只能用于 default 状况,而且设计为在未来出现新案例时能够运行。

例如:

func showNew(error: PasswordError) {
    switch error {
    case .short:
        print("Your password was too short.")
    case .obvious:
        print("Your password was too obvious.")
    @unknown default:
        print("Your password wasn't suitable.")
    }
}
复制代码

该代码如今将产生警告,由于 switch 块再也不详尽,Swift 是但愿咱们明确处理每一个 case 的。实际上这只是一个 警告,这使得这个属性很实用:若是一个框架在将来添加一个新 case,你将获得警告,但它不会让你的代码编译不经过。

try? 嵌套可选的展平

SE-0230 修改 try? 的工做方式,以便嵌套的可选项被展平成为一个常规的选择。这使得它的工做方式与可选链和条件类型转换(if let)的工做方式相同,这两种方法都在早期的 Swift 版本中展平了可选项。

这是一个演示变化的示例:

struct User {
    var id: Int

    init?(id: Int) {
        if id < 1 {
            return nil
        }

        self.id = id
    }

    func getMessages() throws -> String {
        // 复杂的一段代码
        return "No messages"
    }
}

let user = User(id: 1)
let messages = try? user?.getMessages()
复制代码

User 结构体有一个可用的初始化器,由于咱们想确保开发者建立具备有效 ID 的用户。getMessages() 方法理论上包含某种复杂的代码来获取用户的全部消息列表,所以它被标记为 throws,我已经让它返回一个固定的字符串,因此代码可编译经过。

关键在于最后一行:由于用户是可选的而使用可选链,由于 getMessages() 能够抛出错误,它使用 try? 将 throw 方法转换为可选的,因此咱们最终获得一个嵌套的可选。在 Swift 4.2 和更早版本中,这将使 messages 成为 String??,一个可选的可选字符串,可是在 Swift 5.0 和更高版本中 try? 若是对于已是可选的类型,它们不会将值包装成可选类型,因此 messages 将只是一个 String?

此新行为与可选链和条件类型转换(if let)的现有行为相匹配。也就是说,若是你须要的话,能够在一行代码中使用可选链十几回,但最终不会有那么多个嵌套的可选。相似地,若是你使用 as? 的可选链,你仍然只有一个级别的可选性,而这一般是你想要的。

Integer 整倍数自省

SE-0225 为整数添加 isMultiple(of:) 方法来容许咱们以比使用取余数运算 % 更清晰的方式检查一个数是不是另外一个数的倍数。

例如:

let rowNumber = 4

if rowNumber.isMultiple(of: 2) {
    print("Even")
} else {
    print("Odd")
}
复制代码

没错,咱们可使用 if rowNumber % 2 == 0 实现相同的功能,但你不得不认可这样看起来不清晰,使用 isMultiple(of:) 意味着它能够在 Xcode 的代码自动补全中列出,这有助于你发现。

使用 compactMapValues() 转换和解包字典值

SE-0218 为字典添加了一个新的 compactMapValues() 方法,它可以将数组中的 compactMap() 功能转换我须要的值,解包结果,而后丢弃任何 nil,与字典中的 mapValues() 方法一块儿使用能保持键的完整并只转换值。

举个例子,这里是一个比赛数据的字典,以及他们完成的秒数。其中有一我的没有完成,标记为 “DNF”(未完成):

let times = [
    "Hudson": "38",
    "Clarke": "42",
    "Robinson": "35",
    "Hartis": "DNF"
]
复制代码

咱们可使用 compactMapValues() 建立一个名字和时间为整数的新字典,删除一个 DNF 的人:

let finishers1 = times.compactMapValues { Int($0) }
复制代码

或者你能够直接将 Int 初始化器传递给 compactMapValues(),以下所示:

let finishers2 = times.compactMapValues(Int.init)
复制代码

你还可使用 compactMapValues() 来展开选项并丢弃 nil 值而不执行任何类型转换,以下所示:

let people = [
    "Paul": 38,
    "Sophie": 8,
    "Charlotte": 5,
    "William": nil
]

let knownAges = people.compactMapValues { $0 }
复制代码

被移除的特性:计算序列中的匹配项

这个 Swift 5.0 功能在 beta 版中被撤销,由于它致使了类型检查器的性能问题。但愿它可以在 Swift 5.1 回归,或者用一个新名称来避免问题。

SE-0220 引入了一个新的 count(where:) 方法,该方法执行 filter() 的等价方法并在一次传递中计数。这样能够节省当即丢弃的新阵列的建立,并为常见问题提供清晰简洁的解决方案。

此示例建立一个测试结果数组,并计算大于或等于 85 的数的个数:

let scores = [100, 80, 85]
let passCount = scores.count { $0 >= 85 }
复制代码

这计算了数组中有多少名称以 “Terry” 开头:

let pythons = ["Eric Idle", "Graham Chapman", "John Cleese", "Michael Palin", "Terry Gilliam", "Terry Jones"]
let terryCount = pythons.count { $0.hasPrefix("Terry") }
复制代码

全部遵循 Sequence 的类型均可以使用此方法,所以你也能够在集合和字典上使用它。

接下来干吗?

Swift 5.0 是 Swift 的最新版本,但以前的版本也包含了不少功能。你能够阅读如下文章:

但还有更多,苹果已经在 Swift.org 上宣布了 Swift 5.1发布流程,其中包括模块稳定性以及其余一些改进。在撰写本文时,5.1 的附加条款不多,但看起来咱们会看到它在 WWDC 附近发布。

若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索