Swift 5.9 有哪些新特性(二)

news2024/12/23 0:48:13

在这里插入图片描述

文章目录

    • 前言
    • Noncopyable 结构体和枚举
    • 结束变量绑定的生命周期
    • makeStream() 方法
    • 添加 sleep(for:) 到 Clock
    • Discarding task groups
    • 总结

在这里插入图片描述

前言

虽然 Swift 6 已经在地平线上浮现,但 5.x 版本仍然有很多新功能-更简单的 if 和 switch 用法、宏、非可复制类型、自定义 actor 执行器等等都将在 Swift 5.9 中推出,再次带来了一个巨大的更新。

在本文中,将介绍这个版本中最重要的变化,提供代码示例和解释,以便可以自行尝试。需要在 Xcode 14 中安装最新的 Swift 5.9 工具链,或者使用 Xcode 15 beta。

Noncopyable 结构体和枚举

SE-0390 引入了无法复制的结构体和枚举的概念,从而允许在代码的多个位置共享一个结构体或枚举的单个实例,虽然只有一个所有者,但现在可以在代码的不同部分访问。

首先,此更改引入了用于取消要求的新语法:~Copyable。这意味着 “此类型不能被复制”,并且此取消语法目前在其他地方不可用 - 例如,我们不能使用 ~Equatable 来退出类型的 ==

因此,我们可以像下面代码创建一个新的不可复制的 User 结构体:

struct User: ~Copyable {
    var name: String
}

注意:Noncopyable 不能满足除 Sendable 之外的任何协议。

一旦创建了 User 实例,其不可复制的特性意味着它与 Swift 的先前版本不一样。例如,下面的示例代码:

func createUser() {
    let newUser = User(name: "Anonymous")

    var userCopy = newUser
    print(userCopy.name)
}

createUser()

但是我们已经声明了 User 结构体为不可复制,也无法复制 newUser,将 newUser 分配给 userCopy 导致原始的 newUser 值被消耗,这意味着不能使用,因为所有权现在属于 userCopy。如果尝试将 print(userCopy.name) 更改为 print(newUser.name),Swift 会抛出一个编译器错误。

新的限制还适用于如何将非可复制类型用作函数参数:SE-0377 规定函数必须明确指定是打算消费值并在函数完成后使其在调用点无效,还是希望借用值以便与代码中的其他借用部分同时读取其数据。

因此,可以编写一个函数来创建用户,另一个函数来借用用户以获得只读访问其数据的权限:

func createAndGreetUser() {
    let newUser = User(name: "Anonymous")
    greet(newUser)
    print("Goodbye, \(newUser.name)")
}

func greet(_ user: borrowing User) {
    print("Hello, \(user.name)!")
}

createAndGreetUser()

与此相反,如果我们使 greet() 函数使用 consuming User,则 print("Goodbye, \(newUser.name)") 将不被允许 - Swift 将认为 greet() 运行后,newUser 值将无效。另一方面,由于 consuming 方法必须结束对象的生命周期,可以自由地修改其属性。

这种共享行为赋予了非可复制结构体以前仅限于类和 actor 的超能力:当对非可复制实例的最后一个引用被销毁时,可以提供自动运行的析构函数。

重要提示: 这与类上的析构函数的行为略有不同,可能是早期实现的问题或有意为之。

首先,下面是使用类的析构函数的代码示例:

class Movie {
    var name: String

    init(name: String) {
        self.name = name
    }

    deinit {
        print("\(name) is no longer available")
    }
}

func watchMovie() {
    let movie = Movie(name: "The Hunt for Red October")
    print("Watching \(movie.name)")
}

watchMovie()

当运行该代码时,会先打印 “Watching The Hunt for Red October”,然后打印 “The Hunt for Red October is no longer available”。但是,如果将类型的定义从 class Movie 更改为 struct Movie: ~Copyable,将会看到这两个 print() 语句以相反的顺序运行 - 先说电影不再可用,然后说正在观看。

