译自 www.hackingwithswift.com/articles/24…
更多内容,欢迎重视大众号「Swift花园」

一切协议类型支撑 Existentials

SE-0309 使得 Swift 放宽了把具有 Self 或相关类型的协议用作类型的禁令,转向仅基于特定特点或办法不受束缚的模型。

译注:相关把协议用作类型的禁令,大家对下面这个无情的提示应该都不陌生:

Protocol ‘XXX’ can only be used as generic constraint because it has Self or associated type requirements

简略来说,这意味着以下代码变得合法:

let firstName: any Equatable = "Paul"
let lastName: any Equatable = "Hudson"

Equatable 是一个有 Self 要求的协议,这意味着它供给的功用引证了特定的遵从该协议的类型。例如,Int 遵从 Equatable,所以当咱们说 4 == 4 时,实际上是在运转一个承受两个整数并在它们匹配时回来 true 的函数。

咱们当然能够运用类似于 func ==(first: Int, second: Int) -> Bool 的函数来完成这个功用,但这不能很好地扩展 —— 咱们得编写许多这样的函数来处理布尔值、字符串、数组等。因而,Equatable 协议有这样的要求:func ==(lhs: Self, rhs: Self) -> Bool。这意味着需求能够承受两个相同类型的实例。

在 Swift 5.7 之前,只需 Self 出现在协议中,编译器就不答应咱们像下面这样运用协议:

let tvShow: [any Equatable] = ["Brooklyn", 99]

从 Swift 5.7 开端,上面的代码答应的。束缚被转移到咱们尝试在 Swift 实际执行类型束缚的地方。所以这儿不能firstName == lastName 。由于之前说到, == 有必要保证它有两个相同类型的实例才干作业。经过运用 any Equatable ,咱们隐藏了数据的切当类型。由此获得的是对咱们的数据进行运转时检查的才能,以清晰咱们正在处理的内容。

比方针对混合数组,咱们能够这样写:

for parts in tvShow {
    if let item = item as? String {
        print("Found string: \(item)")
    } else if let item = item as? Int {
        print("Found integer: \(item)")
    }
}

或许是两个字符串:

if let firstName = firstName as? String, let lastName = lastName as? String {
    print(firstName == lastName)
}

了解这个改动的关键在于记住它使得咱们能够更自由地运用协议,而不用了解类型内部的作业。

例如,咱们能够编写代码来检查恣意序列中的一切项目是否契合 Identifiable 协议:

func canBeIdentified(_ input: any Sequence) -> Bool {
    input.allSatisfy { $0 is any Identifiable }
}

Primary Associated Type 的轻量级同类型要求

SE-0346 增加了更新、更简略的语法来引证具有特定相关类型的协议。

例如,假设咱们要编写代码以不同的办法缓存不同类型的数据,咱们或许会像下面这样做:

protocol Cache<Content> {
    associatedtype Content
    var items: [Content] { get set }
    init(items: [Content])
    mutating func add(item: Content)
}

注意,这个协议现在看起来既是一个协议,又是一个泛型类型。

括号中的部分是 Swift 的 primary associated type,之所以冠以 primary, 是由于并不是一切的相关类型都应该在那里声明。相反,应该只列出调用代码最关怀的那些类型,例如字典的键和值的类型或许 Identifiable 协议中的标识符类型。在咱们的例子中,缓存的内容 —— 字符串、图像、用户等便是它的首要相关类型。

至此,咱们能够像曾经相同继续运用协议 —— 创立某种咱们想要缓存的数据,然后创立一个遵从协议的详细缓存类型,如下所示:

struct File {
    let name: String
}
struct LocalFileCache: Cache {
    var items = [File]()
    mutating func add(item: File) {
        items.append(item)
    }
}

新的优点在这儿:当咱们要创立缓存时,咱们本来当然能够直接创立一个特定类型:

func loadDefaultCache() -> LocalFileCache {
    LocalFileCache(items: [])
}

但更为常见的做法应当是像下面这样:

func loadDefaultCacheOld() -> some Cache {
    LocalFileCache(items: [])
}

运用 some Cache 让咱们能够灵敏地改动回来的缓存类型,而 SE-0346 则是在绝对的详细类型和适当模糊的不透明类型之间供给一个折衷方案。为此,咱们能够特型化协议,像下面这样:

func loadDefaultCacheNew() -> some Cache<File> {
    LocalFileCache(items: [])
}

这样一来,咱们既保留了在未来迁移到遵从 Cache 协议的不同类型的才能,又清晰了缓存加载函数处理的内容是文件。

这种更智能的语法也能够拓展到扩展:

extension Cache<File> {
    func clean() {
        print("Deleting all cached files…")
    }
}

以及泛型束缚:

func merge<C: Cache<File>>(_ lhs: C, _ rhs: C) -> C {
    print("Copying all files into a new location…")
    // now send back a new cache with items from both other caches
    return C(items: lhs.items + rhs.items)
}

