本文へスキップ

Swift 6.4 の Concurrency 新機能を試した記録

はじめに

現時点で試せる Swift 6.4 の Concurrency 新機能を試してみました。

$ swift --version
Apple Swift version 6.4 (swiftlang-6.4.0.20.104 clang-2100.3.20.102)
Target: arm64-apple-macosx26.0

async defer (SE-0493)

defer の中で await が直接書けるようになりました。

// Before
func loadAd() async {
    isLoading = true
    defer {
        Task { await metrics.recordLoadFinished() }
        isLoading = false
    }
    let task = Task { try await adClient.requestAd() }
    try? await task.value
}

// After
func loadAd() async {
    isLoading = true
    defer {
        await metrics.recordLoadFinished()
        isLoading = false
    }
    let task = Task { try await adClient.requestAd() }
    try? await task.value
}

Before の Task { await ... } は別タスクに切り出されるため、関数を抜ける前に完了する保証がありません。After の async defer は同期 defer と同じく関数末尾でブロックして完了を待つので、エラーで抜けても順序が保証され、try/finally 相当の構文として使えそうです。

書き換えどころは次のあたりです。

  • リソースの close()disconnect() を呼ぶ箇所
  • リクエストの計測(開始時刻と終了時刻のセット)
  • 一時ファイルの削除や、ロックされた状態のリセット

逆に、後始末の実行順を気にしない場面(あとでログが残ればよく、関数を抜けるタイミングと前後しても問題ない、など)は書き換える必要がないので、順序の保証が必要な箇所から優先的に置き換えるのが良さそうです。

~Sendable (SE-0518)

「この型は意図的に Sendable ではない」と明示するための宣言です。エラーを直す機能ではなく、暗黙の Sendable 推論を将来にわたって止めるためのマーカーです。

// Before: 何も付けない(暗黙の Sendable 推論に依存)
final class PagerCoordinator {
    var currentIndex: Int = 0
    func advance() { currentIndex += 1 }
}

// After: Sendable にする気はないと明示
final class PagerCoordinator: ~Sendable {
    var currentIndex: Int = 0
    func advance() { currentIndex += 1 }
}

どちらも @Sendable クロージャに渡すと同じようにコンパイルエラーになります。

actor Repository {
    func setupHandler(_ handler: @escaping @Sendable () -> Void) {}

    func use(_ coordinator: PagerCoordinator) {
        setupHandler {
            coordinator.advance()
            // error: capture of 'coordinator' with non-Sendable type 'PagerCoordinator'
            //        in a '@Sendable' closure [#SendableClosureCaptures]
        }
    }
}

違うのは note の文言だけです。

  • Before: does not conform to the 'Sendable' protocol
  • After: explicitly suppresses conformance to 'Sendable' protocol

つまり「うっかり Sendable と推論されないように釘を刺す」のが ~Sendable の役割です。@Sendable 要求のエラーを実際に消したいなら、別 actor 化する、@unchecked Sendable で自己責任で準拠させる、そもそも @Sendable を要求しないシグネチャに変える、といった別の手段が必要になりそうです。

例外を投げる Task の値を捨てたときの警告 (SE-0520)

これまでは、こういうコードを書いてもコンパイラから何も言われませんでした。

// これまで(Swift 6.3 まで): 警告なしでビルドが通る
func bind() {
    Task { try await viewModel.refresh() }
}

refresh() が投げたエラーは誰も観測しないまま消えます。クラッシュもログも残らないので、症状(画面が更新されない、ローディングが止まらない、など)から原因にたどり着くのに時間がかかるバグの温床でした。

Swift 6.4 では例外を投げる Task の戻り値を捨てると、#NoUseUnstructuredThrowingTask 警告が出るようになりました。

NoUseUnstructuredThrowingTask 警告の Xcode 表示

警告のサジェスト通り、消し方は次の 3 つです。

// (a) Task の内側で do/catch する(エラーを実際にハンドルする)
func bind() {
    Task {
        do {
            try await viewModel.refresh()
        } catch {
            Logger.warn("refresh failed: \(error)")
        }
    }
}

// (b) Task の内側で try? に変える(throws を消す)
func bind() {
    Task { try? await viewModel.refresh() }
}

// (c) 戻り値を `_` で明示的に捨てる
func bind() {
    _ = Task { try await viewModel.refresh() }
}

使い分けは、エラー時にユーザーへ何かしら返したい場面なら (a)、ログだけ残せば十分な場面なら (b)、リトライや上位の監視が別にある場面なら (c)、が目安になりそうです。(c) はエラーが握りつぶされる挙動自体は変わらないので、「ここで捨てるのは確信犯」というコメント代わりの書き方になりそうです。

async defer と actor を組み合わせる

actor の使い捨て利用も async defer で素直に書けます。

// Before
func fetchUser(id: String) async -> Result<User, any Error> {
    let conn = Connection()
    do {
        let user = try await conn.queryUser(id: id)
        await conn.close()
        return .success(user)
    } catch {
        await conn.close()   // 例外パスでも閉じる、を二重に書く
        return .failure(error)
    }
}

// After
func fetchUser(id: String) async -> Result<User, any Error> {
    let conn = Connection()
    defer {
        await conn.close()   // SE-0493: 成功/失敗どちらも閉じる
    }
    do {
        let user = try await conn.queryUser(id: id)
        return .success(user)
    } catch {
        return .failure(error)
    }
}

Web API クライアントの一時セッションや、URLSessioninvalidateAndCancel() のような「使い切る」リソースで素直に効きそうです。

まとめ

書き換えの優先順位としては、次の順で当たっていくとよさそうです。

  1. defer { Task { await ... } } の順序バグ予備軍 → async defer に置換
  2. Task { try await ... } の値破棄警告 → 内側で do/catch するか、try? で意思表示
  3. 「越境しない」内部型 → ~Sendable で意思表示

よければ皆さんも自分のプロジェクトで grep してみてください。

参考リンク