LoginSignup
20
20

More than 5 years have passed since last update.

ObjectiveCでストリームのcloseなどのクリーンアップの記述漏れを防ぐ

Posted at

open + 複数returnの憂鬱

以下のコードを見てください。

-(BOOL)hoge{
    NSInputStream *stream = [NSInputStream inputStreamWith...];
    if(!stream)return NO;
    [stream open];
    while(YES){
        uint8_t buf[1024];
        int readLen = [stream read:buf maxLength:sizeof(buf)];
        if(readLen == -1){
            [stream close]; // ★1
            return NO;
        }
        process(buf);
    }
    [stream close]; // ★2
    return YES;
}

こんな風に、ストリームをopenするコードでは、★1と★2のように、returnする前にcloseしなければなりません。でも★1を忘れそうで不安になりますよね。みなさんはこういうコードを書く時どのようにミスを防止していますか?
おそらく2つのやり方があると思います。

  • open以下をtryで括ってfinallyにcloseを書く
  • exitラベルを用意して、エラーの場合はgoto、returnを1箇所にする

個人的にはどっちのやり方もあまり好きではありません。C++ならRAIIとか、pythonならwith文とか、Goならdefer文とかあるのになぁ・・・、と思っていました。

Defer文、自作できるんじゃね?

  • そういえば、ARCのstrongポインタはスコープを抜けるとすぐ解放されます。
  • Blockを使えば、コードブロックをリテラルとして書けます。

こいつらを組み合わせたら、Go言語のdefer文みたいな事、できるんじゃないかと思いました。

やってみた

こんなのできました。

-(BOOL)hoge{
    NSInputStream *stream = [NSInputStream inputStreamWith...];
    if(!stream)return NO;
    [stream open];
    OMCDefer(
        [stream close]; //★
    );
    while(YES){
        uint8_t buf[1024];
        int readLen = [stream read:buf maxLength:sizeof(buf)];
        if(readLen == -1)return NO;
        process(buf);
    }
    return YES;
}

openの直後に、closeを実行するコードを一度だけ書いています。そして、どちらのreturnが呼び出されても、そのタイミングでcloseが実行されます。OMCDeferの中には、複数行のコードが書けます。

解説

用意したのはこんなコードです。

#define OMCDefer(code) _OMCDefer(code,__LINE__)
#define _OMCDefer(code,line) __OMCDefer(code,line)
#define __OMCDefer(code,line) \
OMCDeferObject * __strong _defer_##line = [[OMCDeferObject alloc]initWithBlock:^(){code}]; \
(void)_defer_##line;

@interface OMCDeferObject : NSObject
@property(nonatomic,copy)dispatch_block_t block;
-(id)initWithBlock:(dispatch_block_t)block;
@end

@implementation OMCDeferObject
-(id)initWithBlock:(dispatch_block_t)block{
    self = [super init];
    if(self){
        _block = [block copy];
    }
    return self;
}
-(void)dealloc{
    self.block();
}
@end

以下の様な流れになっています。

  1. OMCDeferはマクロになっていて、引数として複数行のコードを取ります。
  2. そのコードは、ブロックリテラルの中身の部分としてマクロ展開されます。
  3. そのブロックリテラルは、OMCDeferObjectのイニシャライザに渡されます。
  4. ブロックはイニシャライザの中でcopyされ保持されます。
  5. initされたOMCDeferObjectは、その場でstrongローカル変数にセットされます。
  6. スコープを抜ける時、そのローカル変数が解放されます。
  7. OMCDeferObjectが解放されるとき、deallocの中で、渡されていたブロックを実行します。
  8. 結果として、スコープ脱出時に、マクロに渡したコードが実行されます。
  • __LINE__は、OMCDeferを複数書いた時にローカル変数名が被らないようにしています。
  • (void)の部分は、その変数のunused警告を抑制しています。

ローカル変数自体がマクロによって作られるため、OMCDeferObjectが他の変数から参照され、生き延びてしまう事がありません。そのため確実にスコープ脱出時に解放されることになるのです。

注意

if文のブロックでも使えるし、try-catch-finallyと一緒にも使えるのですが、うまく動かないケースがあります。それは、 例外がthrowされ、それによってスコープを抜ける場合 です。その理由はclangのARCのページに書いてあります。

By default in Objective C, ARC is not exception-safe for normal releases:

It does not end the lifetime of __strong variables when their scopes are abnormally terminated by an exception.

例外のthrowによってスコープ脱出する時、strongの解放が行われないそうです。

The standard Cocoa convention is that exceptions signal programmer error and are not intended to be recovered from.

Cocoa規約によれば、例外を飛ばすのはプログラマにエラーがあった時にするそうです。なので、例外発生時はリカバリはせずにアプリはその場でクラッシュしましょう、だからメモリリークしても問題ないですね、という事らしいです。その考え方ならば、どうせアプリがクラッシュするんだから、ストリームのcloseができなくても問題ないというわけです・・・。

いくらなんでもそれは無い、という方はコンパイルオプション"-fobjc-arc-exceptions"を指定してビルドしてください。するとこの挙動が変わり、期待通りに解放されるようになります。

ソースコード

ソースコードをgithubで公開しています。 OMCViewController.mに、使用例を何パターンか書いてあります。また、プロジェクト設定で、-fobj-arc-exceptionsを有効にしてあります。

20
20
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
20
20