但最有协助的是 SE-0358 把首要相关类型也引进了 Swift 的标准库,包含 SequenceCollection 等都将获益。

束缚的 existential 类型

SE-0353 供给了把 SE-0309 (“一切协议类型支撑 Existentials ”) 和 SE-0346(“Primary Associated Type 的轻量级同类型要求”) 组合在一起的才能,因而咱们能够运用 any Sequence<String> 这样的写法。

分布式 actor 阻隔

SE-0336 和 SE-0344 引进让 Actor 具备了分布式作业才能 —— 运用长途进程调用 (RPC) 来调用长途办法或许读写特点。

这其间你或许幻想到的杂乱性大致能够被分解为下面三点:

  1. Swift 的 位置透明 机制有效地迫使咱们假定 Actor 是长途的,而实际上咱们无法在编译期确定 Actor 是本地的还是长途的。由于都是运用相同的 await 调用,假设 Actor 恰好是本地的,那么调用将作为常规的本地 Actor 函数处理。
  2. Apple 没有强迫咱们树立自己的 Actor 通讯体系,而是供给了一个 现成的完成 ,背面的理念是 “期望少数成熟的完成最终能够登上舞台”。不过 Swift 中的一切分布式 Actor 特性都是不行知的,不管你是运用哪种通讯体系。
  3. 要从一个分布式 Actor 转移到另一个分布式 Actor,咱们首要是编写distributed actor,然后按需编写distributed func

例如,咱们能够编写下面这样的代码来模拟跟踪卡牌交易体系:

typealias DefaultDistributedActorSystem = ClusterSystem
distributed actor CardCollector {
    var deck: Set<String>
    init(deck: Set<String>) {
        self.deck = deck
    }
    distributed func send(card selected: String, to person: CardCollector) async -> Bool {
        guard deck.contains(selected) else { return false }
        do {
            try await person.transfer(card: selected)
            deck.remove(selected)
            return true
        } catch {
            return false
        }
    }
    distributed func transfer(card: String) {
        deck.insert(card)
    }
}

由于分布式 Actor 调用的抛出特性,假设对 person.transfer(card:) 的调用没有抛出过错,咱们能够确定从一个 collector 中移除卡牌是安全的。

Swift 的方针是让你能够很容易地将你对 Actor 的了解转移到分布式 Actor 上,可是这儿面有一些重要的区别。

首先,一切分布式函数都有必要运用 tryawait 调用,即便该函数没有被标记为 throwing,由于网络调用犯错或许会导致失败。

其次,分布式办法的一切参数和回来值有必要遵从你挑选的序列化进程,例如 Codable。这会在编译期被检查,因而 Swift 能够保证它能够发送和接纳来自长途参与者的数据。

第三,你应该考虑调整你的 Actor API 以最小化数据恳求。例如,假设你想读取分布式参与者的 usernamefirstNamelastName 特点,运用单个办法调用来恳求一切这三个特点,而不是将它们作为独自的特点来恳求,以避免过多的潜在网络传输。

用于成果生成器的 buildPartialBlock

SE-0348 极大地简化了完成杂乱成果生成器所需的重载,正是由于这个特性 Swift 高档正则表达式才成为或许。同时,理论上它还取消了 SwiftUI 的 10 View 束缚。

为了阐明这个特性,请看下面这个 SwiftUI 的 ViewBuilder 简化版别:

@resultBuilder
struct SimpleViewBuilderOld {
    static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View {
        TupleView((c0, c1))
    }
    static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> where C0: View, C1: View, C2: View {
        TupleView((c0, c1, c2))
    }
}

这个成果生成器能够用于函数或许特点:

@SimpleViewBuilderOld func createTextOld() -> some View {
    Text("1")
    Text("2")
    Text("3")
}

buildBlock<C0, C1, C2>() 变体接纳三个 Text 视图,并回来一个包含一切这些的 TupleView。在这个简化的示例中,无法增加第四个 视图,由于咱们没有供给任何重载办法。

新的 buildPartialBlock() 便是为了解决这个问题。它的作业办法类似于序列的 reduce() 办法:有一个初始值,然后经过加上新值来累计。

据此,咱们能够创立一个新的成果生成器,它接纳单个视图,以及如何将该视图与另一个视图组合的 block:

@resultBuilder
struct SimpleViewBuilderNew {
    static func buildPartialBlock<Content>(first content: Content) -> Content where Content: View {
        content
    }
    static func buildPartialBlock<C0, C1>(accumulated: C0, next: C1) -> TupleView<(C0, C1)> where C0: View, C1: View {
        TupleView((accumulated, next))
    }
}

这样一来,咱们就能够按需累加恣意多的视图:

@SimpleViewBuilderNew func createTextNew() -> some View {
    Text("1")
    Text("2")
    Text("3")
}

代码看起来相同,可是类型是不同的:在之前的写法中,咱们将回来一个 TupleView<Text, Text, Text>,而现在的写法咱们将回来一个 TupleView<(TupleView<(Text, Text)>, Text)> —— 一个嵌套的 TupleView

