Optional を返すメソッドをオプショナルチェイニングで呼び出したときの戻り値の型

Swift プログラミング

Optional を返すメソッドを Optional Chaining で呼び出したとき、戻り値をアンラップしても Optional な値が得られてしまうと聞いて、どうしてそのようになるのか探ってみました。


ツイッターを眺めていたら、Swift の面白そうな話題が舞い込んできました。

Optional 型でクラスを扱っていて、そのクラスが持つ Optional を戻り値として返すメソッドを呼び出したときに、戻り値をアンラップしようとしまいと、なぜか Optional 型の結果が得られてしまう様子です。

アンラップする順番が影響している様子

さっそく Playground で同じコードを書いて再現させてみると、たしかにお話どおり、どちらとも Optional 型の値を取得できました。

どちらかが二重に Optional で包まれている訳ではなく、両方ともひとつの Optional で包まれていることがポイントになりそうです。

アンラップを ! を使わずに行ってみる。

そこで、実際のアンラップ処理を「!」を使わずに直接コードで書いてみました。

let get = { (c:C?) -> String? in

	if let value = c {
		
		return value.f()
		
	}
	else {
		
		return nil
	}
}

こうしたところ次のコードで、結果的には「c?.f()」や「c?.f()!」のときと同じ、Optional な結果を得ることができました。

get(c)

どちらの場合とも同じ結果ですが、先ほど書いたget関数 は「c?.f()」を元に作ったコードです。

これが「c?.f()!」のときを考えてコードにしてみると、結果が同じということも考慮して、次のようになるのでしょう。

let get = { (c:C?) -> String? in

	if let value = c {
		
		return value.f()!
		
	}
	else {
		
		return nil
	}
}

書き換えたところは、if 文の真のケースです。

戻り値を「value.f()」から「value.f()!」にしていますが、これでたしかに「c?.f()!」という意味を表現できているように思います。

こうしたときにどういう動きをするかというと、関数自体の戻り値がOptional型 なので、アンラップされてString型 になった値をOptional型 に詰めて返却することになります。

つまり、どちらとも結果は、関数の戻り値どおりのString?型 が得られます。

戻り値がアンラップされているのを期待するには

それではこのとき、結果がアンラップされた状態を期待するにはどうしたら良いかと考えてみると、答えが見えてきました。

関数の戻り値をアンラップすれば良いことになるので、次のコードにすればよさそうです。

get(c)!

実際にこうしてみたところ、結果がアンラップされてString型 で得られました。これで「期待どおり」な動きをしてくれた様子です。

考察

以上から、アンラップで使う!演算子 の優先順位が、結果に影響している様子が窺えます。

つまり最初の「c?.f()!」という方法は「f()」との結びつきが強く、メソッドの戻り値をアンラップしてから、それをオプショナルチェイニングの値として採用するということになっているのでしょう。

オプショナルチェイニングの戻り値は、そもそもがnil だったときにnil を返せるようにOptional型 でラップされるので、アンラップした結果がラップされて得られます。


逆に言うと、メソッドの結果がOptional型 だったとしても、それを二重にOptional でラップすることはないようです。

つまり、そもそもがnil だったのか、それともメソッドの戻り値がnil だったのかを判断することはできないということになりますね。

もっともそれはObjective-C のnil の仕組みと同じ動きなので、それと同等の機能と考えれば、むしろその方が自然な結果が得られそうです。

期待どおりにアンラップするための表記方法

アンラップで使う演算子の優先順位が影響していることが見えてきたので、それなら、カッコを使って優先順位を整えてあげれば、期待通りの結果が得られそうです。

そういうことで、次のようにオプショナルチェイニングで結果を得るところをカッコで括って、その外側でアンラップを仕掛けてみました。

そうしたところ、アンラップをしなかった方はString?型 で、アンラップした方はString型 で、結果を得ることができました。

どうやら期待どおりに動いてくれた様子です。

そもそもの問題として…

ただ、当初の期待と同じ動作をするコードが書けたところで、今回の「期待通り」の表記はそもそもの理論上の問題をはらんでいることに気がつきました。

(c?.f())!

このコード、たしかにfメソッド の戻り値がアンラップされた結果を取得できるのですけど、そもそも変数cnil だったときに、カッコ内がnil になって、それをアンラップしようとして落ちます。

そう言われれば、たしかに元々の c?.f()! で考えると、当初の期待どおりに動作したとしたら、変数cnil のときはnil が、そうでないときはnil ではないString型 が得られるということになり、戻り値の型に矛盾を来たします。

そこから考えても、今回に書いてみた具体的なコードが、実際に見ても適切な動作ということになりそうです。


そしてそこから考えて、今回の (c?.f())! というようなコードを書くのであれば、そもそも次の通りに書くのが論理的には正しそうです。

c!.f()!

結果として Optional ではない値を取得したいなら、途中でnil が許容されない、つまりオプショナルチェイニングを使う意味がないので、そもそも変数c を強制アンラップして、そこから呼び出したfメソッド の戻り値も強制アンラップして得る、というのがきっと自然な流れになりそうでした。