SwiftのOptionalってなんじゃい!?〜「(null)を食べました」編〜

こんにちは、ご無沙汰です。おひろです。

最近Swiftで1プロジェクト実装しました。
そこで良い点などを、書いてみようかと思ったのですが、Swiftならでは
という観点で見ると実はあまり無いのでは、という気になっています。

Objective-Cと比べればいろいろ便利なところはありますが、
最近の言語では普通にやっているようなことが多いので、それを書いても微妙だなと。。
Swiftの本質をまだまだ理解できていないだけかもしれないですけど。。

そんな中で、良いなと思ったのはOptional型という仕組みです。
今回はこれについて書こうと思います。

多分最初にSwiftを始めたときにナンジャコレと、ジャマクサイナコレと
感じる概念かと思います。僕も最初はそう思いましたが、今ではコイツが好きになりました。

Optional型の重要なポイント(良い点)は以下の2つかと思っています。
・変数にnilを代入することを許容するかどうかを明示的にできる(nilチェックが減り、よりスッキリ安全なコードが書ける。)
・その変数を使うとき、また代入するときにnilを考慮した実装をしていなければコンパイラが教えてくれる

例を挙げます。
Objective-C以下のようなクラスがあったとします。

// ヘッダ
@interface Ohiro : NSObject
- (instancetype)initWithFavoriteFood:(NSString *)favoriteFood;
- (void)goToTenma;
@end

// 実装
@interface Ohiro () {
}
@property (nonatomic, readonly, copy) NSString *favoriteFood;
@end

@implementation Ohiro

- (instancetype)initWithFavoriteFood:(NSString *)favoriteFood {
    self = [super init];
    if (self) {
        _favoriteFood = [favoriteFood copy];
    }
    return self;
}

- (void)goToTenma {
    NSLog(@"おひろは天満に行って%@を食べました。", self.favoriteFood);
}

@end

これをSwiftで書こうと思うと、実は全く同じ挙動をするものを書くことはできません。
できるだけ近しい形で書くと、以下のようになると思います。

class Ohiro {

    private var favoriteFood: String?

    init(favoriteFood: String?) {
        self.favoriteFood = favoriteFood
    }

    func goToTenma() {
        println("おひろは天満に行って\(self.favoriteFood!)を食べました。")
    }
}

「String?」と型の後ろに?をつけることでOptional型となります。
そしてfavoriteFoodを使用する際に!を変数のおしりにつけていますがこれを「アンラップ」と言います。

なおOptional型で宣言するとself.favoriteFoodのように直接変数を使用することはできません。(コンパイルエラーになる)
アンラップするかself.favoriteFood?のように「?」をつけてOptional型として使用する必要があります。

Optional型として使用した場合には、変数がnilだった場合にはメソッドの実行などは無視されます。
その辺の挙動としてはObjective-Cと同じです。

さて以下のようにオブジェクトを生成してメソッドを呼ぶと何と表示されるでしょう。

// Objective-C
Ohiro *ohiro = [[Ohiro alloc] initWithFavoriteFood:@"串カツ"];
[ohiro goToTenma];

// Swift
let ohiro = Ohiro(favoriteFood: "串カツ")
ohiro.goToTenma()

「おひろは天満に行って串カツを食べました。」とコンソールに表示されます。
串カツが食べたくなりましたね。

じゃあ次のようにnilを渡すとどうなるでしょう。

// Objective-C
Ohiro *ohiro = [[Ohiro alloc] initWithFavoriteFood:nil];
[ohiro goToTenma];

// Swift
let ohiro = Ohiro(favoriteFood: nil)
ohiro.goToTenma()

Objective-Cの方を実行すると、「おひろは天満に行って(null)を食べました。」と表示されます。
(null)という食べ物は食べたことがありませんが、なめこかもすぐの様なものでしょうか。
これは仕方ないですね。nilを文字列のフォーマットに突っ込むと(null)になるのは仕様です。

一方Swiftの方を実行すると、…ハングします。
理由は!をつけてアンラップしているところにあります。
アンラップ=nilを許容しない形に変える、ということです。よってfavoriteFoodがnilであれば
矛盾しますので実行時にfatal errorで止めてくれるのです。

ハングしてくれちゃ困るよ…と思った方。
いえいえそんなことはありません。意図しない動作をしているとわざわざ教えてくれているわけです。
Objective-Cの方はハングしませんが、(null)と表示されてもこれはバグであることには変わりないでしょう。
むしろハングしないのでバグに気づかないというリスクがあるのです。

Swiftの方では次のようにOptionalをやめればnilを許容しなくなるのでハングしなくなります。
さらにnilを渡そうとするとコンパイラがエラーを吐くので、他人が見てもnilを渡しては
ダメなんだなということが明示的になります。

class Ohiro {

    private var favoriteFood: String

    init(favoriteFood: String) {
        self.favoriteFood = favoriteFood
    }

