テックノート

Swift 5.5の新機能とは?
【AsyncSequence編】

新しいAsyncSequenceプロトコルを使用して、非同期の値のシーケンスをループすることができます。
代表者 ロジャース リチャード 投稿者: ロジャース リチャード
xCode 13+ iOS 15+ macOS 12+ Swift 5.5+

Swift 5.5には、async/await、actors, throwingプロパティなど、膨大な改良が施されています。あまりにも多くのことが変わっているので、「Swift 5.5で新しくないものは何か」と尋ねるのが初めて簡単になったかもしれません。

概要

この記事では、コードサンプルを使ってそれぞれの変更点を説明し、実際にどのように動作するかを確認していただきます。これは、非常に多くの巨大な Swift Evolution の提案が、これほど緊密にリンクされた初めてのケースです。そのため、これらの変更を首尾一貫した流れの中で整理しようとしましたが、並行処理の作業のいくつかの部分は、いくつかの提案を読んだ後でなければ本当に理解できません。

ヒント: コードサンプルを自分で試してみたい方は、XcodeのPlaygroundとしてダウンロードすることもできます。

Async/await:シーケンス

SE-0298では、新しいAsyncSequenceプロトコルを使って、非同期の値の列をループする機能を導入しています。これは、計算に時間がかかったり、まだ利用できないなどの理由で、すべての値を一度に計算するのではなく、利用できるようになった値を順番に処理したい場合に便利です。

AsyncSequenceの使い方はSequenceとほぼ同じですが、型がAsyncSequenceAsyncIteratorに準拠していることと、next()メソッドにasyncのマークがついていることが違います。シーケンスの終了時には、sequenceと同様にnext()からnilを返すようにします。

例えば、1から始まり、呼ばれるたびにその数を倍にするDoubleGeneratorシーケンスを作ることができます。

struct DoubleGenerator: AsyncSequence {
    typealias Element = Int

    struct AsyncIterator: AsyncIteratorProtocol {
        var current = 1

        mutating func next() async -> Int? {
            defer { current &*= 2 }

            if current < 0 {
                return nil
            } else {
                return current
            }
        }
    }

    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator()
    }
}
ヒント: このコードの中で「async」が出てくる箇所をすべて削除すると、まったく同じことをしている有効なSequenceになります。

非同期シーケンスができたら、非同期コンテキストでfor awaitを使って、次のようにその値をループさせることができます。

func printAllDoubles() async {
    for await number in DoubleGenerator() {
        print(number)
    }
}

AsyncSequenceプロトコルでは、map()、compactMap()、allSatisfy()など、さまざまな一般的なメソッドのデフォルト実装も提供しています。例えば、ジェネレータが特定の数値を出力するかどうかを確認するには、次のようにします。

func containsExactNumber() async {
    let doubles = DoubleGenerator()
    let match = await doubles.contains(16_777_216)
    print(match)
}

繰り返しになりますが、これを使用するには、非同期コンテキストである必要があります。