先のエントリで書いたように、かつて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を次のように書き換えます。
continue
とbreak
でループを中断しています。
#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はスマートポインタとほぼ同じものを提供してくれるものかと勝手に想像していたのですが、どうやらそういうわけではなさそうです。このあたりはもう少し慣れが必要な気配です。