What are the new features of Swift 5.9 (2)

insert image description here


insert image description here

foreword

While Swift 6 is already on the horizon, 5.x versions still have a lot of new features - simpler if and switch usage, macros, non-copyable types, custom actor executors, and more are coming in Swift 5.9, Once again comes a huge update.

In this article, we describe the most important changes in this release, providing code samples and explanations so you can try them yourself. Requires the latest Swift 5.9 toolchain installed in Xcode 14, or use Xcode 15 beta.

Noncopyable Structs and Enums

SE-0390 introduces the concept of uncopyable structs and enums, allowing a single instance of a struct or enum to be shared in multiple places in the code, with only one owner, but now accessible in different parts of the code .

First, this change introduces a new syntax for canceling requirements: ~Copyable. This means " this type cannot be copied ", and this cancellation syntax is currently not available elsewhere - for example, we cannot use ~Equatableto exit a type ==.

UserTherefore, we can create a new non-copyable struct like this :

struct User: ~Copyable {
    
    
    var name: String
}

Note: Noncopyable cannot satisfy Sendableany protocol except .

Once an instance is created User, its non-copyable nature means it is different from previous versions of Swift. For example, the sample code below:

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

    var userCopy = newUser
    print(userCopy.name)
}

createUser()

But we've declared Userthe struct as non-copyable, and can't be copied newUser, and newUserassigning to userCopycauses the original newUservalue to be consumed, which means it can't be used, since ownership now belongs to userCopy. If you try to print(userCopy.name)change to print(newUser.name), Swift throws a compiler error.

New restrictions also apply to how non-copyable types are used as function arguments: SE-0377 states that functions must explicitly specify whether they intend to consume the value and invalidate it at the call site after the function completes, or whether they wish to borrow the value so that it can be used with Other borrowed parts read their data concurrently.

So, one could write a function that creates a user, and another function that borrows a user for read-only access to its data:

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

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

createAndGreetUser()

In contrast, if we make greet()the function use consuming User, the print("Goodbye, \(newUser.name)")will not be allowed - Swift will consider the value to be invalid greet()after running newUser. On the other hand, since the consuming method must end the life cycle of the object, its properties can be modified freely.

This sharing behavior gives non-copyable structs a superpower previously limited to classes and actors: the ability to provide a destructor that runs automatically when the last reference to a non-copyable instance is destroyed.

IMPORTANT NOTE: This behaves slightly differently than destructors on classes, and may be an issue with earlier implementations or intentional.

First, here's a code example that uses a class's destructor:

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()

When this code is run, it prints "Watching The Hunt for Red October" and then "The Hunt for Red October is no longer available". However, if you class Moviechange the type's definition from to struct Movie: ~Copyable, you'll see the two print()statements run in reverse order -- first saying the movie is no longer available, and then saying it's being watched.

Methods inside non-copyable types are borrowed by default, but can be marked like copyable types mutating, and can also be marked as consuming, indicating that the value is not valid after the method runs.

For example, in the movie and TV series "Mission: Impossible" we are familiar with, secret agents get mission instructions through a self-destruct tape that can only be played once. Non-copyable structs are great for this approach:

struct MissionImpossibleMessage: ~Copyable {
    
    
    private var message: String

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

    consuming func read() {
    
    
        print(message)
    }

A message so marked is itself private, so read()it can only be accessed by calling methods on the consuming instance.

Unlike mutating methods, consuming methods can operate on constant instances of a type. Therefore, code like the following is fine:

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

createMessage()

Note: Since message.read()consumes messagethe instance, trying to call for the second time message.read()will result in an error.

When combined with destructors, consuming methods cause cleanup to be repeated. For example, if you're tracking high scores in a game, you might want to have a consume finalize()method that writes the latest high score to persistent storage and prevents others from changing the score further, but also saves the latest score to disk when the object is destroyed.

To avoid this problem, Swift 5.9 introduces a new discardoperator that can be used in consumer methods for non-copyable types. Using in a consuming method discard selfprevents the object's destructor from running.

Therefore, a struct can be implemented like this 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()

Tip: When running this code, you'll see that deinitializerthe message is printed twice - once when changing valuethe property, actually destroying and recreating the struct, and once at createHighScore()the end of the method.

There are some additional complexities to be aware of when using this new feature:

