工場裏のアーカイブス

素人によるiPhoneアプリ開発の学習記 あと機械学習とかM5Stackとか

ARCについて色々実験(2)

ARCについて色々実験(1)の続きとなります。

自作のイニシャライザによるオブジェクトの生成

実験2-01

srcARC1-01 の myClass1 は num というインスタンス変数を持っていますが、この値を設定する方法が用意されていませんでした。そこで、この myClass1 に引数を持つイニシャライザを追加して、引数により num の値を設定出来るようにしてみます。また、それに合わせて main 関数の中身も for ループを用いたものに書き換えます。それを srcARC2-01 として以下に示します。

//srcARC2-01
//当然ながら、ARCをオンにしてのコンパイルが必須です

#import <Foundation/Foundation.h>

@interface myClass1 : NSObject{
    int num;
}
@end

@implementation myClass1

//イニシャライザを追加
-(id)initWithNumber:(int)n{
    self = [super init];
    if(self != nil){
        num = n;
        NSLog(@"******** myClass No.%i イニシャライザにより初期化\n", n);
    }
    
    return self;
}

-(void)showNumber{
    NSLog(@"******** 私は myClass No.%iです。\n", num);
}

-(void)dealloc
{
    NSLog(@"******** myClass No.%i 解放\n", num);
}

@end


int main(int argc, const char * argv[])
{
    //for ループを用いたものに書き換え

    NSLog(@"** ループ開始");
    for(int i = 1; i <= 3; i++){
        NSLog(@"**** ループ%i週目 先頭\n", i);
            
        id mc = [[myClass1 alloc] initWithNumber:i];
        [mc showNumber];
            
        NSLog(@"**** ループ%i週目 末尾\n", i);
    }
    NSLog(@"** ループ終了\n");
    
    return 0;
}

イニシャライザであるinitWithNumber: メソッドは引数 n を持ち、この値によってインスタンス変数 num を初期化出来ます。なお、イニシャライザを定義するときには

self = [super init];

のように、 self にスーパークラスのイニシャライザの戻り値を代入する書き方が定石となっています。しかしARC利用時には、init メソッドファミリに属するメソッド以外のメソッドで、self にオブジェクトを代入するとエラーが出るようです(initWithNumber: は init メソッドファミリであるためOK)。 つまりイニシャライザは、ARCでは必ず init メソッドファミリである必要があるようです。

これを実行すると、以下のログが出力されます。

** ループ開始
**** ループ1週目 先頭
******** myClass No.1 イニシャライザにより初期化
******** 私は myClass No.1です。
**** ループ1週目 末尾
******** myClass No.1 解放
**** ループ2週目 先頭
******** myClass No.2 イニシャライザにより初期化
******** 私は myClass No.2です。
**** ループ2週目 末尾
******** myClass No.2 解放
**** ループ3週目 先頭
******** myClass No.3 イニシャライザにより初期化
******** 私は myClass No.3です。
**** ループ3週目 末尾
******** myClass No.3 解放
** ループ終了

initWithNumber: はメソッドファミリに属するため、これ(と alloc)により生成されたオブジェクトを変数 mc (デフォルトで強参照)に代入することによって、mc はオブジェクトのオーナーになります。そして mc は for ループ内のローカル変数であるため、ループが回るたびにその末尾で消滅し、ここで mc からの強参照が外れたオブジェクトの dealloc が呼び出されて解放されています。


実験2-02

次に、srcARC2-01 の main 関数を以下の srcARC2-02 のように書き換えて、変数 mc の宣言を for ループの外側に持ってきます。

//srcARC2-02
//当然ながら、ARCをオンにしてのコンパイルが必須です

//※ main 関数以外は、srcARC2-01 と同一

int main(int argc, const char * argv[])
{                
    id mc; 

    NSLog(@"** ループ開始");
    for(int i = 1; i <= 3; i++){
        NSLog(@"**** ループ%i週目 先頭\n", i);

        mc = [[myClass1 alloc] initWithNumber:i]; 
        [mc showNumber];
            
        NSLog(@"**** ループ%i週目 末尾\n", i);
    }
    NSLog(@"** ループ終了\n");
    
    return 0;
}

srcARC2-02 を実行すると、以下のログが出力されます。

** ループ開始
**** ループ1週目 先頭
******** myClass No.1 イニシャライザにより初期化
******** 私は myClass No.1です。
**** ループ1週目 末尾
**** ループ2週目 先頭
******** myClass No.2 イニシャライザにより初期化
******** myClass No.1 解放
******** 私は myClass No.2です。
**** ループ2週目 末尾
**** ループ3週目 先頭
******** myClass No.3 イニシャライザにより初期化
******** myClass No.2 解放
******** 私は myClass No.3です。
**** ループ3週目 末尾
** ループ終了
******** myClass No.3 解放

今度は変数 mc は、for ループに関係なく main 関数の最後まで存在し続けます。そのため srcARC2-01 とは違い、ループの末尾でオブジェクトの解放が起こることはありません。そして次回のループの冒頭で、新しいオブジェクト(例えば「myClass No.2」)が生成されて mc に代入されると、当然古いオブジェクト(例えば「myClass No.1」)への強参照が外れます。このとき、この古いオブジェクトの dealloc が呼び出されて解放されています。

また、最後の回のループ(3週目)で生成される「myClass No.3」が mc に代入されると、それ以降 mc には代入が起こりません。そのため「myClass No.3」は mc に保持されたまま main 関数の最後まで存在し続けます。