非可复制类型内部的方法默认情况下是借用的,但是可以像可复制类型一样标记为 mutating,并且还可以标记为 consuming,表示该值在方法运行后无效。

例如,我们熟悉的电影和电视剧《碟中谍》,秘密特工们通过一卷只能播放一次的自毁磁带获得任务指令。对于这样的方式,非可复制结构体非常适合:

struct MissionImpossibleMessage: ~Copyable {
    private var message: String

    init(message: String) {
        self.message = message
    }

    consuming func read() {
        print(message)
    }

这样标记的 message 本身是私有的,因此只能通过调用消费实例的 read() 方法来访问它。

与变异方法不同,消费方法可以在类型的常量实例上运行。因此,像下面这样的代码是可以的:

func createMessage() {
    let message = MissionImpossibleMessage(message: "You need to abseil down a skyscraper for some reason.")
    message.read()
}

createMessage()

注意: 因为 message.read() 消费了 message 实例,所以尝试第二次调用 message.read() 将会报错。

与析构函数结合使用时,消费方法会使清理工作重复执行。例如,如果在游戏中跟踪高分,可能希望具有一个消费的 finalize() 方法,将最新的高分写入永久存储,并阻止其他人进一步更改分数,但在对象销毁时也保存最新的分数到磁盘。

为了避免这个问题,Swift 5.9 引入了一个新的 discard 操作符,可以用于非可复制类型的消费方法。在消费方法中使用 discard self 可以阻止该对象的析构函数运行。

因此,可以像这样实现 HighScore 结构:

struct HighScore: ~Copyable {
    var value = 0

    consuming func finalize() {
        print("Saving score to disk…")
        discard self
    }

    deinit {
        print("Deinit is saving score to disk…")
    }
}

func createHighScore() {
    var highScore = HighScore()
    highScore.value = 20
    highScore.finalize()
}

createHighScore()

提示: 当运行该代码时,你会看到 deinitializer 消息被打印两次 - 一次是在更改 value 属性时,实际上销毁并重新创建了结构体,一次是在 createHighScore() 方法结束时。

在使用这个新功能时,还有一些额外的复杂性需要注意:

  • 类和 actor 不能是非可复制的。
  • 非可复制类型暂时不支持泛型,这排除了可选的非可复制对象和非可复制对象数组。
  • 如果在另一个结构体或枚举类型中将非可复制类型用作属性,那么父结构体或枚举类型也必须是非可复制的。
  • 当对现有类型添加或移除 Copyable 时需要非常小心,因为会改变用法。如果在库中发布代码,这将破坏 ABI。

结束变量绑定的生命周期

使用消耗运算符结束变量绑定的生命周期

SE-0366 扩展了对可复制类型的局部变量和常量的消耗值概念,这对于希望避免在其数据传递过程中发生不必要的保留/释放调用的开发人员可能很有益处。

最简单的形式下,消耗运算符如下所示:

struct User {
    var name: String
}

func createUser() {
    let newUser = User(name: "Anonymous")
    let userCopy = consume newUser
    print(userCopy.name)
}

createUser()

其中重要的是 let userCopy 这一行,同时执行两个操作:

