【コラム】

ダイナミックObjective-C

58 デザインパターンをObjective-Cで - Prototype (2)

    木下誠  [2006/12/06]

    前回は、Prototypeパターンの基本的な実装を紹介した。その結果、ある意味当然だが、Prototypeパターンではオブジェクトのコピーが重要な事が分かったと思う。

    そこで、今回はObjective-CとCocoaにおける、コピー機能の紹介をしよう。非常に基本的な機能なのだが、議論すべき点はたくさんある。

    単純なコピー

    まず、最も単純なコピーから考えてみよう。コピーする対象はオブジェクトなのだが、いくらオブジェクトといっても所詮はメモリ上のデータなので、同じサイズのメモリ領域を確保して値をコピーしてやれば、コピーは完了する。Objective-CはC言語とのハイブリッドなので、mallocとmemcpyを使えばいい。

    といっても、それでは原始的すぎるので、Cocoaではオブジェクトの単純なコピーをサポートする関数が用意されている。NSCopyObjectだ。定義は次のようになる。

    List 1.

    id <NSObject> NSCopyObject(id <NSObject> anObject, unsigned int extraBytes, NSZone* zone)

    NSCopyObjectは、3つの引数を取る。最初の引数は、コピーの対象となるオブジェクト。2つ目は、コピーしたオブジェクトに、余分なメモリ領域を加えるかどうか指示する。たとえば、インスタンス変数の領域を増やしたいときなどに使えるだろう。3つ目の引数は、コピーを行うゾーンを指定する。ゾーンについては次回説明しよう。

    この関数を使うと、オブジェクトのコピーは次のようになる。

    List 2.

    id object;
    // objectを用意する

    id copiedObject;
    copiedObject = NSCopyObject(object, 0, nil);

    これで、もとのオブジェクトとまったく同じオブジェクトができ上がる。

    浅いコピー、深いコピー、retainを伴う浅いコピー

    通常のメモリ領域のコピーならばこれで終わりだが、オブジェクトの場合は、これだけでは十分ではない。問題となるのは、インスタンス変数として、他のオブジェクトへの参照があるときだ。この場合、参照の値だけをコピーするのか、それとも参照しているオブジェクトもコピーするのかを、決めなくてはならない。

    このとき、前者を浅いコピー(shallow copy)、後者を深いコピー(deep copy)と呼ぶ。NSCopyObjectによって行われるのは、浅いコピーだ。

    また、Objective-Cならではの問題として、浅いコピーを行ったときに、オブジェクトに新たにretainを投げて参照カウントを増やすのかどうか、という問題もある。

    どのスタイルのコピーを採用するのかは、オブジェクトごとに個別に対応する必要がある。そこで、コピーメソッドの実装のために提供されるのが、前回も紹介した、copyWithZone:メソッドなわけだ。このメソッドの中で、浅いコピー、深いコピー、retainを伴う浅いコピー、を必要に応じて実装する。

    ちなみに、NSObjectにはcopyというメソッドが用意されているが、これはcopyWithZone:を呼び出すものなので、これを実装しているオブジェクトでしか呼び出す事ができない。それに対してNSCopyObjectは、単純にメモリをコピーするだけなので、すべてのオブジェクトに対して呼び出す事が可能だ。ただし、NSCopyObjectでは浅いコピーしかできないので、そのオブジェクトが意図するコピーを行うためにも、特別な場合を除いてcopyメソッドを使うべきだろう。

    copyWithZone:の例

    例を示そう。_titleというインスタンス変数を持つオブジェクトに、コピーメソッドを実装してみる。_title変数も、一緒にコピーしよう。

    List 3.

    - (id)copyWithZone:(NSZone*)zone
    {
        id  copiedObject;
        copiedObject = [[[self class] allocWithZone:zone] init];
        copiedObject->_title = [_title copyWithZone:zone];
        
        return copiedObject;
    }

    まず、allocとinitを使いコピーするオブジェクトを作る。そこに、copyWithZone:でコピーした_titleを設定している。これを、retainした_titleを渡すようにしてやれば、retainを伴った浅いコピーとなる。

    インスタンス変数をコピーするには、copyWithZone:を使う方法と、alloc+initを使う方法とがあるだろう。これも、状況に応じて使い分ける事になる。

    深いコピーは、インスタンス変数の構造が複雑になると大変だが、Cocoaによるサポートもある。たとえば、配列を表すNSArrayクラスには、保持しているオブジェクトを深いコピーするかどうかを指定できる初期化メソッドがある。先ほどのList 3.に、_namesというNSArray型の変数を付け加えて、深いコピーをしてみよう。

    List 4.

    - (id)copyWithZone:(NSZone*)zone
    {
        id  copiedObject;
        copiedObject = [[[self class] allocWithZone:zone] init];
        copiedObject->_title = [_title copyWithZone:zone];
        copiedObject->_names = [[[_names class] allocWithZone:zone] 
                initWithArray:_names copyItems:YES];
        
        return copiedObject;
    }

    initWithArray:copyItmes:というメソッドを使ってコピーしている。2つ目の引数にYESを指定すれば、配列の中身をすべてコピーしてくれる便利なメソッドだ。NOを指定した場合は、retainを投げる事になる。

    このように、一口に「コピー」といっても、気をつけなくてはいけない事はたくさんある。始めのうちは、浅いコピーと深いコピーは混同しやすいので、十分気を付けてほしい。

    次回も、コピーにまつわる話を続けよう。

    新着記事

    特設サイトの情報

      人気記事

      一覧

        イチオシ記事

        新着記事

        特別企画

        マイナビニュースマガジン