なぜNSStringをプロパティにする際にcopyオプションを選択すべきなのか

  • 96
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

さて、NSStringをプロパティとして保持すること、たくさんありますよね。Googleのコーディング規約にもcopyにするよう書いてあります。

自分もそうしていたのですが、なぜそうしなければいけないのかというところまで考えないエンジニア失格状態だったのでちゃんと考えてみました。

さて、そもそもcopyメソッドとは何なのか?

copyメソッド

copyメソッドはNSObjectで定義されているのですが、copyメソッドを呼ぶとcopyWithZone:メソッドが内部的に呼ばれるようになっています。

APIリファレンスを見るとしっかりと書かれています。

Return Value
The object returned by the NSCopying protocol method copyWithZone:,.

copyWithZone:メソッドはNSCopyingプロトコルのメソッドです。ただしNSObjectcopyメソッドの定義はしていますがNSCopyingプロトコルは採用していません。copyWithZone:が実装されていない場合には例外が発生します。

copyWithZone:メソッド

前述の通りNSCopyingプロトコルのメソッドです。

- (id)copyWithZone:(NSZone *)zone

引数はNSZone型のオブジェクトです。ゾーンは以前、実行効率を向上されるために使用されていたそうですが、もう使われていないとのこと(ゾーンについてはこちらを参照)。現在はcopyメソッドが呼ばれた際は引数にNULLを指定して自身のcopyWithZone:を呼び出すようになっています。

copyWithZone:についてAPIリファレンスを見てみると、このようなことが書かれています。

The copy returned is immutable if the consideration “immutable vs. mutable” applies to the receiving object; otherwise the exact nature of the copy is determined by the class.

immutableなオブジェクトを返すようになっています。ただcopyの振る舞いはクラスによって変わってくるようです。

プロパティのcopyについて

copyがクラスによって振る舞いを変えるということが分かりました。

ここでプロパティのオプションにcopyを指定すると、内部的にどのような処理が行われるか見てみましょう。

- (void)setName:(TYPE)obj {
    if (name != obj) {
        name = [obj copy];
    }
}

copyを指定した場合、このようにセッタが作られます。copyWithZone:を考慮すると、mutableなオブジェクトがセットされるとimmutableなオブジェクトになります。

NSStringのcopyメソッド

ここでようやくNSStringに触れられます。NSStringクラスのインスタンスにcopyを送っても同じアドレスを指すようになっています。

NSString *string = NSStringFromClass([self class]);
NSLog(@"%p",string); // 0x8dbb950
NSLog(@"%p",[string copy]); // 0x8dbb950

そもそもimmutable = 不変なオブジェクトで変わることは考えられないので、わざわざコピーを作る必要はないということなのでしょう。つまりimmutableなオブジェクトが渡された場合、(内部的にcopyは呼ばれるが)結果的にstrongと同じ動作になります。

NSMutableStringの場合は、通常のコピーが行われます。

NSMutableStringによる意図しない変更

ではプロパティのオプションをstrongにした場合に何が困るのでしょうか?

プロパティのオプションをstrongにした場合、参照しているNSMutableStringオブジェクトの値が変更されると、プロパティの値も変更されてしまい、意図しない変更が起きてしまいます。

それぞれプロパティを用意して試してみましょう。

ViewController.m
@interface ViewController ()
@property (nonatomic, strong) NSString *strongString;
@property (nonatomic, copy)   NSString *copiedString;
@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    NSMutableString *mutableString = [[NSMutableString alloc] initWithString:@"one, two, three"];

    self.strongString = mutableString;
    self.copiedString = mutableString;

    [mutableString appendString:@", four"];

    NSLog(@"%@",self.strongString);
    // one, two, three, four

    NSLog(@"%@",self.copiedString);
    // one, two, three
}

上記の例ではself.strongStringmutableStringと同じアドレスを指しているので、mutableStringが変更されたことによりself.strongStringも変更されてしまっています。

self.copiedStringの方は、copyが行われ新たなメモリ領域が作られます。よってmutableStringの変更の影響は受けません。

プロパティにcopyオプションを指定しておけば、NSMutableStringの代入を行って意図しない変更が発生する心配をせずに済みますね。

まとめ

以上、ここまでNSStringについて書きましたが、immutableなクラスのcopyがインスタンス自身を返すようになっていたり、mutableなクラスのcopyの返り値がimmutableなのはNSArrayNSDictionaryも同様です。

まとめるとこんな感じになります。

1.NSStringNSArrayなどのimmutableなクラスのcopyメソッドは同じアドレスを指すようになっている。よってstrongでもcopyでも動作は変わらない。

2.NSMutableStringcopyメソッドは、immutableなオブジェクトが返される。

mutableなクラスを渡されないことは保証できない、immutableなオブジェクトの場合はプロパティにcopyを指定してもstrongと変わらない動作をする。

これがcopyオプションを選択すべき理由です。