ソフトウェア設計

Swiftのプロトタイプパターン:NSCopyingと値型のコピーの違い

プロトタイプパターンは既存のオブジェクトを複製し、生成コストと設定の重複を減らします。Swiftの値型のコピーとクラスのNSCopyingの違い、浅いコピーとCopy-on-Writeが作用する場面をまとめます。

読了 5 分
Swiftのプロトタイプパターン:NSCopyingと値型のコピーの違いのカバー画像

Swiftでオブジェクトをコピーしていて、「これは本当にコピーされたのか」と手が止まったことはありませんか?

クラスインスタンスを1行の代入でコピーすると、複製先を変更しただけなのに元のオブジェクトまで変わる問題につながりがちです。

結論から言うと、Swiftのコピーには2つの方向性があります。

値型(struct)は代入するだけで自動的にコピーされますが、クラス(参照型)は本当のコピーにするためにNSCopyingを直接実装する必要があります。

この記事では、プロトタイプパターンとは何か、そしてNSCopyingと値型のコピーがどう違うのかをコードで整理します。

複製方式の境界を先に理解したい場合は浅いコピーと深いコピーを、Swiftのコレクションが実際のコピーを遅延させる仕組みはCopy-on-Writeのまとめも参照してください。


プロトタイプパターンとは?

プロトタイプパターンは一言で言えば、既存のオブジェクトを複製して新しいオブジェクトを作るデザインパターンです。

最初から作り直すのではなく、設定済みの元オブジェクトをスタンプのように複製する方法です。

私はこれを判子にたとえると理解しやすいと思っています。元の判子を1つきれいに作れば、あとは押すだけですよね。

オブジェクトの生成コストが高い場合や、初期設定が複雑な場合に特に役立ちます。

たとえばゲームキャラクターを作るとき、能力値や装備、スキルを毎回設定する代わりに、基本キャラクターを複製して少しずつ変更します。

重要なのは、複製先が元のオブジェクトから完全に分離されていることです。

複製先を変更したときに元のオブジェクトも変わるなら、それはコピーではなく参照の共有です。

まさにここで、値型と参照型の違いが現れます。


値型のコピーはなぜ便利?

Swiftのstructとenumは値型です。

値型の最大の利点は、代入や関数への受け渡しだけで自動的にコピーされることです。

次のコードを見るとイメージしやすいでしょう。元の値をコピーした後で変更しても、互いに影響しません。

struct Character {
    var name: String
    var level: Int
}

var origin = Character(name: "戦士", level: 1)
var copy = origin      // 値全体がコピーされる
copy.level = 99
print(origin.level)    // 1 (元の値はそのまま!)

copy.levelを99に変更しても、origin.levelは1のままです。

別途コピー用のコードを書く必要がないため、ミスの余地が大きく減ります。

そのため、特別な理由がなければ、私はまずstructを検討します。

Swiftが値型のコピーを推奨する理由も、この安全性にあります。


NSCopyingはどう使う?

問題は参照型であるクラスです。

クラスは代入してもコピーされず、同じインスタンスを指すアドレスだけが共有されます。

そのため本当のコピーが必要なら、NSCopyingプロトコルを採用し、copy(with:)メソッドを自分で実装する必要があります。

class Character: NSCopying {
    var name: String
    init(name: String) { self.name = name }

    func copy(with zone: NSZone? = nil) -> Any {
        return Character(name: self.name)
    }
}

let origin = Character(name: "戦士")
let clone = origin.copy() as! Character

copy()を呼び出すと新しいインスタンスが作られるため、cloneを変更してもoriginはそのままです。

ここで1つ注意点があります。

完全なコピー(深いコピー)に近づけるには、copy(with:)の中で内部プロパティも新しく作る必要があります。

内部オブジェクトをそのまま代入するだけでは、外側だけがコピーされ、中身は共有されたままの浅いコピーになるからです。


値型のコピーとNSCopyingを一覧で比較

2つの方式の違いを表にまとめました。

項目 値型(struct)のコピー NSCopying(class)
コピー方法 代入時に自動コピー copy()を直接呼び出す
実装が必要 不要 copy(with:)の実装が必要
基本動作 常に分離されたコピー 代入時にアドレスを共有
浅いコピー/深いコピー 気にすることが少ない 自分で対応する必要がある
おすすめの場面 ほとんどのデータモデル 参照型が必須の場合

まとめると、基本的には値型で設計するほうが簡単で安全です。

ただし継承が必要な場合、Objective-C APIと連携する場合、インスタンスの同一性が重要な場合は、クラスとNSCopyingを使います。

このようなとき、プロトタイプパターンをNSCopyingで実装するときれいに収まります。

代入するだけでコピーされるので、私はまずstructを思い浮かべます
代入するだけでコピーされるので、私はまずstructを思い浮かべます

よくある質問(Q&A)

Q. structだけ使うならNSCopyingは知らなくても大丈夫ですか?

基本的なコピーについてはそのとおりです。ただしUIKitやObjective-CベースのAPIを扱うとクラスをコピーする場面があるため、概念は知っておくとよいでしょう。

Q. copy()とmutableCopy()は何が違いますか?

copy()は不変のコピー、mutableCopy()は変更可能なコピーを作るために使います。後者ではNSMutableCopyingを採用する必要があります。

Q. 深いコピーが常に正解ですか?

いいえ。内部オブジェクトを共有してもよい状況なら、浅いコピーのほうが性能面で有利な場合があります。

元の判子を1つきれいに作れば、あとは押すだけです
元の判子を1つきれいに作れば、あとは押すだけです

まとめ

コピーで迷ったら、「元のオブジェクトを変更したとき、複製先も変わるか?」だけを考えてみてください。

値型ならその悩みをSwiftが代わりに解決してくれます。クラスではNSCopyingを使って自分で管理すればよいのです。

今日まとめた内容が、コピーで夜通し悩む時間を少しでも減らす助けになればうれしいです。頑張ってください!