Swift AsyncSequence — detailed explanation of code examples

foreword

AsyncSequencePart of the Concurrency Framework and SE-298 proposal. Its name implies that it is a type that provides asynchronous, sequential and iterative access to its elements. In other words: it's an asynchronous variant of the regular sequences we're familiar with in Swift.

Just like you don't create your custom sequence very often, I don't expect you to create a custom AsyncSequenceimplementation . However, you will most likely have to work with asynchronous sequences due to use with types AsyncThrowingStream和AsyncStreamsuch as . Therefore, I will guide you to work with AsyncSequenceexamples .

insert image description here

What is AsyncSequence?

AsyncSequenceis an asynchronous variant Sequenceof . Due to its asynchronous nature, we need to use awaitthe keyword since we are dealing with methods defined asynchronously. If you haven't used it async/await, I encourage you to read my article: async/await in Swift - code examples explained

Values ​​can become available over time, meaning that a AsyncSequencevalue may or may not contain some, or all, values ​​the first time you use it.

It's important to understand AsyncSequencethat is just a protocol. It defines how to access the value, but does not produce or contain the value. AsyncSequenceThe implementor of the protocol provides one AsyncIteratorand is responsible for developing and potentially storing the value.

Function Note
contains(_ value: Element) async rethrows -> Bool Requires Equatable element
contains(where: (Element) async throws -> Bool) async rethrows -> Bool The async on the closure allows optional async behavior, but does not require it
allSatisfy(_ predicate: (Element) async throws -> Bool) async rethrows -> Bool
first(where: (Element) async throws -> Bool) async rethrows -> Element?
min() async rethrows -> Element? Requires Comparable element
min(by: (Element, Element) async throws -> Bool) async rethrows -> Element?
max() async rethrows -> Element? Requires Comparable element
max(by: (Element, Element) async throws -> Bool) async rethrows -> Element?
reduce<T>(_ initialResult: T, _ nextPartialResult: (T, Element) async throws -> T) async rethrows -> T
reduce<T>(into initialResult: T, _ updateAccumulatingResult: (inout T, Element) async throws -> ()) async rethrows -> T

For these functions, we first define a type that conforms to AsyncSequencethe protocol . The name is modeled after existing standard library "sequence" types, such as LazyDropWhileCollectionand LazyMapSequence. We AsyncSequencethen add a function in the extension of that creates the new type (using 'self' as 'upstream') and returns it.

Function
map<T>(_ transform: (Element) async throws -> T) -> AsyncMapSequence
compactMap<T>(_ transform: (Element) async throws -> T?) -> AsyncCompactMapSequence
flatMap<SegmentOfResult: AsyncSequence>(_ transform: (Element) async throws -> SegmentOfResult) async rethrows -> AsyncFlatMapSequence
drop(while: (Element) async throws -> Bool) async rethrows -> AsyncDropWhileSequence
dropFirst(_ n: Int) async rethrows -> AsyncDropFirstSequence
prefix(while: (Element) async throws -> Bool) async rethrows -> AsyncPrefixWhileSequence
prefix(_ n: Int) async rethrows -> AsyncPrefixSequence
filter(_ predicate: (Element) async throws -> Bool) async rethrows -> AsyncFilterSequence

Create AsyncSequence

Create a custom AsyncSequence.

To better understand AsyncSequencehow works, I'll show an example implementation. However, when defining your custom implementation AsyncSequenceof , you may want to use AsyncStreaminstead, as it is more convenient to set up. Therefore, this is just a code example to better AsyncSequenceunderstand how works.

The following example follows the example in the original proposal and implements a counter. The values ​​are available immediately, so there is not much need for an asynchronous sequence. However, it does show the basic structure of an asynchronous sequence:

struct Counter: AsyncSequence {
    
    
    typealias Element = Int

    let limit: Int

    struct AsyncIterator : AsyncIteratorProtocol {
    
    
        let limit: Int
        var current = 1
        mutating func next() async -> Int? {
    
    
            guard !Task.isCancelled else {
    
    
                return nil
            }

            guard current <= limit else {
    
    
                return nil
            }

            let result = current
            current += 1
            return result
        }
    }

    func makeAsyncIterator() -> AsyncIterator {
    
    
        return AsyncIterator(howHigh: limit)
    }
}

As you can see, we define AsyncSequencea Counterstruct that implements the protocol. The protocol requires us to return a custom one AsyncIterator, which we solved using an internal type. We could decide to rewrite this example to remove the need for the inner type:

struct Counter: AsyncSequence, AsyncIteratorProtocol {
    
    
    typealias Element = Int

    let limit: Int
    var current = 1

    mutating func next() async -> Int? {
    
    
        guard !Task.isCancelled else {
    
    
            return nil
        }

        guard current <= limit else {
    
    
            return nil
        }

        let result = current
        current += 1
        return result
    }

    func makeAsyncIterator() -> Counter {
    
    
        self
    }
}

We can now return selfas an iterator and keep all logic centralized.

Note that we must help the compiler comply with AsyncSequencethe protocol .

next()method is responsible for iterating over the overall value. Our example boils down to feeding as many count values ​​as possible until we hit the limit. We implement cancellation support through a check Task.isCancelledon .

Please add a picture description

Iteration of an asynchronous sequence

Now that we know what is AsyncSequenceand how it's implemented, it's time to start iterating over the values.

Using the above example as an example, we can use Counterto start the iteration:

for await count in Counter(limit: 5) {
    
    
    print(count)
}
print("Counter finished")

// Prints:
// 1
// 2
// 3
// 4
// 5
// Counter finished

We must use awaitthe keyword because we may receive values ​​asynchronously. We exit the for loop once there are no more expected values. Implementors of asynchronous sequences next()can nilsignal reaching the limit by returning in the method. In our case, we hit this expectation once the counter reaches the configured limit, or the iteration cancels:

mutating func next() async -> Int? {
    
    
    guard !Task.isCancelled else {
    
    
        return nil
    }

    guard current <= limit else {
    
    
        return nil
    }

    let result = current
    current += 1
    return result
}

Many regular sequence operators can also be used with asynchronous sequences. As a result, we can perform operations such as mapping and filtering asynchronously.

For example, we can filter only even numbers:

for await count in Counter(limit: 5).filter({
    
     $0 % 2 == 0 }) {
    
    
    print(count)
}
print("Counter finished")

// Prints: 
// 2
// 4
// Counter finished

Or we can map the count to one before iterating String:

let counterStream = Counter(limit: 5)
    .map {
    
     $0 % 2 == 0 ? "Even" : "Odd" }
for await count in counterStream {
    
    
    print(count)
}
print("Counter finished")

// Prints:
// Odd
// Even
// Odd
// Even
// Odd
// Counter finished

We can even use AsyncSequenceinstead of a for loop, by using containsmethods like .

let contains = await Counter(limit: 5).contains(3)
print(contains) // Prints: true

Note that the above method is asynchronous, meaning it has the potential to wait endlessly for a value to exist until the underlying AsyncSequencecompletes .

in conclusion

AsyncSequenceis the asynchronous replacement for the Sequenceregular . Just Sequenceas , it's also unlikely that you'd create a custom asynchronous sequence.

Guess you like

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