  • Classes and actors cannot be non-copyable.
  • Generics are temporarily not supported for non-copyable types, which excludes optional non-copyable objects and arrays of non-copyable objects.
  • If a non-copyable type is used as a property in another struct or enum type, then the parent struct or enum type must also be non-copyable.
  • CopyableCare should be taken when adding or removing s to existing types , as this will change usage. This will break the ABI if the code is released in a library.

Ends the lifecycle of a variable binding

Use the consume operator to end the lifetime of a variable binding

SE-0366 extends the concept of consumed values ​​for local variables and constants of copyable types, which may be beneficial to developers who wish to avoid unnecessary retain/release calls during their data transfers.

In its simplest form, the consume operator looks like this:

struct User {
    
    
    var name: String
}

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

createUser()

The important one is let userCopythis line, which does two things at the same time:

  1. Copies newUserthe value of into userCopyinto .
  2. ends newUserthe lifetime of the , so any further attempts to access it will throw an error.

This will explicitly tell the compiler "this value is not allowed to be used again", which will enforce this rule.

_We can see that this is especially common when using so-called black holes , where we don't want to copy the data, but just mark it as destroyed, for example:

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

In fact, probably the most common case is passing a value to a function like this:

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

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

createAndProcessUser()

There are two things in particular worth knowing.

First, Swift keeps track of which branches of code consume values ​​and enforces the rules conditionally. So, in this code, only one of the two possibilities consumes Userthe instance:

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()

Second, strictly speaking, consumeoperators act on bindings, not values. In practice, this means that if a variable is used for consumption, it can be reinitialized and used normally:

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

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

createThenRecreate()

makeStream() method

SE-0388 Added a new method to AsyncStreamand that returns the stream itself and its continuation.AsyncThrowingStreammakeStream()

Therefore, it is no longer necessary to write the following code:

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

It is now possible to get both:

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

This is especially handy where the continuation needs to be accessed outside of the current context, such as in another method. For example, it used to be possible to write a simple number generator like this, which needed to store the continuation as its own property so that it could be called from the queueWork()method:

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()
        }
    }
}

With the new makeStream(of:)method, this code becomes much simpler:

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()
        }
    }
}

Add sleep(for:) to Clock

SE-0374Clock adds a new extension method to Swift's protocol that allows execution to be paused for a period of time, while also supporting duration-based task sleep with a specific tolerance.

ClockThe change is small but significant, especially when simulating concrete Clockinstances to eliminate latencies that exist in production during testing.

For example, you can create this class with any type Clock, and use that Clock to sleep before triggering the save operation:

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…")
    }
}

Since it is used any Clock<Duration>, it can be used in production ContinuousClock, and in tests it can be used with custom ones DummyClock, where all sleep()commands are ignored to make the tests run fast.

In older Swift versions, the corresponding code could theoretically be try await clock.sleep(until: clock.now.advanced(by: .seconds(1))), but it doesn't work in this example because Swift doesn't know what type of clock is being used, so it can't get it clock.now.

As for Taskthe change of sleep, you can start from the following code:

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

Simplifies to:

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

Discarding task groups

SE-0381 adds new Discarding task groups, filling an important gap in the current API: tasks created inside task groups are automatically discarded and destroyed after completion, which means that long-running task groups (or Taskgroups that may be running all the time, such as a server) will not leak memory over time.

You might run into problems when using the raw withTaskGroup()API, because Swift only next()discards subtasks and their result data when calling or looping through the subtasks of a taskgroup. Calling next()will cause the code to pause while all subtasks are executing, so the problem is: I want the server to always listen for connections in order to add tasks to process, but also need to stop periodically to clean up old completed tasks.

A clean solution to this problem was introduced in Swift 5.9 with the addition of the withDiscardingTaskGroup()and withThrowingDiscardingTaskGroup()functions for creating new groups of discarded tasks. These task groups automatically discard and destroy tasks after each task completes, eliminating the need to manually call next()to consume it.

In order to understand what is triggering the problem, a simple directory monitor can be implemented that runs in a loop and reports the names of files or directories that have been added or removed:

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))
            }
        }
    }
}

It can then be used from a simple application, although for brevity only prints the URL without any complex processing:

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())")
    }
}

This code will run forever, or at least until the user terminates the program or the watched directory is no longer accessible. However, with the use of withThrowingDiscardingTaskGroup(), this problem doesn't exist: addTask()a new subtask is created every time you call it, but since it's not called anywhere group.next(), these subtasks are never destroyed. In increments of perhaps only a few hundred bytes at a time, this code will take up more and more memory until eventually the OS runs out of memory and is forced to terminate the program.

This problem disappears completely in Discarding task groups: just withThrowingTaskGroup(of: Void.self)replace with withThrowingDiscardingTaskGroup, and each subtask will be destroyed automatically after it has done its work.

Summarize

Special thanks to every editor in the Swift community editorial department, thank you for your hard work, provide high-quality content for the Swift community, and contribute to the development of the Swift language.

Guess you like

Origin blog.csdn.net/qq_36478920/article/details/131450564