Cocoaでいこう! Macらしく 第36回
Yoshiki(DreamField)
この記事は、MOSAが発行するデベロッパ向けのデジタルマガジンMOSADeN 第98号(2004年1月27日発行)に掲載された記事です。2〜3ヶ月遅れで、ここに掲載して行きます。

前回説明したように、実際にプログラムを組む時には色々と考慮すべきことがあります。今回は、説明を分かりやすくするために、あえて手を抜いて説明から除外していた部分で、しかしプログラムを組む上では考慮しておいた方が良いことを説明します。

オブジェクトをセットするメソッドで気をつけるべきこと

MyImageViewクラスでは、イメージをセットするメソッドを次の様に実装していました。

- (void)setImage:(NSImage *)newImage{
    image = newImage;
    [ image retain];
    [ self setFrameSize: [ image size]];
}

実はこれはあまり良い実装ではありません。どこがまずいのかと言うと、2回以上呼び出されることを考慮していないからです。

確かに今回のプログラムでは、2回呼び出されることはありません。しかし、将来どんな改良を加えることになるか分かりませんし、他のプログラムに流用するかもしれません。あるいは、バグによって2回以上呼び出されることだって考えられます。従いまして、どの様な使われ方をしても、妥当な動作をする様に組んでおくことが、将来に渡って問題を起こしにくくするコツであると言えます。

では、このメソッドが2回以上呼び出されたら、何が起こるのか考えてみましょう。1回目は、imageに新しいイメージへのポインタがセットされ、retainすることにより所有することになります。2回目も、imageに新しいイメージへのポインタがセットされ、retainすることにより所有します。プログラムの動作上、特に問題は起こりません。しかし、良く考えてみて下さい。1回目に受け取ったイメージはどうなってしまったのでしょうか。自分がretainしたのですから、自分でreleaseする義務があります。ところが、このプログラムではこれを行っていません。つまり、1回目にセットしたイメージは、既に必要無いのに、プログラムが終了するまで所有しっぱなしです。この様に、2回以上呼び出されると、メモリリークを起こしてしまうのです。

これを防ぐためには、既にセットされているイメージをreleaseしてから、新しいイメージをセットする必要があります。そこで次の様に実装してみましょう。

- (void)setImage:(NSImage *)newImage{
    [ image release];
    image = newImage;
    [ image retain];
    [ self setFrameSize: [ image size]];
}

このプログラムはimageをreleaseしてから、新しいイメージをセットしています。こうすれば、前回セットしたイメージが残ってしまうことを防げます。では、1回目に呼び出された時はどうなるでしょうか。1回目は、イメージがセットされていないのに、releaseメッセージを送ってしまっています。ですが、これは問題になりません。何故なら、インスタンスを生成した時に、全てのインスタンス変数は0クリアされており、imageの値はnilだからです。nilにメッセージを送っても、何も起こりません。

さて、これで一見問題は無いように見えます。ですが、実はこれでもまだ駄目なのです。どんな時に駄目なのかと言うと、2回以上連続で同じイメージをセットしようとした時です。どうなるのか考えてみましょう。1回目はimageはnilですから、releaseメッセージを送っても何も起こりません。そしてimageに新しく渡って来たイメージのポインタがセットされ、retainして所有します。2回目は、imageには前回セットしたイメージへのポインタが入っています。releaseメッセージを送れば、このイメージは解放されます。そして、今解放したのと同じイメージへのポインタをimageにセットし、これにretainメッセージを送って・・・。ちょっと待って下さい。解放してしまったイメージにretainメッセージを送ったら、このプログラムは誤動作してしまいます(おそらくは吹っ飛びます)。そもそも、一度解放して消えてしまったイメージを、今更所有なんて出来ません。つまり、まったく同じイメージを2回以上連続でセットすると、吹っ飛ぶという、とても危険なプログラムになってしまったのです。

それでは、このことも考慮した実装にしましょう。

- (void)setImage:(NSImage *)newImage{
    NSImage *tmpImage;

    tmpImage = image;
    image = newImage;
    [ image retain];
    [ tmpImage release];
    [ self setFrameSize: [ image size]];
}

imageの値を退避しておき、後で使えるようにした上で、imageに渡って来たイメージのポインタをセットしています。そして、こちらを先にretainし、その後で退避していた値を使って、今まで所有していたイメージをreleaseしています。この様にすれば、同じイメージが来ても、先にretainしていますから、releaseしても解放されません。違うイメージの場合でも、それぞれに対してretain、releaseしていますから問題は起こりません。とてもシンプルな方法です。

もう一つの書き方を紹介しましょう。私は普段、こちらの書き方で組んでいます。

- (void)setImage:(NSImage *)newImage{
    [ newImage retain];
    [ image release];
    image = newImage;
    [ self setFrameSize: [ image size]];
}

まず、新しいイメージをretainしてしまいます。それから、今までセットされていたイメージをreleaseします。そして、その後imageに新しいイメージへのポインタを格納します。この例の場合も先ほどと同様、新しいイメージのretainを先に行っていますから、たとえ同じイメージが来たとしても、releaseで解放されません。違うイメージの場合も、それぞれに対してretain、releaseしているのですから、大丈夫です。imageに格納するのはあくまでポインタであって、オブジェクトそのものではありませんので、この様な書き方も出来るわけです。

前頁目次次頁