  1. newUser 的值复制到 userCopy 中。
  2. 结束 newUser 的生命周期,因此任何进一步访问它的尝试都会引发错误。

这样可以明确告诉编译器“不允许再次使用这个值”,这将代表强制执行这个规则。

可以看到这在使用所谓的黑洞 _ 时特别常见,我们不希望复制数据,而只是想将其标记为已销毁,例如:

func consumeUser() {
    let newUser = User(name: "Anonymous")
    _ = consume newUser
}

实际上,最常见的情况可能是将值传递给如下的函数:

func createAndProcessUser() {
    let newUser = User(name: "Anonymous")
    process(user: consume newUser)
}

func process(user: User) {
    print("Processing \(name)…")
}

createAndProcessUser()

有两件特别值得了解的事情。

首先,Swift 跟踪代码的哪些分支消耗了值,并有条件地强制执行规则。因此,在这段代码中,两种可能性中只有一种消耗了 User 实例:

func greetRandomly() {
    let user = User(name: "Taylor Swift")

    if Bool.random() {
        let userCopy = consume user
        print("Hello, \(userCopy.name)")
    } else {
        print("Greetings, \(user.name)")
    }
}

greetRandomly()

其次,严格来说,consume 操作符作用于绑定而不是值。实践中,这意味着如果使用一个变量进行消耗,可以重新初始化该变量并正常使用:

func createThenRecreate() {
    var user = User(name: "Roy Kent")
    _ = consume user

    user = User(name: "Jamie Tartt")
    print(user.name)
}

createThenRecreate()

makeStream() 方法

SE-0388 在 AsyncStreamAsyncThrowingStream 中添加了一个新的 makeStream() 方法,返回流本身以及其 continuation。

因此,不再需要编写以下代码:

var continuation: AsyncStream<String>.Continuation!
let stream = AsyncStream<String> { continuation = $0 }

现在可以同时获取:

let (stream, continuation) = AsyncStream.makeStream(of: String.self)

这在需要在当前上下文之外访问 continuation 的地方特别方便,例如在另一个方法中。例如,以前可能会像下面这样编写一个简单的数字生成器,需要将 continuation 存储为自己的属性,以便能够从 queueWork() 方法中调用:

struct OldNumberGenerator {
    private var continuation: AsyncStream<Int>.Continuation!
    var stream: AsyncStream<Int>!

    init() {
        stream = AsyncStream(Int.self) { continuation in
            self.continuation = continuation
        }
    }

    func queueWork() {
        Task {
            for i in 1...10 {
                try await Task.sleep(for: .seconds(1))
                continuation.yield(i)
            }

            continuation.finish()
        }
    }
}

使用新的 makeStream(of:) 方法,这段代码变得简单多了:

struct NewNumberGenerator {
    let (stream, continuation) = AsyncStream.makeStream(of: Int.self)

    func queueWork() {
        Task {
            for i in 1...10 {
                try await Task.sleep(for: .seconds(1))
                continuation.yield(i)
            }

            continuation.finish()
        }
    }
}

添加 sleep(for:) 到 Clock

SE-0374 在 Swift 的 Clock 协议中添加了一个新的扩展方法,允许暂停执行一段时间,同时还支持特定容差的基于持续时间的任务睡眠。

Clock 的更改虽然很小,但非常重要,特别是在模拟具体 Clock 实例以消除在测试中存在于生产环境中的延迟时。

例如,可以使用任何类型的 Clock 创建这个类,并在触发保存操作之前使用该 Clock 进行睡眠:

class DataController: ObservableObject {
    var clock: any Clock<Duration>

    init(clock: any Clock<Duration>) {
        self.clock = clock
    }

