【コラム】

ダイナミック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を投げる事になる。

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

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

    新着記事

    特設サイトの情報

    人気記事

    一覧

    イチオシ記事

    新着記事

    特別企画

    一覧