Swift & Objective-C

浅いコピー vs 深いコピー:SwiftのCOWまで

参照コピー、浅いコピー、深いコピーでは、新しく作成するデータの範囲が異なります。Swiftの値型に参照型を含める場合の落とし穴と、Copy-on-Writeが独立したコピーを維持する仕組みを解説します。

読了 5 分
浅いコピー vs 深いコピー:SwiftのCOWまでのカバー画像

配列をコピーして新しい変数に代入しました。コピー側だけを変更したのに、元の配列まで変わっています。プログラミングで一度は遭遇する謎です。

原因は「コピー」という言葉が持つ二つの顔です。値全体を複製するコピーもあれば、同じデータを指す名前をもう一つ作るだけのコピーもあります。

浅いコピーと深いコピーを分ける基準を確認し、Swiftでこの問題がどう変わるかを整理します。値型とCopy-on-Writeで事情は変わりますが、落とし穴は残ります。

要点をまとめます。

この区別をオブジェクト生成パターンに適用するとSwiftのプロトタイプパターンとNSCopyingにつながり、性能のために実際のコピーを遅延させる実装はSwift Copy-on-Writeで詳しく扱います。

  1. 参照コピー:データはそのままに、指している名前だけを複製。一方を変更すると両方が変わる
  2. 浅いコピー:外側だけ新しく作るが、中の参照は同じ対象を指す
  3. 深いコピー:中身まで再帰的にすべて新しく作る。完全に独立する
  4. Swiftの値型は代入だけで独立したコピーのように動作しますが、クラスを含むと浅いコピーの問題がそのまま戻ってきます。

正確には3段階に分ける必要があります

浅いコピーと深いコピーの2種類で語られがちですが、参照コピーを加えた3段階で考えて初めて全体像を正しく捉えられます。

参照コピーはコピーではなく、別名を付けることです。クラスインスタンスを別の変数に代入しても、オブジェクトは一つで、それを指す参照だけが二つになります。

class Profile { var name = "開発者キム" }
let a = Profile()
let b = a          // 参照コピー:オブジェクトは一つ
b.name = "コーディング担当イ"
print(a.name)      // "コーディング担当イ」— 元の値も変わる

浅いコピーは外側の一層だけを新しく作ります。中身が参照なら、その参照が指す対象は共有されます。

深いコピーは箱の中の箱、その中の箱まで全て新しく作ります。二つの構造は完全に独立しているため、どちらを変更しても影響しません。

浅いコピーは外側だけ、深いコピーは中身まで全て新しく作る
浅いコピーは外側だけ、深いコピーは中身まで全て新しく作る

Swiftの値型:代入がそのままコピーになる

Swiftの構造体・列挙型・基本型は値型なので、代入した瞬間に値が複製されます。

var original = [1, 2, 3]
var copy = original
copy.append(4)
print(original) // [1, 2, 3] — 安全

別の言語で配列の代入が参照コピーになるのとは大きく異なります。Pythonでリストを代入すると、同じリストを指すことになります。

「毎回全体をコピーしたら遅くない?」という疑問に答えるのがCopy-on-Write(COW)です。Array・Dictionary・Stringは代入時に内部バッファを共有し、一方が変更された瞬間にだけ実際のコピーを行います。読むだけならコピーのコストはなく、意味上は常に独立した値として動作します。値型の安全性と参照共有の効率を両立した設計です。


それでも落とし穴は残ります

値型に参照型が入った瞬間、浅いコピーの問題が再び現れます。

class Attachment { var filename = "a.png" }

struct Mail {
    var title: String
    var attachment: Attachment  // 構造体内のクラス
}

var mail1 = Mail(title: "元の値", attachment: Attachment())
var mail2 = mail1               // 構造体はコピーされるが...
mail2.attachment.filename = "b.png"
print(mail1.attachment.filename) // "b.png" — 元の値も変わる!

Mail構造体自体は確かにコピーされています。titleは独立しています。しかしattachmentプロパティに入っていたのは「参照」なので、コピーされたものも参照です。二つのMailが同じAttachmentを共有することになります。これがまさに浅いコピーの定義です。

解決するには深いコピーを自分で作る必要があります。新しいAttachmentを作って格納するコピー処理を書くか、最初からAttachmentを構造体に変更します。Objective-C系ではNSCopyingのcopy(with:)を実装するのが伝統的な方法です。クラスの中にさらにクラスがある場合は、真の深いコピーにするため、その中身まで再帰的にコピーする必要があります。

実務上の結論は、Swiftが値型を推奨する理由と一致します。モデルを構造体と値型のプロパティだけで構成すれば、この問題自体がなくなります。

モデルを値型だけで構成すれば、この問題自体がなくなる
モデルを値型だけで構成すれば、この問題自体がなくなる

面接で一言にまとめると

「浅いコピーは最上位の外側だけを新しく作り、内部の参照を共有するコピーです。深いコピーは内部まで再帰的に複製し、完全に独立させます。Swiftの値型は代入だけで独立したコピーのように動作しますが、値型内の参照型プロパティは共有されたままなので注意が必要です。」

追加質問には、「COWとは何ですか?」(変更時までコピーを遅らせる最適化)、「深いコピーはどう実装しますか?」(NSCopyingまたは手動複製を再帰的に適用)まで答えられれば十分です。


まとめ

  • コピーは、参照コピー → 浅いコピー → 深いコピーの3段階で理解する
  • 参照コピーは名前だけを複製(オブジェクトを共有)、浅いコピーは外側だけを複製(内部参照を共有)、深いコピーは全てを複製(完全に独立)
  • Swiftの値型では代入がコピーになり、Array・StringはCOWによって変更時まで実際のコピーを遅らせる
  • 値型の中にクラスプロパティがあると、浅いコピーの問題がそのまま再現される
  • 深いコピーが必要なら、NSCopyingまたは手動の複製ロジックを再帰的に実装する
  • 根本的な解決策はモデルを値型で構成すること—Swiftが構造体を推奨するもう一つの理由