    func delayedSave() async throws {
        try await clock.sleep(for: .seconds(1))
        print("Saving…")
    }
}

由于使用了 any Clock<Duration>,因此在生产中可以使用 ContinuousClock,而在测试中可以使用自定义的 DummyClock,其中忽略所有的 sleep() 命令以使测试运行快速。

在较旧的 Swift 版本中,相应的代码理论上可能是 try await clock.sleep(until: clock.now.advanced(by: .seconds(1))),但在这个示例中不起作用,因为 Swift 不知道具体使用了哪种类型的时钟,因此无法获得 clock.now

至于对于 Task 睡眠的改变,可以从以下代码:

try await Task.sleep(until: .now + .seconds(1), tolerance: .seconds(0.5))

简化为:

try await Task.sleep(for: .seconds(1), tolerance: .seconds(0.5))

Discarding task groups

SE-0381 添加了新的 Discarding task groups,填补了当前 API 中的一个重要空白:在任务组内部创建的任务在完成后会自动丢弃和销毁,这意味着长时间运行的任务组(或者在 Web 服务器等情况下可能一直运行的任务组)不会随着时间的推移泄漏内存。

在使用原始的 withTaskGroup() API 时,可能会遇到问题,因为 Swift 只在调用 next() 或循环遍历任务组的子任务时才丢弃子任务及其结果数据。调用 next() 会导致代码在所有子任务都在执行时暂停,因此面临的问题是:希望服务器始终监听连接以便添加任务来处理,但是还需要定期停止以清理已完成的旧任务。

在 Swift 5.9 中引入了解决这个问题的清晰方案,添加了 withDiscardingTaskGroup()withThrowingDiscardingTaskGroup() 函数,用于创建新的丢弃式任务组。这些任务组会自动在每个任务完成后丢弃和销毁任务,无需手动调用 next() 来消费它。

为了了解触发问题的情况,可以实现一个简单的目录监视器,循环运行并报告已添加或删除的文件或目录的名称:

struct FileWatcher {
    // 正在监视文件更改的 URL。
    let url: URL

    // 已返回的 URL 集合。
    private var handled = Set<URL>()

    init(url: URL) {
        self.url = url
    }

    mutating func next() async throws -> URL? {
        while true {
            // 读取我们目录的最新内容,或者如果发生问题则退出。
            guard let contents = try? FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) else {
                return nil
            }

            // 找出我们尚未处理的 URL。
            let unhandled = handled.symmetricDifference(contents)

            if let newURL = unhandled.first {
                // 如果我们已经处理过此 URL,则它必须已被删除。
                if handled.contains(newURL) {
                    handled.remove(newURL)
                } else {
                    // 否则,此 URL 是新的,因此将其标记为已处理。
                    handled.insert(newURL)
                    return newURL
                }
            } else {
                // 没有文件差异;睡眠几秒钟后重试。
                try await Task.sleep(for: .microseconds(1000))
            }
        }
    }
}

然后可以从一个简单的应用程序中使用,尽管出于简洁起见,只打印 URL 而不进行任何复杂的处理:

struct FileProcessor {
    static func main() async throws {
        var watcher = FileWatcher(url: URL(filePath: "/Users/twostraws"))

        try await withThrowingTaskGroup(of: Void.self) { group in
            while let newURL = try await watcher.next() {
                group.addTask {
                    process(newURL)
                }
            }
        }
    }

    static func process(_ url: URL) {
        print("Processing \(url.path())")
    }
}

这段代码将永远运行,或者至少直到用户终止程序或监视的目录不再可访问为止。然而,由于使用了 withThrowingDiscardingTaskGroup(),这个问题就不存在了:每次调用 addTask() 时都会创建一个新的子任务,但由于没有在任何地方调用 group.next(),这些子任务永远不会被销毁。每次可能只增加几百字节,这段代码将占用越来越多的内存,直到最终操作系统耗尽内存并被迫终止程序。

这个问题在 Discarding task groups 中完全消失:只需将 withThrowingTaskGroup(of: Void.self) 替换为 withThrowingDiscardingTaskGroup,每个子任务在完成工作后将自动销毁。

总结

特别感谢 Swift社区 编辑部的每一位编辑,感谢大家的辛苦付出,为 Swift社区 提供优质内容,为 Swift 语言的发展贡献自己的力量。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/699036.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

(二十四)专题地图编制——制图数据操作

专题地图编制——制图数据操作 目录 专题地图编制——制图数据操作 1.复制地图数据框2.旋转制图数据框3.绘制坐标网格3.1经纬网3.2方里格网3.3参考格网 1.复制地图数据框 要复制地图数据框&#xff0c;请按照以下步骤操作&#xff1a; ①在 ArcMap 中打开地图文档。 ②在“表…

