Objective-C のプロパティの属性指定のまとめ


追記

こっちの記事の方がよっぽどまとまってた

はじめに

Objective-C にはプロパティという機能があり、これには属性を設定することができます。この記事ではプロパティに設定する属性についてまとめます。

まとめ

下記に結論をまとめます。詳細は各章で説明します。

  • 常に nonatomic を使う
  • assign/retain/strong は使わない
  • Delegate は weak を使う
  • getter/setter は極力使わない
  • nonnull は使わず、 nullable のみ使う
  • 不変クラスは copy を使う。可変クラスは copy を使わない
  • 読み込みプロパティは、ヘッダで readonly を使い、実装で readwrite を使う
  • class を使う場合は自動生成されないことに注意する

常に nonatomic を使う

一般に、プロパティへのアクセスは原子性(atomic)を担保するために、ロック処理が自動的に生成されます。プロパティへのアクセスのみがロック処理の対象となるため、あまり有効活用できません。ロックが必要な場合は自前でロック処理を書いたほうが良いです。
ロック処理の自動生成を抑制するために nonatomic を使います。

assign/retain/strong は使わない

assign/retain は strong/weak に置き換わりました。また、strong は省略可能なので、これら 3 つの属性は指定しません。

Delegate は weak を使う

Delegate など、所有権をもたないプロパティは weak を使います。

getter/setter は使わない

よほど特殊な状況でない限り、getter/setter のアクセサ名を変えるべきではありません。

nonnull は使わず、nullable のみ使う

基本的には全てのオブジェクトは nonnull であるほうが使いやすいです。そのため、コード上では NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END で囲み、nonnull を明示せず、nullable なプロパティを明示すると良いです。
最新の Xcode では、デフォルトで NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END で囲われるようになるので、このマクロはもはや意識しなくて良くなりました。

不変クラスは copy を使う。可変クラスは copy を使わない

ここでいう「不変クラス」は、クラス階層の中で Mutable クラスを持つクラスのことです。例えば NSString や NSArray になります。これらの可変クラスは NSMutableString, NSMutableArray となります。
不変クラスをプロパティとする場合は、必ず copy 属性をつける必要があります。これはサブクラスである可変クラスを設定された場合、そのプロパティがその他の場所で変更されてしまうからです。

Test.m
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Hoge : NSObject
@property(nonatomic) NSString *foo;
@end

@implementation Hoge
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Hoge *hoge = [Hoge new];
        NSMutableString *foo = [@"foo" mutableCopy];
        hoge.foo = foo;
        NSLog(@"hoge.foo: %@", hoge.foo); // hoge.foo: foo
        [foo setString:@"bar"];
        NSLog(@"hoge.foo: %@", hoge.foo); // hoge.foo: bar
    }
    return 0;
}

NS_ASSUME_NONNULL_END

これを防ぐためには foo プロパティに copy 属性をつけます。

Test.m
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Hoge : NSObject
@property(nonatomic, copy) NSString *foo;
@end

@implementation Hoge
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Hoge *hoge = [Hoge new];
        NSMutableString *foo = [@"foo" mutableCopy];
        hoge.foo = foo; // ここで hoge.foo = [foo copy] となる。
        NSLog(@"hoge.foo: %@", hoge.foo); // hoge.foo: foo
        [foo setString:@"bar"];
        NSLog(@"hoge.foo: %@", hoge.foo); // hoge.foo: foo
    }
    return 0;
}

NS_ASSUME_NONNULL_END

一方、可変クラスをプロパティの型として使う場合、copy 属性を使ってはいけません。copy 属性を使うことで副作用のあるメソッドへのアクセスができないインスタンスがプロパティに設定されます。プロパティの型は可変クラスであるため、副作用のあるメソッドへのアクセスはコンパイラでチェックできません。

Test.m
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Hoge : NSObject
@property(nonatomic, copy) NSMutableString *foo;
@end

@implementation Hoge
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Hoge *hoge = [Hoge new];
        NSMutableString *foo = [@"foo" mutableCopy];
        hoge.foo = foo; // ここで hoge.foo = [foo copy] となる。
        NSLog(@"hoge.foo: %@", hoge.foo);
        [hoge.foo setString:@"bar"]; // signal SIGABORT
        NSLog(@"hoge.foo: %@", hoge.foo);
    }
    return 0;
}

NS_ASSUME_NONNULL_END

このケースでは、可変クラス型のプロパティに copy 属性をつけないことで回避できます。

Test.m
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface Hoge : NSObject
@property(nonatomic) NSMutableString *foo;
@end

@implementation Hoge
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Hoge *hoge = [Hoge new];
        NSMutableString *foo = [@"foo" mutableCopy];
        hoge.foo = foo;
        NSLog(@"hoge.foo: %@", hoge.foo);
        [hoge.foo setString:@"bar"];
        NSLog(@"hoge.foo: %@", hoge.foo);
    }
    return 0;
}

NS_ASSUME_NONNULL_END

読み込みプロパティは、ヘッダで readonly を使い、実装で readwrite を使う

ヘッダに readonly のみで定義してしまうと、自動生成メソッドはゲッターしか生成されません。実装側でセッターがないと不便なことが多いので、無名カテゴリに readwrite で同名のプロパティを宣言します。

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

NS_ASSUME_NONNULL_BEGIN

@interface Hoge : NSObject
@property(nonatomic, readonly) NSString *fuga;
@end

NS_ASSUME_NONNULL_END
Hoge.m
#import "Hoge.h"

NS_ASSUME_NONNULL_BEGIN

@interface Hoge ()
@property(nonatomic, readwrite, copy) NSString *fuga;
@end

@implementation Hoge
@end

NS_ASSUME_NONNULL_END

class を使う場合は自動生成されないことに注意する

Xcode の最新のバージョンでは class という属性が使えます。主にシングルトンクラスのプロパティである sharedInstance の実装に使われます。
ただし、この属性がついたプロパティは他のプロパティと違って自動生成されないことに注意してください。

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

NS_ASSUME_NONNULL_BEGIN

@interface Singleton : NSObject
@property(class, nonatomic, readonly) Singleton *sharedSingleton;
@end

NS_ASSUME_NONNULL_END
Singleton.m
#import "Singleton.h"

NS_ASSUME_NONNULL_BEGIN

static Singleton *_sharedSingleton;

@implementation Singleton

+ (Singleton *)sharedSingleton {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _sharedSingleton = [Singleton new];
    });
    return _sharedSingleton;
}

@end

NS_ASSUME_NONNULL_END

最後に

社内で Objective-C のプロパティに指定する属性についての指針がないことが嘆かれていたので、まとめてみました。個人的には、今更 Objective-C のプロパティの属性に悩むのではなく、Swift 移行が進んでそもそも悩む必要がなくなるといいなと思っています。