今回のエンジニアブログ担当の山下です。
先日AppleのSwift Blogを眺めていたところ、
「Nullability and Objective-C」という気になる記事を見つけました。
もしかするとObjective-CでもSwiftのような安全なコーディングが出来るようになるのでは…!?と
若干の期待をしつつ試してみました。
nullableとnonnull
Objective-Cの新しい言語仕様ではnullableとnonnullの2つの注釈が追加されています。
それぞれnilやNULLが入る可能性のあるもの、入れることが認められないものを意味します。
注釈は*記号の後に記述します。
nullを許容しないプロパティの例を以下に示します。
@interface ObjCClass : NSObject @property NSString * __nonnull string; @end @implementation ObjCClass - (void)setNil { self.string = nil; } @end
ちなみにプロパティにnonnullやnullableを指定する際は、属性に記述することもできます。
このときは頭にアンダーライン(_ )2つを付ける必要はありません。
以下の2行は同じ意味となります。
@property NSString * __nonnull string; @property(nonnull) NSString * string;
メソッドの戻り値や引数の型にもこのアンダーラインを付けない書き方が可能です。
- (nonnull NSString *)method:(nonnull NSString *)string;
こちらを積極的に使った方が書きやすいと思います。
さて先ほどのクラス定義のコードに戻ると、stringプロパティは__nonnullを追加することで
nullを許容しないようになりました。
ここで-setNilメソッドでnilを代入していますが、ビルド時には警告が出るようになります。
しかしこの警告はプロパティに対する直接的な代入時にしか表示されないようで、
以下のように間接的に代入すると警告は出ませんでした。
この時点で早くもObjective-Cを安全に書くという夢は打ち砕かれました。
- (void)setNil { NSString *string = nil; self.string = string; }
Swiftとの連携
ではこのnonnullやnullableを付けたObjective-Cのクラスを、
Swiftから参照するとどうなるか見てみましょう。
SwiftとObjective-Cの連携の方法は過去の記事を参照してください。
参考: SwiftとObjective-Cを共存させる
まず以下のようなObjective-Cのクラスを用意します。
NSStringオブジェクトを返すメソッドにそれぞれ注釈を追加しています。
- 従来通りの宣言
- 宣言にnonnullアノテーションを追加
- 宣言にnullableアノテーションを追加
@interface ObjCClass : NSObject - (NSString *)getString; - (nonnull NSString *)getNonNullString; - (nullable NSString *)getNullableString; @end @implementation ObjCClass - (NSString *)getString { return @"Hello World!"; } - (nonnull NSString *)getNonNullString { return @"Hello World!"; } - (nullable NSString *)getNullableString { return @"Hello World!"; } @end
このクラスの各メソッドを以下のSwiftコードで呼び出したとき、
各変数はどんな型に推論されるのかを検証してみました。
var object = ObjCClass() let string = object.getString() let nonNullString = object.getNonNullString() let nullableString = object.getNullableString()
※XcodeではOptionキーを押しながら変数名をクリックすることで、
変数の型を調べることが出来ます。
従来通りの書き方
まず上のObjective-Cクラスを実装したとき、getStringメソッドの宣言に以下の警告がつきます。
Pointer is missing a nullability type specifier (nonnull or nullable)
同じクラス内にnonnullかnullableが付けられたメソッドかプロパティが一つでも宣言されていれば、
クラス内全ての宣言で明示するよう圧力がかかるようです。
ではgetStringメソッドの返却値を見てみましょう。
従来通り、注釈を付けずに書いたものは、SwiftではString!型として扱われています。
暗黙的にString型として扱うことができますが、
万が一getStringメソッドがnilを返してきた場合アプリがクラッシュする原因になります。
nonnullを付けた場合
getNonNullStringメソッドには、nonnull注釈を付けています。
Swiftで返却値の型を調べると、OptionalでないString型として扱われています。
このメソッドが確実にnilを返さないことはObjective-C側で保証する必要があります。
仮にnilを返してもビルド時にエラーは出ませんが、実行時にはクラッシュします。
nullableを付けた場合
getNullableStringメソッドには、nullable注釈を付けています。
Swiftでは返却値の型はString?型と推論されました。
中の文字列を扱うにはif let等でのunwrapが必要になり、
Swift側で安全にコードを書くことが出来ます。
範囲によるnonnull指定
ここまで試してみて、プロパティやメソッドに一通りnonnullかnullableを付けたくなると思います。
ただコード全体を書き換える必要があるので、これにはより簡単な方法が用意されています。
以下のようにNS_ASSUME_NONNULL_BEGINとNS_ASSUME_NONNULL_ENDで囲うことで、
明示していないポインタをnonnullとして扱うことができます。
NS_ASSUME_NONNULL_BEGIN @interface ObjCClass : NSObject - (NSString *)getString; // nonnullとして扱われる @end NS_ASSUME_NONNULL_END
あとはnullableにしたい部分だけnullableを明示するだけです。
終わりに
というわけで、あくまでもSwiftとのスムーズな連携が目的であり、
Objective-Cでのnilの取り扱いはプログラマに任せられているというオチでした。
しかしながらSwiftとObjective-Cのコードを共存させる時には
nilが渡される可能性のある場所、そうでない場所を明記しておくメリットは大きいと思いました。