图片怎么转换成PDF格式?学会这些方法轻松转换

将多张图片合并成一个PDF文件&#xff0c;方便用户阅读和管理&#xff0c;避免了多个图片文件的混乱。并且PDF文件可以设置密码保护&#xff0c;避免图片被未经授权的人员查看、复制和编辑。此外&#xff0c;PDF文件可以设置禁止打印和禁止复制等权限&#xff0c;增强图片的安全…

mysql exists深入理解

前言&#xff1a; 某天用exists语句时发现跟我的理解有出入&#xff0c;我以前理解的exists是子查询的结果不会影响最终返回的结果&#xff0c;只要子查询查到有结果&#xff0c;则返回true,没有则返回false。即下面图中的sql&#xff0c;只要house_appraisal不是空表&#xf…

Android中实现Material3主题

Android中实现Material3主题 Material 3是由Google引入的一种设计系统&#xff0c;通过采用一套设计原则、指南和组件&#xff0c;提供统一直观的用户体验。 在本篇文章中&#xff0c;您将学习如何&#xff1a; 在您的Android应用程序中应用Material 3主题。如何使用Materia…

10.1寸工业三防平板应用于工业自动化生产

随着工业自动化的不断发展&#xff0c;工业生产中对于设备的要求也越来越高。在恶劣的工作环境中&#xff0c;工业设备需要具备防尘、防水、防震等功能&#xff0c;以确保设备的稳定运行和长期使用。10.1寸工业三防平板作为一种重要的工业自动化设备&#xff0c;广泛应用于各个…

【算法】算法效率分析 -- 时间空间复杂度

文章目录 概述时间复杂度常数阶 O ( 1 ) O(1) O(1)线性阶 O ( n ) O(n) O(n)对数阶 O ( l o g n ) O(logn) O(logn)线性对数阶 O ( n ∗ l o g n ) O(n*logn) O(n∗logn)次方阶 O ( n 2 ) O(n^2) O(n2) O ( n 3 ) O(n^3) O(n3) O ( n k ) O(n^k) O(nk) O ( n m ) O(nm) …

保持无损连接的BCNF分解算法

建议在看之前熟悉候选键的求法&#xff0c;不清楚的可以转到这里来&#xff1a; http://t.csdn.cn/fW30Q 步骤&#xff1a; INPUT:关系模式R以及在R上成立的函数依赖集F 1.初始化P{R} 2.若P中的所有关系模式S都是BCNF&#xff0c;则转步骤(4) 3.若P中有一个模式S不是BCNF&am…

Linux——IP协议2

目录 公网IP ​编辑 特殊的IP地址 IP地址的数量限制 私有IP地址和公网IP地址 路由 数据链路层 认识以太网 以太网帧格式 如何解包和封装&#xff0c;交付及分用 重谈局域网通信原理 认识MTU MTU对于TCP协议的影响 查看硬件地址和MTU ARP协议 模拟ARP请求 …

ros2内结合gazebo和rviz进行yolov8检测记录

前提&#xff1a;第一次接触ros2, 遇到的问题解决方式不一定准确&#xff0c;只是这次我尝试成功了&#xff0c;想和大家分享一下。 ubuntu20.04系统 目录 1. ros2 1.1 ros2是啥&#xff1f; 1.2 ros2的版本和ubuntu版本的对应关系&#xff0c;当下入门尤其是ubuntu20.04系…

【物理摩擦力图像】对摩擦力大小的因素研究图像

摩擦力与编程 两个相互接触并挤压的物体&#xff0c;当它们发生相对运动或具有相对运动趋势时&#xff0c;就会在接触面上产生阻碍相对运动或相对运动趋势的力&#xff0c;这种力叫做摩擦力&#xff08;Ff或f&#xff09;。 摩擦力与正压力&#xff08;B物体上的A物体产生的压…

数据库实验—复杂查询