init 以外のメソッドファミリに属するメソッドによるオブジェクトの生成

実験2-03

実は、「詳解 Objective-C 2.0 第3版」によると、メソッドファミリの中でも init とそれ以外(alloc、copy、mutableCopy、new)のメソッドファミリでは、若干扱いが異なるようです。

init メソッドファミリについては、書籍中で以下のように説明されています。

init メソッドファミリはインスタンスメソッドでなければならず、Objective-C のポインタタイプを返さなければならない。返り値は id 型か、宣言しているクラスのスーパークラスかサブクラスへのポインタでなければならない。

一方、それ以外の全てのメソッドファミリについては、以下のように説明されています。クラスメソッドとインスタンスメソッドの区別も無いそうです。

メソッドは保持可能なオブジェクトへのポインタを返さなくてはならない。

これによると、やはりARCでは init メソッドファミリに属するメソッドは、イニシャライザとして用いることが前提となっているようです。そもそもObjective-Cでは、ARCが導入されるもっと以前から、イニシャライザの名前は init または init で始まる文字列とするのが慣例であったそうです。その慣例が init メソッドファミリという形で、文法的なルールとして取り入れられたという所でしょうか(あくまで自分なりの理解ですので、間違っているかもしれませんが)。

一方で、それ以外のメソッドファミリには init ほどの制約は無いようです。そこで、srcARC2-01 の myClass1 に、以下の srcARC2-03 のように newMyClassWithNumber: というクラスメソッド(これは new メソッドファミリに属します)を追加してみます。main 関数の内容も、このメソッドを用いるように少しだけ書き換えてあります。

//srcARC2-03
//当然ながら、ARCをオンにしてのコンパイルが必須です

#import <Foundation/Foundation.h>

@interface myClass1 : NSObject{
    int num;
}
@end

@implementation myClass1

-(id)initWithNumber:(int)n{
    self = [super init];
    if(self != nil){
        num = n;
        //NSLog(@"******** myClass No.%i イニシャライザにより初期化\n", n);
    }
    
    return self;
}

//このクラスメソッドを追加
+(id)newMyClassWithNumber:(int)n{
    NSLog(@"******** myClass No.%i newMyClassWithNumber:で生成\n", n);
    return [[self alloc] initWithNumber:n];
}

-(void)showNumber{
    NSLog(@"******** 私は myClass No.%iです。\n", num);
}

-(void)dealloc
{
    NSLog(@"******** myClass No.%i 解放\n", num);
}

@end


int main(int argc, const char * argv[])
{
    NSLog(@"** ループ開始");
    for(int i = 1; i <= 3; i++){
        NSLog(@"**** ループ%i週目 先頭\n", i);
            
        id mc = [myClass1 newMyClassWithNumber:i];
        [mc showNumber];
            
        NSLog(@"**** ループ%i週目 末尾\n", i);
    }
    NSLog(@"** ループ終了\n");
    
    return 0;
}

newMyClassWithNumber: メソッドでは、以下のように alloc と initWithNumber: によってオブジェクトを生成し、それを戻り値として返します(このメソッドはクラスメソッドなので、ここでの self は myClass1 というクラス自体を指すことに注意が必要です)。

return [[self alloc] initWithNumber:n];

これを実行すると、以下のログが出力されます。

** ループ開始
**** ループ1週目 先頭
******** myClass No.1 newMyClassWithNumber:で生成
******** 私は myClass No.1です。
**** ループ1週目 末尾
******** myClass No.1 解放
**** ループ2週目 先頭
******** myClass No.2 newMyClassWithNumber:で生成
******** 私は myClass No.2です。
**** ループ2週目 末尾
******** myClass No.2 解放
**** ループ3週目 先頭
******** myClass No.3 newMyClassWithNumber:で生成
******** 私は myClass No.3です。
**** ループ3週目 末尾
******** myClass No.3 解放
** ループ終了

「〜newMyClassWithNumber:で生成」と文章が書き変わっただけで、動作内容自体は srcARC2-01 と全く同じです。つまり、以下のように alloc と initWithNumber: メソッドを用いたオブジェクト生成手順を、ひとつのメソッドにまとめたことになります。

//以下の2つのコードは、同じ動作をする
id mc = [[myClass1 alloc] initWithNumber:i];

id mc = [myClass1 newMyClassWithNumber:i];

なお newMyClassWithNumber: メソッドは、例えば「copyMyClass〜」や「allocMyClass〜」という具合に、他のメソッドファミリに属するように名前を書き換えても、全く同様に動作してしまうようです(ただし「initMyClass〜」といった名前に書き換えると、メソッドファミリに属していると認識されなくなります。前述の 「init メソッドファミリはインスタンスメソッドでなくてはならない」という規則に反するからのようです)。

しかし、だからといってこのメソッドに「copyMyClass〜」や「allocMyClass〜」なんて名前を付けるべきでは無いでしょう。名前から予想されるメソッドの動作と、実際の動作が乖離してしまいます。メソッドファミリの各キーワードは、プログラマが意識的に、それぞれの名前に応じた適切な使い分けをすることが必要だと思います。


ここまでの実験では、メソッドファミリに属するメソッドが、戻り値としてオブジェクトを返す場合について検証してきました。それでは、メソッドファミリに属さないメソッドについてはどうでしょうか?例えば、newMyClassWithNumber: メソッドを、メソッドファミリに属さない名前に書き換えたら何が起こるのでしょうか?

それについては、ARCについて色々実験(3) 以降で実験をして行きます。