提示: buildPartialBlock() 是 Swift 的语言的一部分,而不是基于特定平台的运转时,所以假设咱们要适配它的话,是能够向后发布到更早的体系版别的。

隐式翻开的 existentials

SE-0352 答应 Swift 在许多情况下运用协议调用泛型函数,以消除曾经存在的一个有点奇怪的妨碍。

例如,这是一个简略的通用函数,它能够处理任何类型的 Numberic 值:

func double<T: Numeric>(_ number: T) -> T {
    number * 2
}

假设咱们直接调用它,例如double(5),那么 Swift 编译器能够挑选 特型化 函数—— 创立一个直接接纳 Int 的版别。这么做是出于性能考量。

而 SE-0352 引进的特性是答应咱们在只知道数据契合协议而不知道详细类型时调用该函数,如下所示:

let first = 1
let second = 2.0
let third: Float = 3
let numbers: [any Numeric] = [first, second, third]
for number in numbers {
    print(double(number))
}

咱们运用的实际数据类型处在一个盒子里,当咱们调用盒子上的办法时,Swift 隐式调用盒子内部数据的办法。 SE-0352 也将同样的才能扩展到了函数调用:咱们循环中的 number 值是 existential 类型(一个包含 IntDoubleFloat 的盒子),但 Swift 能够把盒子里的值发送给泛型 double()函数。

这个特性也有束缚,比方下面的代码是无法经过编译的:

func areEqual<T: Numeric>(_ a: T, _ b: T) -> Bool {
    a == b
}
print(areEqual(numbers[0], numbers[1]))

由于 Swift 无法静态地校验(在编译期)两个值之间能够运用 == 来比较。

Swift snippets

SE-0356 引进了 snippets 的概念,用于填补工程文档中的一个小而重要的空白:当你的示例代码比简略的 API 文档杂乱,又比一个示例项目简略时,能够采用它。它能够展示项目中的一个特定内容。

表面上看,这好像很简略,能够幻想,咱们能够供给演示单个 API 或特定问题解决方案的 snippets,但有三个细节值得注意:

  1. 能够在注释中放置少数特殊标记,以调整 snippets 的出现办法。
  2. 能够从指令行轻松构建和运转它们。
  3. Snippet 和 DocC 完美集成。

特殊标记有两种方式能够运用 //! 注释为每个片段创立简短描绘,而且能够运用 // MARK: Hide// MARK: Show 创立不行见的块代码。幻想咱们需求演示代码,而代码中包含了一段和演示的内容不特别相关的作业。

例如,咱们能够创立一个这样的 Snippet:

//! Demonstrates how to use conditional conformance
//! to add protocols to a data type if it matches
//! some constraint.
struct Queue<Element> {
    private var array = [Element]() 
    // MARK: Hide
    mutating func append(_ element: Element) {
        array.append(element)
    }
    mutating func dequeue() -> Element? {
        guard array.count > 0 else { return nil }
        return array.remove(at: 0)
    }
    // MARK: Show
}
extension Queue: Encodable where Element: Encodable { }
extension Queue: Decodable where Element: Decodable { }
extension Queue: Equatable where Element: Equatable { }
extension Queue: Hashable where Element: Hashable { }
let queue1 = Queue<Int>()
let queue2 = Queue<Int>()
print(queue1 == queue2)

运用 // MARK: Hide// MARK: Show 隐藏一些完成细节,让读者专心于重要的部分。

至于指令行支撑,咱们现在能够运转三个新的指令变体:

  • swift build –build-snippets 构建一切源方针,包含一切片段 # 构建源方针,包含片段。
  • swift build SomeSnippet 将 SomeSnippet.swift 构建为独立的可执行文件。
  • swift run SomeSnippet 当即运转 SomeSnippet.swift。

不行异步特点

SE-0340 经过答应咱们将类型和函数标记为在异步上下文中不行用,部分屏蔽了 Swift 并发模型中的潜在风险情况。

要将某些内容标记为在异步上下文中不行用,需求运用 @available,然后在末尾增加 noasync,例如:

@available(*, noasync)
func doRiskyWork() {
}

在常规的同步函数中,咱们能够正常调用它:

func synchronousCaller() {
    doRiskyWork()
}

假设咱们试图在异步函数中也这么做,Swift 会指出过错:

func asynchronousCaller() async {
    doRiskyWork()
}

这个维护机制是一种改善,但咱们不该过度依赖它。由于它无法防备咱们把调用嵌套到同步函数内部的情况,例如:

func sneakyCaller() async {
    synchronousCaller()
}

上面的代码在异步上下文中运转,但调用同步函数,该函数又能够调用 noasync 函数 doRiskyWork()

所以,运用 noasync 的时候还是需求当心的。不过 Swift Evolution 提案说到 “该特点预计将用于一组适当有限的专门用例” —— 期望咱们用不上这个关键字吧。

封面来自 Marliese Streefland on Unsplash