查询20161151班的学生在大学一年级选修的课程情况&#xff0c;查询结果要显示学号(Sno)、姓名(Sname)、专业名(Mname)、选课的课程号(Cno)、选课的课程名称(Cname)及成绩&#xff08;Grade)&#xff0c;并按照学号、课程号升序排序 select Sno, Sname,Mname, Cno,Cname,Grade f…

Windows11安装WSL2(Ubuntu20.04)

以管理员身份打开 PowerShell&#xff0c;输入以下命令安装&#xff1a; wsl --install dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /no…

【历史上的今天】6 月 29 日:SGI 和 MIPS 合并;微软收购 PowerPoint 开发商;新闻集团出售 Myspace

整理 | 王启隆 透过「历史上的今天」&#xff0c;从过去看未来&#xff0c;从现在亦可以改变未来。 今天是 2023 年 6 月 29 日&#xff0c;在 2008 年的今天&#xff0c;CNNIC 发布《第 22 次中国互联网络发展状况统计报告》&#xff0c;截至 2008 年 6 月底&#xff0c;中国…

draggable里包裹的卡片,卡片里有个input,点击input聚焦无效。

在input标签上加pointerdown.stop.native <el-input placeholder"请输入" pointerdown.stop.native v-model"dataForm.nickName" :style"{width:180px}" suffix-icon"el-icon-search" lazy />

TLD7002学习笔记(三)-使用S32K144EVB烧录TLD7002

文章目录 1. 前言2. 烧录数据准备2.1 OTP Wizard的下载与安装2.2 OTP Wizard的配置2.3 OTP寄存器烧录数据提取 3. OTP烧录和仿真的流程3.1 OTP烧录流程3.2 OTP仿真流程 4. 验证测试4.1 测试代码4.2 测试环境4.3 测试情况 5. 参考资料 1. 前言 本篇文章是TLD7002学习笔记的第三…

设计模式第18讲——中介者模式

目录 一、什么是中介者模式 二、角色组成 三、优缺点 四、应用场景 4.1 生活场景 4.2 java场景 五、代码实现 5.0 代码结构 5.1 抽象中介者&#xff08;Mediator&#xff09;——LogisticsCenter 5.2 抽象同事类&#xff08;Colleague&#xff09;——Participant 5…

Beego之Bee安装(Windows)以及创建,运行项目

一.简介 Bee是什么&#xff1f; bee工具是一个为了协助快速开发 Beego 项目而创建的项目&#xff0c;通过 bee 可以很容易的进行 Beego 项目的 创建、热编译、开发、测试和部署 Beego中文文档 Beego中文文档: Beego简介 安装前提 在安装bee之前&#xff0c;首先得提前安装好Go的…

【T3】打开财务报表提示不能登陆到服务器,请检查服务器配置。

【问题描述】 在使用畅捷通T3软件的时候&#xff0c; 打开【财务报表】提示&#xff1a;不能登陆到服务器&#xff08;GUPR7FM&#xff09;&#xff0c;请检查服务器配置。 但是打开【总账系统】&#xff0c;填制凭证、查看报表等操作都正常。 【解决方法】 由于操作【总账系…

【Java高级编程】Java反射机制

Java反射机制 1、反射的概述1.1、本章的主要内容1.2、关于反射的理解1.3、体会反射机制的“动态性”1.4、反射机制能提供的功能1.5、相关API 2、Class类的理解与获取Class的实例2.1、Class类的理解2.2、获取Class实例的几种方式2.3、总结&#xff1a;创建类的对象的方式2.4、Cl…

生产者与消费者问题

本篇文章我们使用C探讨一下生产者与消费者问题. 1. 多线程的引入 我们学习了操作系统, 知道了进程和线程的概念, 但是如果不进行代码实战的话, 会很难理解它们. 特别是编程的初学者(比如我), 在了解了进程和线程后通常会感到疑惑: 多线程怎么用? 为啥我平时写代码没有使用到…