夏までにiPhone アプリつくってみっか!

趣味でiPhone/Androidアプリを開発し、日々勉強した事を書いています。オープンワールド系レースゲームをUnityで開発中です。

【Objective-C】クラスの継承とイニシャライザについて

前回のポストで書いた、initメソッドの呼び方を間違えると無限ループに陥るという問題をもう少し詳しく調べてみました。
そこで参考にしたのがこの本。Objective-Cについての本です。

この第3版はARCについての詳しい解説もありますので、cococs2dでARC対応のプログラムを書く方は必読だと思います。ARCを使用する事で面倒なリテインカウントの事を考えなくて良くなるので便利なのですが、逆に手動でリテインカウントを操作すると誤動作します。ネット上のチュートリアルをARC対応の環境で動かそうとするときには不要なコードを取り除く必要があります。Xcodeのリファクタ機能で自動的にコンバートできますが、知っておいて損はないと思います。
話がそれました。
サブクラスとスーパークラスのイニシャライザがどのように呼ばれるか、前回のプログラムで考えてみたいと思います。
これが私がCCSpriteから継承したMySpriteクラスのイニシャライザです。

- (id)initWithImage
{
    if ((self = [super initWithFile:@"Icon-72.png"])) {
        CGSize screen = [CCDirector sharedDirector].winSize;
        CGPoint myPosition;
        myPosition.x = screen.width / 2;
        myPosition.y = screen.height / 2;
        self.position = myPosition;
        
        [self scheduleUpdate];
    }
    return self;
}

[super initWithFile:@"Icon-72.png"]でスーパークラスのイニシャライザを呼んでいます。このinitWithImageはスーパークラスのイニシャライザを呼んでいますので、MySpriteクラスの指定(designated)イニシャライザということになります。
仮にこのクラスが別のイニシャライザを持っている場合、[self initWithImage]という形で、必ず指定イニシャライザからスーパークラスの指定イニシャライザを呼びます。
ここで問題になるのが、CCSpriteのinitWithFile:がはたして指定イニシャライザかどうなのかというところです。
initWithFileが指定イニシャライザではなく、initWithImage(注:MySpriteクラスにも同名のメソッドがあります)がCCSpriteの指定イニシャライザであったと仮定します。CCSpriteクラスのinitWithFile内部では[self initWithImage]で指定イニシャライザを呼び出そうとしますが、selfが示すのはあくまでMySpriteクラスのオブジェクトのため、CCSpriteクラスではなく、MySpriteクラスのinitWithImageが呼ばれ、その中ではさらにCCSpriteクラスのinitWithFileが呼ばれ・・・永遠に抜け出せなくなります。
スーパークラスの指定イニシャライザがつかってなさそうなメソッド名を使う」という方法で事実上は回避できそうですが、本来は、継承するクラスの指定イニシャライザが何なのかをきちんと把握する必要があります。これはcocos2dのリファレンスに書かれています。CCSpriteの項目を開いてメソッドリストを見て行くと、initWithTextureメソッドが指定イニシャライザであることがわかります。

- (id) initWithTexture:		(CCTexture2D *) 	texture
rect:		(CGRect) 	rect
rotated:		(BOOL) 	rotated 
Initializes an sprite with a texture and a rect in points, optionally rotated. The offset will be (0,0). IMPORTANT: This is the designated initializer.

この指定イニシャライザを呼ぶにはいろいろパラメーターを渡す必要があるようです。
これは面倒なので、自前の指定イニシャライザを作るのをやめ、副次的(secondary)イニシャライザを作る事にします。それがこれ。

- (id)initWithImage
{
    id ret = [self initWithFile:@"Icon-72.png"];
    CGSize screen = [CCDirector sharedDirector].winSize;
    CGPoint myPosition;
    myPosition.x = screen.width / 2;
    myPosition.y = screen.height / 2;
    self.position = myPosition;
    [self scheduleUpdate];
    return ret;
}

内部ではさらに別の副次的イニシャライザinitWithFileを呼んでいます。これは前回までは[super initWithFile:@"Icon-72.png"]としてスーパークラスのメソッドを呼んでいたのですが、今回は[self initWithFile:@"Icon-72.png"]として、自分のクラスのメソッドを呼んでいる事にご注意ください。MySpriteクラスはinitWithFileメソッドをオーバライドしていないので結果的にはスーパークラスで実装されているメソッドが使用されるのですが、initWithImageは指定イニシャライザではないのでselfのメソッドを呼び出す必要があるのです。
cocos2dのCCSprite.mを読めばわかりますが、initWithFile先に何段階か同じような名前の副次的イニシャライザを経由してようやくinitWithTexture:texture rect:rect rotated:rotatedにたどり着きます。そこで呼ばれるのが[super init]。
ここでもう一つの疑問が。
このinitメソッドはどのクラスで実装されているもの?
self=MySpriteクラス、super=CCSpriteクラスと理解してるとCCSpriteクラスの指定イニシャライザがinitということになり矛盾が生じます。
そこで再び詳解 Objective-C 2.0 第3版に助けを求めると、こんな記述が。
「どのメソッドが実行されるのかはコンパイル時にクラスの継承関係から決定されます。」
つまり、selfが誰であろうとも、そのソースコード上のスーパークラスのメソッドが実行されるという事です。つまり、CCSpriteのスーパークラスのinitです。これで先ほどの矛盾は解消します。
しかしまた新たな疑問が!
「MySpriteクラスを継承したクラスはMySpriteのどのイニシャライザを呼べばいいの?」
MySpriteの初期化を実行するにはinitWithImageを呼ぶ必要がありますが、これは指定イニシャライザではないので[super initWithImage]と呼ばない方がいいのでしょう。従って、[self initWithImage]で自分のクラスの副次的イニシャライザとして呼び出して初期化します。こう考えて行くとあまりにも複雑でこの方法が本当に正しいのか心配になってきますが、「スーパークラスの指定イニシャライザで十分なばあいはサブクラスが自前の指定イニシャライザを実装する必要はない」Appleのドキュメントには書かれています。
Sometimes the designated initializer of a superclass may be sufficient for the subclass, and so there is no need for the subclass to implement its own designated initializer. Other times, a class’s designated initializer may be an overridden version of its superclass's designated initializer.

結局のところ、オブジェクトが適切に初期化され、再帰呼び出しによる無限ループがおこらなければ結果オーライなので、実用上はそこまで考えずに、自前イニシャライザのメソッド名がスーパークラスの指名イニシャライザのメソッド名(および指名イニシャライザにたどり着くまでのすべての副次的イニシャライザのメソッド名)と被らないようにしてあとはスーパークラスの使いたいイニシャライザをsuperで呼び出せば良いのかもしれません。つまり1番目に書いたinitWithImageす。

これまで多くのチュートリアルを見てきましたが、どれもself = [super init]で始まっていた気がします。なので、多分このシンプルな考え方で問題ないのだと思います。