エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Objective-C 2.0のARCについて学ぶ

先のエントリで書いたように、かつてObjective-Cで挫折したのはメモリ管理の手法を身につけることができなかったからでした。今、Objective-Cに再挑戦中ですが、やっぱり従来の手動のメモリ管理の手法はよくわかりません(>_<)。ですが現在ではありがたいことにARC (Automatic Reference Counting)という機構を利用することができるようになったことで、その苦渋から脱出できそうな気配です。


とはいえ動作の基礎をキチンとおさえておかないと、ものがものだけに致命的な間違いに直結してしまいます。
そんなわけで。ARCがどのように機能するのか調べてみました。

その1:ループ内で宣言された変数に対してどう動く?

手始めに。次のようなコードを走らせてみます。

#import <Foundation/Foundation.h>
#import <stdio.h>

@interface Sample : NSObject

@property int number;

- (id)initWithNumber:(int)n;

@end

@implementation Sample

@synthesize number;

- (id)initWithNumber:(int)n
{
    self = [super init];
    if(self != nil)
    {
        self.number = n;
        printf("------------ Sample No.%d initializing\n", self.number);
    }
    return self;
}

- (void)dealloc
{
    printf("------------ Sample No.%d deallocating\n", self.number);
}

@end

void sample1()
{
    printf("    before for loop\n");
    for(int i = 0; i < 3; ++i)
    {
        printf("        head of for loop\n");
        Sample *s = [[Sample alloc] initWithNumber:i];
        printf("        No.%d\n", s.number);
        printf("        tail of for loop\n");
    }
    printf("    after for loop\n");
}

int main(int argc, char *argv[])
{
    printf("before sample1\n");
    sample1();
    printf("after sample1\n");
    return 0;
}


clangコマンドに-fobj-arcオプションをつけてコンパイルします。

$ clang -fobjc-arc -o sample1 sample1.m


実行結果。

 $ ./sample1 
 before sample1
     before for loop
         head of for loop
 ------------ Sample No.0 initializing
         No.0
         tail of for loop
 ------------ Sample No.0 deallocating
         head of for loop
 ------------ Sample No.1 initializing
         No.1
         tail of for loop
 ------------ Sample No.1 deallocating
         head of for loop
 ------------ Sample No.2 initializing
         No.2
         tail of for loop
 ------------ Sample No.2 deallocating
     after for loop
 after sample1


初期化のコードが「head of for loop」のうしろに書かれているので、「head of for loop」が表示されてから初期化されるのは当然なのですが、意外だったのが破棄が「tail of for loop」と「head of for loop」の間でおこなわれていること。ループの過程1回ごとに破棄がおこなわれているようです。

その2:ぢゃ、ループの外で変数を宣言したらどう動く?

コードを次のように変更してみます。
Sample *sをループの外側で宣言するようにしてみました。

#import <Foundation/Foundation.h>
#import <stdio.h>

// Sample クラスの定義は同じなので省略

void sample2()
{
    printf("    before for loop\n");
    Sample *s;
    for(int i = 0; i < 3; ++i)
    {
        printf("        head of for loop\n");
        s = [[Sample alloc] initWithNumber:i];
        printf("        No.%d\n", s.number);
        printf("        tail of for loop\n");
    }
    printf("    after for loop\n");
}

int main(int argc, char *argv[])
{
    printf("before sample2\n");
    sample2();
    printf("after sample2\n");
    return 0;
}


実行結果。

 $ ./sample2
 before sample2
     before for loop
         head of for loop
 ------------ Sample No.0 initializing
         No.0
         tail of for loop
         head of for loop
 ------------ Sample No.1 initializing
 ------------ Sample No.0 deallocating
         No.1
         tail of for loop
         head of for loop
 ------------ Sample No.2 initializing
 ------------ Sample No.1 deallocating
         No.2
         tail of for loop
     after for loop
 ------------ Sample No.2 deallocating
 after sample2


