イニシャライザが守るべき3つのルール

More than 5 years have passed since last update.

オブジェクトを初期化するイニシャライザ、init で始まるメソッドですね。

init とか initWithString とか。他言語でいうところコンストラクタ代わりです。

iOS開発している人なら、「指定イニシャライザ」 という単語は聞いたことある人多いと思いますが、これの意味と役割を理解せずに組んでいる人も多いのではないでしょうか?

そしてイニシャライザではいくつか守ったほうがいいルールがあります。


3 Rules for initializers

結論から書くと、以下の3つルールをイニシャライザでは守ると効率的になります。

(1)必ず一つの指定イニシャライザを持つ(基本的に複数はナシ)

(2)指定イニシャライザ以外のイニシャライザは、必ず指定イニシャライザ経由で初期化する

(3)親クラスの指定イニシャライザは必ずオーバーライドする

理由はこの記事で説明します。(※長いです)


class Rectangle

まず四角形を表すRectangleというクラスを考えてみましょう。幅高さを初期値で渡します


Rectangle.m

- (id) initWithWidth:(float)width height:(float)height {

if (self = [super init]) {
self.width = width;
self.height = height;
}
return self;
}

ただObjective-Cの構造上、

直接親クラスのイニシャライザを呼び出すことができてしまいます


Main.m

- (void) main {

//Rectangleにinitはないが、NSObjectにinitがあるのでビルドは通る
Rectangle *rect = [[Rectangle alloc] init];
}

これでは 幅と高さが渡らず正常に初期化できません。

その他の変数も初期化できていないので、まず間違いなく不具合を起こすでしょう。


override init

そのため、 initをオーバーライドしておきます。

方法は2つあり、「適当な値で初期化する」「どうしても初期化不可能ならnilを返す(最悪例外)」です。


Rectangle.m

//先ほどのコードは省略

//適当な値で初期化するとき
- (id) init {
if (self = [super init]) {
self.width = 1;
self.height = 2;
}
return self;
}
//どうしても初期化できないとき
- (id) init {
return nil; ///奥の手として @throw [NSException exceptionWith...];
}

こうすることでinitが呼び出されても大丈夫になりました。ただまだ問題があります。

※ビルドエラーを出す方法をコメントでいただきました。ありがとうございます。

 initが呼び出されたくない場合のやり方はコメント欄を参照してください。


class Square extends Rectangle

さらにRectangleを継承したSquareクラスを考えてみましょう。

この場合、イニシャライザにはlengthのみ渡せばOKなのでそういうイニシャライザを作ります。


Square.m

- (id) initWithLength:(float)length {

self = [super initWithWidth:length height:length];
if (self) { /*その他初期化処理*/ }
return self;
}

これだけでは先ほど同様、 initWithWidth:height: や、initが呼び出されたとき問題が発生します。

確実に正方形になる保証がないからです。

ではその二つをオーバーライドすればいい、となりますが、2つもオーバーライドしたくないですよね?

2つならがんばれるかもしれませんが、10個あったら?

親クラスのイニシャライザ全部を初期化するのはしんどいですし非常に手間がかかります。


Designated initializer

そこで登場するのが、指定イニシャライザ です。

そのオブジェクトを初期化するのに必要十分な引数を持ったイニシャライザです。

すべてのクラスは、「1つの指定イニシャライザをもち、

その他のイニシャライザはすべて指定イニシャライザ経由で初期化」
させるようにします。

Rectangleはこうなります。


Rectangle.m

- (id) initWithWidth:(float)width height:(float)height {

if (self = [super init]) {
/*先ほど同様なので省略*/
}
return self;
}
- (id) init {
return [self initWithWidth:1 height:2]; //指定イニシャライザを通す
}
- (id) initWithSize:(CGSize) size {
self = [self initWithWidth:size.width height:size.height]; //これも指定イニシャライザを通す
if (self) { /* 他に必要ならここにかく */}
return self;
}

こうすれば、すべて指定イニシャライザを通すので サブクラスは指定イニシャライザをオーバーライドするだけで全イニシャライザが呼び出されたときの処理を記述できます

よってSquareはこうなります


Square.m

- (id) initWithLength:(float)length {

//省略
}
//親クラス指定イニシャライザは必ずオーバーライドする
- (id) initWithWidth:(float)width height:height {
//今回は幅高さの平均を正方形の大きさとする
float length = (width + height) / 2;
return [self initWithLength:length]; //このクラスの指定イニシャライザを通す
}

こうすることですべてのパターンでinitWithLengthを通り、予想どおり初期化ができるようになります。

ためしに [[Square alloc] init] が呼び出された場合だと


(1) Rectangleのほうのinitがマッチ。 initWithWidth:height: が呼び出される

(2) initWithWidth:height: はSquareでオーバーライドしているので、Squareのほうのメソッドがマッチする。

(3) SquareのほうのinitWithWidth:height: から指定イニシャライザ、initWithLength が呼び出される

(4) 指定イニシャライザが呼び出されて無事初期化完了


このように、ちゃんと指定イニシャライザの意味を理解し、実装すればSquare にinitがなくてもちゃんと初期化できるのです。

ただし initWithCoder:(NSCoder *)は特殊なので例外です。

これは親クラスの initWithCoder を呼び出すようにすれば正常に初期化できるはずです。

ぜひこのルールを守ってください。もう一度3つのルールを書きます


3 Rules for initializers

(1)必ず一つの指定イニシャライザを持つ(基本的に複数はナシ)

(2)指定イニシャライザの以外のイニシャライザは、必ず指定イニシャライザ経由で初期化する

(3)親クラスの指定イニシャライザは必ずオーバーライドする