【物語で学ぶ Swift Concurrency】データ競合を回避するための安全なデータコピー戦略

Swift 6 の並行処理を使っていると、次のようなエラーに出くわすことがあります。

    Sending 'result' risks causing data races

この記事では、このエラーがなぜ発生するのか、そしてどのように改善すれば安全に非同期処理でデータを渡せるのかを、先生と太郎の対話形式のお話で解説します。

---

## はじめに

並行処理では、複数のタスクが同じデータにアクセスすると、まるで大切なノートをみんなで同じ原本に書き込むような状態になって、内容がぐちゃぐちゃになってしまいます。Swift 6 では、そのリスクを防ぐため、非同期に値を渡すとき、その値が「Sendable」かどうかを厳しくチェックします。

ここでは、例えば `[String: Any]` 型の辞書（大切なノート）をそのまま渡すと危険な理由と、安全に渡すためのコピー戦略を見ていきます。

---

## シーン 1：エラーになるコード

### 登場人物
- **先生**：Swift の並行処理と安全性に詳しい
- **太郎**：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 を使ってディープコピーする

**先生**：「まずは原本のコピーを作る方法だ。これは、原本のノートの写真を撮って新しいコピーを作るようなもの。そうすれば、みんながそのコピーを使っても、元のノートは安全に守られるんだ。」

```swift
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 – 具体的な型に変換して安全に渡す

**先生**：「もう一つの方法は、最初から具体的な型、例えば構造体を使ってデータを表すことだ。構造体は値渡しになるので、変数に代入すると自動的にコピーが作られる。これは、原本のノートを最初から封筒に入れて運ぶようなものだよ。」


```swift
// 生徒情報を表す構造体（封筒に入れたノートのイメージ）
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 の並行処理では、「原本そのまま渡すと危ない！」という考え方を理解し、適切なコピー戦略を採用することが重要です。皆さんも、プロジェクトでデータの安全性を守るために、ぜひこのコピー戦略を活用してみてください！