    func goToTenma() {
        println("おひろは天満に行って\(self.favoriteFood)を食べました。")
    }
}

Objective-Cではこれができません。

もう一つの解としては次のようにnilチェックするという手があります。
これであればObjective-Cの方も変な結果にはなりません。


//-------- Objective-C --------------
// ヘッダ
@interface Ohiro : NSObject
- (instancetype)initWithFavoriteFood:(NSString *)favoriteFood;
- (void)goToTenma;
@end

// 実装
@interface Ohiro () {
}
@property (nonatomic, readonly, copy) NSString *favoriteFood;
@end

@implementation Ohiro

- (instancetype)initWithFavoriteFood:(NSString *)favoriteFood {
    self = [super init];
    if (self) {
        _favoriteFood = [favoriteFood copy];
    }
    return self;
}

- (void)goToTenma {
    if (self.favoriteFood) {
        NSLog(@"おひろは天満に行って%@を食べました。", self.favoriteFood);
    } else {
        NSLog(@"おひろは天満に行って焼肉を食べました。");
    }
}

@end
//-------- Swift --------------
class Ohiro {

    private var favoriteFood: String?

    init(favoriteFood: String?) {
    self.favoriteFood = favoriteFood
    }

    func goToTenma() {
        // 以下のコードはOptional Bindingといって、self.favoriteFoodがnilでなければ
        // アンラップして_favoriteFoodに代入しifの方を通ります。
        // self.favoriteFoodがnilであればelseの方を通ります。
        if let _favoriteFood = self.favoriteFood {
            println("おひろは天満に行って\(self.favoriteFood!)を食べました。")
        } else {
            println("おひろは天満に行って焼肉を食べました。")
        }
    }
}

ただみなさん毎回ちゃんとnilチェックしてますか?
特にObjective-Cではnilに対するメッセージは無視されるという特殊な仕組みがあるので
忘れがち(気づかない)だと思います。

では初期化時にデフォルト値を入れるようにしておけばどうでしょう。
nilチェックはなくていいですし、Swiftの方もOptional型を使わなくて済みます。


//-------- Objective-C --------------
// ヘッダ
@interface Ohiro : NSObject
- (instancetype)initWithFavoriteFood:(NSString *)favoriteFood;
- (void)goToTenma;
@end

// 実装
@interface Ohiro () {
}
@property (nonatomic, readonly, copy) NSString *favoriteFood;
@end

@implementation Ohiro

- (instancetype)initWithFavoriteFood:(NSString *)favoriteFood {
    self = [super init];
    if (self) {
        if (favoriteFood) {
            _favoriteFood = [favoriteFood copy];
        } else {
            _favoriteFood = @"焼肉";
        }
    }
    return self;
}

- (void)goToTenma {
    NSLog(@"おひろは天満に行って%@を食べました。", self.favoriteFood);
}

@end
//-------- Swift --------------
class Ohiro {

    private var favoriteFood: String = "焼肉"

    init(favoriteFood: String?) {
        if let _favoriteFood = favoriteFood {
            self.favoriteFood = _favoriteFood
        }
    }

    func goToTenma() {
        println("おひろは天満に行って\(self.favoriteFood)を食べました。")
    }
}

Objective-Cでもこのような設計である程度回避できますね。
ただ繰り返しになりますが、Objective-Cではnil代入を禁止できないので、
このような対応を忘れがちです。

また、Swiftの方で言えばOptional型は意図的にnilである状況が存在する場合を除いて、
できる限り使わないようにした方がより安全になります。

ただ、Swiftの方でもこれで完全に安全なわけではありません。
以下のようなコードだとコンパイル時にはエラーになりませんので実行時まで気づけません。

var favoriteFood: String? = nil
let ohiro = Ohiro(favoriteFood: favoriteFood!) // 実行時にfatal errorでハング!
ohiro.goToTenma()

Optional型の変数から非Optional型の変数へ値を渡す時には基本nilチェックが
必要と思った方が良いです。

そう考えると何でもかんでもOptional型にするのは逆に面倒くさいと思いませんか?

あるいはOptional型のまま実行すればハングしないので、以下のように何でもかんでも
Optional型で実行していませんか。

self.mou?.wake?.wakannai()

wakannai()が実行されなかった時、その理由がmouがnilなのかwakeがnilなのか、
それこそもう訳が分かりません。せっかくのOptionalの利点を殺してしまっています。

というわけで、Optionalを使うことで、
・変数にnilを代入することを許容するかどうかを明示的にできる(nilチェックが減り、よりスッキリ安全なコードが書ける。)
・その変数を使うとき、また代入するときにnilを考慮した実装をしていなければコンパイラが教えてくれる
というメリットがあるわけです。

…こんなに長々書いておいてアレですが、間違っているかもしれないので悪しからず!

ではでは。

Pocket