Swift 5.9 有哪些新特性(二)

这篇具有很好参考价值的文章主要介绍了Swift 5.9 有哪些新特性(二)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Swift 5.9 有哪些新特性(二)


Swift 5.9 有哪些新特性(二)

前言

虽然 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 语言的发展贡献自己的力量。文章来源地址https://www.toymoban.com/news/detail-508011.html

到了这里,关于Swift 5.9 有哪些新特性(二)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Swift的高级语法特性,如可选类型、闭包、枚举、泛型、协议、扩展等

    Swift是一门现代化的编程语言,具有许多高级语法特性,下面我们来逐一介绍。 1. 可选类型(Optional) Swift中的可选类型是一种特殊的类型,它可以表示一个值是存在或不存在的。在声明一个可选类型时,需要在类型名称后面加上一个问号(?)来表示这个类型是可选的。例如

    2024年02月04日
    浏览(30)
  • 06-3_Qt 5.9 C++开发指南_多窗体应用程序的设计(主要的窗体类及其用途;窗体类重要特性设置;多窗口应用程序设计)

    常用的窗体基类是QWidget、QDialog 和QMainWindow,在创建 GUI应用程序时选择窗体基类就是从这 3 个类中选择。QWidget 直接继承于 QObject,是 QDialog 和 QMainWindow 的父类,其他继承于 QWidget 的窗体类还有 QSplashScreen、QMdiSubWindow和QDesktopWidget。另外还有一个类QWindow,它同时从 QObject 和Q

    2024年02月13日
    浏览(72)
  • 掌握哪些测试技术才能说自己已经学成了?

    一、过硬的基础能力 其实所有的测试大佬都是从底层基础开始的,随着时间,经验的积累慢慢变成大佬。要想稳扎稳打在测试行业深耕,成为测试大牛,首当其冲的肯定就是拥有过硬的基础,所有的基础都是根基,后期所有的发展和提升都是基于测试基础铺垫的。 所以核心

    2024年02月07日
    浏览(37)
  • 当年很流行,现在已经淘汰的前端技术有哪些?

    近几年,前端技术真可谓是飞速发展,不断有新的技术涌现,爆火的前端框架 Astro,前端运行时 Bun,构建工具 Vite 等都给前端提供了强大动力。当然,也有很多前端技术随着技术的发展不再需要使用,有了更好的替代方案。本文就来盘点一下那些不再流行的前端技术,以及对

    2024年02月08日
    浏览(43)
  • css3有哪些新特性?(包含哪些模块)

    css3有哪些新特性?包含哪些模块?以下是整理的21个css3新特性: 1.新增选择器 p:nth-child(n){color: rgba(255, 0, 0, 0.75)} 2.新增伪元素 ::before 和 ::after 3.弹性盒模型 display: flex; 4.多列布局 column-count: 5; 5.媒体查询 @media (max-width: 480px) {.box: {column-count: 1;}} 6.个性化字体 @font-face{font-family:

    2024年02月11日
    浏览(36)
  • InnoDB有哪些特性

    事务支持:InnoDB支持ACID(原子性、一致性、隔离性和持久性)事务,可以保证数据的完整性和一致性。它使用多版本并发控制(MVCC)来实现事务的隔离性,支持读已提交和可重复读两种隔离级别。 行级锁定:InnoDB使用行级锁定来实现并发控制,可以提高并发性能。它支持多

    2024年02月14日
    浏览(24)
  • CSS3有哪些新特性

    CSS3引入了很多新特性,比如: 1. 选择器:CSS3引入了新的选择器,如伪类选择器、伪元素选择器等,使得选择元素更加灵活和精确。 2. 边框圆角:CSS3允许通过 border-radius 属性为元素的边框添加圆角,创建圆形、椭圆形或具有不同角度的矩形边框。 3. 盒阴影:使用 box-shadow 属

    2024年02月09日
    浏览(25)
  • 【全面】CSS3新增了哪些新特性?

    目录 一、选择器的扩展 1.  属性选择器 2. 伪类选择器 3. 伪元素选择器 二、盒子模型的增强  1. box-sizing属性 2. 边框圆角(border-radius) 3. 盒阴影(box-shadow) 三、过渡和动画效果 1. 过渡效果 2. 动画效果 四、响应式布局 1. 媒体查询(media query) 2. 弹性布局(Flexbox)

    2024年02月07日
    浏览(29)
  • JDK 新版本中都有哪些新特性?

    JDK 8 推出了 Lambda 表达式、Stream、Optional、新的日期 API 等 JDK 9 中推出了模块化 JDK 10 中推出了本地变量类型推断 JDK 12 中增加了 switch 表达式 JDK 13 中增加了 text block JDK 14 中增加了 Records JDK 15 中增加了封闭类 JDK 17 中扩展了 switch 模式匹配 JDK 19 中增加了协程 Java 8引入了Stream

    2024年02月11日
    浏览(25)
  • es6有哪些新特性?用法和案例

    目录 箭头函数 模板字符串  解构赋值 Promise  async/await 箭头函数使用 = 符号定义,可以更简洁地书写函数并且自动绑定 this 。比如: 箭头函数通常用在回调函数中,例如: 模板字符串是一种新的字符串格式,可以包含变量、表达式和换行符。通过使用占位符 ${} 可以插入变

    2024年02月06日
    浏览(30)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包