Swift 6 の並行処理を使っていると、次のようなエラーに出くわすことがあります。
Sending 'result' risks causing data races
この記事では、このエラーがなぜ発生するのか、そしてどのように改善すれば安全に非同期処理でデータを渡せるのかを、先生と太郎の対話形式のお話で解説します。
はじめに
並行処理では、複数のタスクが同じデータにアクセスすると、まるで大切なノートをみんなで同じ原本に書き込むような状態になって、内容がぐちゃぐちゃになってしまいます。Swift 6 では、そのリスクを防ぐため、非同期に値を渡すとき、その値が「Sendable」かどうかを厳しくチェックします。
ここでは、例えば [String: Any]
型の辞書(大切なノート)をそのまま渡すと危険な理由と、安全に渡すためのコピー戦略を見ていきます。
シーン 1:エラーになるコード
登場人物
- 先生:Swift の並行処理と安全性に詳しい
- 太郎:Swift を学ぶ高校生
お話
太郎:「先生、こんなコードを書いてみたんですが…」
import Foundation
// 疑似的に非同期で辞書データを取得する関数
func fetchData(completion: @escaping ([String: Any]) -> Void) {
DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
let data: [String: Any] = ["name": "Alice", "age": 17]
completion(data)
}
}
// fetchData で取得した result をそのまま返す関数
@discardableResult
func getUnsafeData() async throws -> [String: Any] {
try await withCheckedThrowingContinuation { continuation in
fetchData { result in
// このまま result を渡すと、データ競合の可能性があるためエラーになる
continuation.resume(returning: result)
}
}
}
先生:「太郎、このコードでは fetchData
で得た result
をそのまま非同期に渡しているね。Swift 6 の並行性チェックは、[String: Any]
のような汎用的な辞書は、内部に変更可能なデータが含まれている可能性があると判断するんだ。もし複数のタスクが同じデータにアクセスすると、どこかで値が変わってデータ競合(データレース)が起こるリスクがあるため、エラーになってしまうんだ。」
太郎:「つまり、同じノートをみんなで使うと、誰かが勝手に書き換えて内容がぐちゃぐちゃになっちゃう、ということですね。」
シーン 2:改善策 1 – JSON を使ってディープコピーする
先生:「まずは原本のコピーを作る方法だ。これは、原本のノートの写真を撮って新しいコピーを作るようなもの。そうすれば、みんながそのコピーを使っても、元のノートは安全に守られるんだ。」
import Foundation
// 辞書をディープコピーする関数(ノートの写真を撮って新しいノートを作る)
func deepCopy(dictionary: [String: Any]) throws -> [String: Any] {
// 1. 原本の辞書を JSON データに変換(写真を撮る)
let data = try JSONSerialization.data(withJSONObject: dictionary, options: [])
// 2. JSON データから新しい辞書(コピー)を作る
guard let copy = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
throw NSError(domain: "DeepCopyError", code: 1, userInfo: nil)
}
return copy
}
@discardableResult
func getSafeData() async throws -> [String: Any] {
try await withCheckedThrowingContinuation { continuation in
fetchData { result in
do {
// 原本の辞書をディープコピーして安全なコピーを作成
let safeResult = try deepCopy(dictionary: result)
continuation.resume(returning: safeResult)
} catch {
continuation.resume(throwing: error)
}
}
}
}
太郎:「この方法なら、原本のノートから写真を撮って新しいコピーを作るので、みんながそのコピーに書き込んでも原本は変わらないんですね!」
シーン 3:改善策 2 – 具体的な型に変換して安全に渡す
先生:「もう一つの方法は、最初から具体的な型、例えば構造体を使ってデータを表すことだ。構造体は値渡しになるので、変数に代入すると自動的にコピーが作られる。これは、原本のノートを最初から封筒に入れて運ぶようなものだよ。」
// 生徒情報を表す構造体(封筒に入れたノートのイメージ)
struct StudentInfo: Sendable {
let name: String
let age: Int
}
@discardableResult
func getStudentInfo() async throws -> StudentInfo {
try await withCheckedThrowingContinuation { continuation in
fetchData { result in
if let name = result["name"] as? String,
let age = result["age"] as? Int {
// 具体的な型のインスタンスを生成して返す
let info = StudentInfo(name: name, age: age)
continuation.resume(returning: info)
} else {
continuation.resume(throwing: NSError(domain: "ParseError", code: 0, userInfo: nil))
}
}
}
}
太郎:「この方法なら、最初から生徒情報という具体的な型を使うので、自動的にコピーが作られ、安全に非同期で渡すことができるんですね!」
まとめ
-
エラーの原因
-
[String: Any]
のような汎用型は、内部に変更可能なデータを含む可能性があり、そのまま非同期に渡すとデータ競合のリスクがある。
-
-
改善策 1:JSON を使ったディープコピー
- 原本の辞書を一度 JSON データに変換し、そのデータから新しい辞書を作成することで、安全なコピーを作る。
-
改善策 2:具体的な型に変換する
- 最初から構造体など具体的な型を使えば、値渡しにより自動的にコピーが作成されるので、非同期処理でも安全にデータを渡せる。
Swift の並行処理では、「原本そのまま渡すと危ない!」という考え方を理解し、適切なコピー戦略を採用することが重要です。皆さんも、プロジェクトでデータの安全性を守るために、ぜひこのコピー戦略を活用してみてください!