破棄の順番がsample1と変わりました。次のオブジェクトが初期化されてから前のオブジェクトが破棄されています。そして最後のオブジェクトは関数を抜けるときに破棄されています。
これはつまり、新しいオブジェクトが初期化され、sに新しいオブジェクト(のポインタ)が代入され、その結果古いオブジェクトはどこからも参照されなくなったため破棄される、ということと思われます。関数から抜けるときに最後のオブジェクトが破棄されるのも、関数から抜けるときに変数sが破棄されて参照する変数がなくなり最後のオブジェクトが破棄される、ということだと思います。

その3:ループの外の変数に代入したらどう動く?

次はsample1を次のように書き換えます。
ループの外に変数を用意し、ループの途中でオブジェクトをその変数に代入します。

#import <Foundation/Foundation.h>
#import <stdio.h>

// Sample クラスの定義は同じなので省略

void sample3()
{
    printf("    before for loop\n");
    Sample *s3;
    for(int i = 0; i < 3; ++i)
    {
        printf("        head of for loop\n");
        Sample *s = [[Sample alloc] initWithNumber:i];
        printf("        No.%d\n", s.number);
        printf("        tail of for loop\n");
        if(i == 1)
        {
            s3 = s;
        }
    }
    printf("    after for loop\n");
}

int main(int argc, char *argv[])
{
    printf("before sample3\n");
    sample3();
    printf("after sample3\n");
    return 0;
}


実行結果。

 $ ./sample3
 before sample3
     before for loop
         head of for loop
 ------------ Sample No.0 initializing
         No.0
         tail of for loop
 ------------ Sample No.0 deallocating
         head of for loop
 ------------ Sample No.1 initializing
         No.1
         tail of for loop
         head of for loop
 ------------ Sample No.2 initializing
         No.2
         tail of for loop
 ------------ Sample No.2 deallocating
     after for loop
 ------------ Sample No.1 deallocating
 after sample3


予想通りNo.1のオブジェクトは関数が抜けるとき、つまり参照している変数がスコープから外れるときに破棄がおこなわれるようになりました。

その4:ループを中断したらどう動く?

さらにsample1を次のように書き換えます。
continuebreakでループを中断しています。

#import <Foundation/Foundation.h>
#import <stdio.h>

// Sample クラスの定義は同じなので省略

void sample4()
{
    printf("    before for loop\n");
    for(int i = 0; i < 3; ++i)
    {
        printf("        head of for loop\n");
        Sample *s = [[Sample alloc] initWithNumber:i];

        if(i == 0)
        {
            continue;
        }

        printf("        No.%d\n", s.number);
        printf("        tail of for loop\n");

        if(i == 1)
        {
            break;
        }
    
    }
    printf("    after for loop\n");
}

int main(int argc, char *argv[])
{
    printf("before sample4\n");
    sample4();
    printf("after sample4\n");
    return 0;
}


実行結果。

 $ ./sample4
 before sample4
     before for loop
         head of for loop
 ------------ Sample No.0 initializing
 ------------ Sample No.0 deallocating
         head of for loop
 ------------ Sample No.1 initializing
         No.1
         tail of for loop
 ------------ Sample No.1 deallocating
     after for loop
 after sample4


キチンと初期化と破棄がおこなわれるようです。


その5:関数がオブジェクトを返したらどう動く?

sample3をもとに次のように書き換えてみます。
関数内で生成したオブジェクトを関数の戻り値として返却します。

#import <Foundation/Foundation.h>
#import <stdio.h>

// Sample クラスの定義は同じなので省略

Sample *sample5()
{
    printf("    before for loop\n");
    Sample *result;
    for(int i = 0; i < 3; ++i)
    {
        printf("        head of for loop\n");
        Sample *s = [[Sample alloc] initWithNumber:i];
        printf("        No.%d\n", s.number);
        printf("        tail of for loop\n");

        if(i == 1)
        {
            result = s;
        }
    
    }
    printf("    after for loop\n");
    return result;
}

int main(int argc, char *argv[])
{
    printf("before sample5\n");
    Sample *s = sample5();
    printf("after sample5\n");
    return 0;
}


実行結果。

