配列をプロパティにするときには注意する

Javaプログラマ向け、Objective-Cにおける配列プロパティの扱い。

自作クラスの属性で可変配列を管理したいということはよくある。
そして、その内部配列を読み取り専用として外部に公開したいということもあるだろう。

たとえば、以下のようなチャットルームクラスを考えてみよう。
チャットルームクラスは、

  • チャットルームにユーザーを参加させるメソッドを持つ
  • チャットルームからユーザーを退室させるメソッドを持つ
  • チャットルーム内の全ユーザーを配列で返す読み取り専用プロパティを持つ

とする。

この場合、以下のように単純に実装してしまうと、少々危険である。

@interface ChatUser : NSObject
@end

@implementation ChatUser
@end

@interface ChatRoom : NSObject
{
    NSMutableArray* _users;
}

// チャットルーム内のユーザーを参照専用プロパティとして公開
@property(readonly) NSArray* users;

-(void)join:(ChatUser*)user;
-(void)leave:(ChatUser*)user;
@end

@implementation ChatRoom
@synthesize users = _users;

-(id)init {
    self = [super init];
    
    if (self) {
        _users = [[NSMutableArray alloc] init];
    }
    
    return self;
}

-(void)dealloc {
    [_users release];
    [super dealloc];
}

-(void)join:(ChatUser*)user {
    [_users addObject:user];
}

-(void)leave:(ChatUser*)user {
    [_users removeObject:user];
}

@end



int main(int argc, const char** argv)
{
    @autoreleasepool {
        ChatRoom* room = [[[ChatRoom alloc] init] autorelease];
        
        // クラスの外部からroomオブジェクト内部の配列を破壊できてしまう。
        NSMutableArray* array = (NSMutableArray*)room.users;
        [array addObject:@"aaa"];
        
        for (id user in room.users) {
            NSLog(@"%@", user);    // aaa
        }
    }
}


この実装の問題は、可変配列をプロパティでそのまま公開してしまっていることにある。
変更不可能な配列として公開するには、以下のように取得メソッドを自分で作成するとよいだろう。

@interface ChatUser : NSObject
@end

@implementation ChatUser
@end

@interface ChatRoom : NSObject
{
    NSMutableArray* _users;
}

// チャットルーム内のユーザーを参照専用プロパティとして公開
@property(readonly, getter = getUsers) NSArray* users;

-(void)join:(ChatUser*)user;
-(void)leave:(ChatUser*)user;
@end

@implementation ChatRoom

-(id)init {
    self = [super init];
    
    if (self) {
        _users = [[NSMutableArray alloc] init];
    }
    
    return self;
}

-(void)dealloc {
    [_users release];
    [super dealloc];
}

-(void)join:(ChatUser*)user {
    [_users addObject:user];
}

-(void)leave:(ChatUser*)user {
    [_users removeObject:user];
}

// 変更不能な配列としてコピーした上で公開する
-(NSArray*)getUsers {
    return [[_users copy] autorelease];
}

@end

このやり方は、Javaでいうところの、java.util.Collections#unmodifiableList を使ったテクニックに似ている。
オブジェクトのコピーについては、過去の記事で詳しく解説している。
http://d.hatena.ne.jp/unk_pizza/20130725/p1


もし、似たような状況で、クラス内で配列メンバを変更することよりも、参照することの方が圧倒的に多いのであれば、次のようにメンバ配列自体を変更不能にしておいて、必要なときにだけ変更可能配列にする方が堅固である。

@interface ChatUser : NSObject
@end

@implementation ChatUser
@end

@interface ChatRoom : NSObject
{
    // チャットルーム内のユーザーを変更不能配列で保持する
    NSArray* _users;
}

// チャットルーム内のユーザーを参照専用プロパティとして公開
@property(readonly) NSArray* users;

-(void)join:(ChatUser*)user;
-(void)leave:(ChatUser*)user;
@end

@implementation ChatRoom

-(id)init {
    self = [super init];
    
    if (self) {
        _users = [[NSArray alloc] init];
    }
    
    return self;
}

-(void)dealloc {
    [_users release];
    [super dealloc];
}

-(void)join:(ChatUser*)user {
    NSMutableArray* users = [[_users mutableCopy] autorelease];
    [users addObject:user];
    [_users release];
    _users = [users copy];
}

-(void)leave:(ChatUser*)user {
    NSMutableArray* users = [[_users mutableCopy] autorelease];
    [users removeObject:user];
    [_users release];
    _users = [users copy];
}

@end

この実装は、外部からの配列プロパティの参照操作については、先ほどのカスタムゲッタでcopyして返すやり方よりも高速になる。ただし、配列の要素数が多い場合は、変更操作にコストがかかる。
また、変更操作を行う際に、オブジェクト参照の切り替えが頻繁に発生するため、GCやARCを使わない場合は、メモリの解放し忘れに注意が必要だ。