$ ./sample5
 before sample5
     before for loop
         head of for loop
 ------------ Sample No.0 initializing
         No.0
         tail of for loop
 ------------ Sample No.0 deallocating
         head of for loop
 ------------ Sample No.1 initializing
         No.1
         tail of for loop
         head of for loop
 ------------ Sample No.2 initializing
         No.2
         tail of for loop
 ------------ Sample No.2 deallocating
     after for loop
 after sample5


返却されたNo.1のオブジェクトが破棄されなくなりました。返却する側の関数からすれば、オブジェクトを返すのでそれを破棄してはいけないのは当然のところ。いっぽう受け取ったmain関数の側で変数に代入しているので、その変数が破棄されるとき(main関数から抜けるとき)にオブジェクトの破棄がおこるのかと想像したのですが、これは完全に外れました。


関数からオブジェクトを受け取る場合、次のように書く必要があるようです。

// 省略

int main(int argc, char *argv[])
{
    @autoreleasepool
    {
        printf("before sample5\n");
        Sample *s = sample5();
        printf("after sample5\n");
    }
    return 0;
}


実行結果。

 $ ./sample5
 before sample5
     before for loop
         head of for loop
 ------------ Sample No.0 initializing
         No.0
         tail of for loop
 ------------ Sample No.0 deallocating
         head of for loop
 ------------ Sample No.1 initializing
         No.1
         tail of for loop
         head of for loop
 ------------ Sample No.2 initializing
         No.2
         tail of for loop
 ------------ Sample No.2 deallocating
     after for loop
 after sample5
 ------------ Sample No.1 deallocating

@autoreleasepoolのスコープから抜けるときに関数の戻り値で受け取ったオブジェクトが破棄されているのがわかります。


ここは要注意。

その6:メソッドがオブジェクトを返したらどう動く?


次のようなコードを試してみます。
その5のように関数でなく、メソッドでオブジェクトを返しています。

#import <Foundation/Foundation.h>
#import <stdio.h>

@interface Sample : NSObject

@property int number;

+ (Sample*)sample;
- (id)initWithNumber:(int)n;

@end

@implementation Sample

@synthesize number;

+ (Sample*)sample
{
    printf("    enter doSomething\n");
    Sample *result = [[self alloc] initWithNumber:1];
    printf("    exit doSomething\n");
    return result;
}

- (id)initWithNumber:(int)n
{
    self = [super init];
    if(self != nil)
    {
        self.number = n;
        printf("------------ Sample No.%d initializing\n", self.number);
    }
    return self;
}

- (void)dealloc
{
    printf("------------ Sample No.%d deallocating\n", self.number);
}

@end

int main(int argc, char *argv[])
{
    printf("before sample6\n");
    Sample *s = [Sample sample];
    printf("after sample6\n");
    return 0;
}


実行結果。

 $ ./sample6
 before sample6
     enter doSomething
 ------------ Sample No.1 initializing
     exit doSomething
 after sample6


この場合もオブジェクトは破棄されませんでした。
オブジェクトを返すのが関数かメソッドかは関係なく、どうやら、あるスコープ内で生成がおこなわれているか否かで挙動が変わるようです。


オブジェクトを返すクラスメソッドはライブラリ中に頻繁に出てきます。またほぼ同じ動作をしながらオブジェクトを生成せず初期化をするメソッドというのも存在します。
たとえば次の2つはどちらも文字列からNSStringのオブジェクトを生成して返すコードです。

NSString *s1 = [[NSString alloc] initWithString:s]; // 1
NSString *s2 = [NSString stringWithString:s];       // 2

これらのばあい、1ではコード内でオブジェクトを生成をしているので変数s1がスコープか外れた時点で参照カウンタが減るのに対し、2の場合はメソッド内でオブジェクトを生成しているので変数s2がスコープから外れても参照カウンタが減らないと想像できます。

だいたいわかってきたけれど…

ARCはスマートポインタとほぼ同じものを提供してくれるものかと勝手に想像していたのですが、どうやらそういうわけではなさそうです。このあたりはもう少し慣れが必要な気配です。