Swift
null安全
swtws

Swiftの `!` は危険信号か?

これは Swift Tweets 2018 Spring で発表(ツイート)した「!なことっ 知っとこな!」の内容をQiita向けに大幅に加筆修正したものです。
LTは5分(約10ツイート)だとぼくが勘違いしていたことと、ツイートの文字数制限があることにより、発表時はインパクトを重要視してだいぶコンパクトにまとめていました1

元の発表ツイートはこちらからの一覧のスレッドです:
https://twitter.com/hironytic/status/985163326530310146

Swift Tweets 2018 Springに関するTogetterまとめはこちら:
https://togetter.com/li/1218153


try! Swiftから1ヶ月経って

ぼくにはひとつ気になっていることがあります。毎年、 try! Swift が開かれるたびに、「try!」だから危険だというネタが登場することです。ネタ自体はよくできていると思います。try? Swiftにすればいいとか、catchしようとか、Swiftに絡めるセンスはうまいと思います。でも、 ! は危険なのでしょうか?

確かに、 :warning: という記号は警告を表すもので、これが少し怖いと感じるのはわかります。だから ! が直感的に危険信号に見えるのもわかります。では、Swiftの try! やOptionalのforced unwrapping(後ろにつける!)は本当に危険なのでしょうか?

調べてみた

Apple の The Swift Programming Language には以下のように書かれています2

forced unwrappingについて はこういう記述です。

Once you’re sure that the optional does contain a value, you can access its underlying value by adding an exclamation mark (!) to the end of the optional’s name. The exclamation mark effectively says, “I know that this optional definitely has a value; please use it.” This is known as forced unwrapping of the optional’s value:

エクスクラメーションマークは「このOptionalは間違いなく値を持ってるからそれを使ってよ」ということを効果的に表明するとあります。

try!について はこうです。

Sometimes you know a throwing function or method won’t, in fact, throw an error at runtime. On those occasions, you can write try! before the expression to disable error propagation and wrap the call in a runtime assertion that no error will be thrown.

関数やメソッドが例外を投げないと知っている時に使えるとか、エラーが発生しないというランタイムアサーションで呼び出しを包むとあります。

実際のところ、 ! は危険なのか?

こんな例を考えてみます。

let numbers = [10, 20, 30]
let x = numbers.first!

firstは配列が空のときにnilを返します。配列は空ではないとわかっているので、この ! は安全です。

世間では ! は邪悪なものだ、できるだけ避けよう、という人もいます。実はかつてぼくもそう考えていました。ですが、 @koher さんと話して少し考えが変わりました。彼の記事「Swiftのエラー4分類が素晴らしすぎるのでみんなに知ってほしい」に書いてあるように、forced unwrappingはSwiftの4種類のエラーのLogic Failureにあたります。

つまり、先ほどのコードは次のように書くようなものです。
逆に言えば、このようなコードをシンプルに書けるのが ! であるとも言えます。

guard let x = numbers.first else {
    // こんなことが起こってはいけない
    preconditionFailure("numbers should have first element")
}

もし、バグにより前提条件が成り立たない場合はプログラムが終了します3。ユーザーの視点に立てば、クラッシュは最もわかりやすいバグです。しかも、やりたいことを阻害する最大のものであり、ユーザー体験を台無しにします。それを避けるために、 preconditionFailure ではなく、 return などを使って単に処理をスキップするという手もあります。

guard let x = numbers.first else {
    // こんなことが起こってはいけないが念のため
    return
}

そうすれば落ちなくなります。ただし、起こってはいけない異常系が見つかりにくくなります。ユーザーはやりたいことを完遂することができるかもしれませんが、実はその時スキップした処理がデータの整合性を狂わせているかもしれません。何日も経った後にプログラムの別の箇所で問題として見つかったとき、大元のスキップしたところが原因だと突き止めるのは大変かもしれません。また、多数のユーザーのデータの整合性の狂ってしまって大混乱を引き起こすなら、そうなる前にクラッシュしてくれた方がマシかもしれません。

問題を検知できるがクラッシュする前者と、問題を検知できないがクラッシュはしない後者。そのどちらがいいのかについては、個々のプログラムの性質にもよるので、一概には言えません。

! は危険なのか?については、バグがあればここでクラッシュする危険性があるのは確かですが、バグにより問題が起こる危険性があるのは ! に限ったことではありません。

! は危険信号ではない

ぼくが出した結論は、 Swiftの ! は危険信号ではないということです。
ただし、上に書いたことを理解した上で使うことが前提です。

「よくわからないけど ! を付けたらコンパイルエラーが出なくなったからOK」みたいな気持ちでコードを書いたら危険です(そのコーディング方法が危険です)。思えば、Swiftが登場した頃は、nil安全性がモダンな点として強調されていました。ですが、安易に ! を使ってしまうとnil安全が崩れます。ですから、当初は

安易に ! を使うのは危険

だったのが、 ! の見た目の雰囲気も影響して、

! を使うのは危険

と、重要な部分が抜け落ちて広まってしまったのかもしれません。

「これがnilになっているはずはない」、「ここでエラーが出るはずはない」というのをちゃんと検討した上で正しく使う ! は、危険信号ではなくて、前提条件を表す強い意思表示になる!4というのがぼくの結論です。

最後に「try! Swift」をもう一度見てみます。危険ですか?違いますね。
ぼくが思うに、これは、Swiftが失敗しないという強い意思表示なのです :slight_smile:
今後、「try!」だから危険だという人を見かけたときは、この記事を教えてあげてください。


  1. なおかつ、try! Swift繋がりでniwatakoさんの聞き起こしっぽく見せたいという裏テーマがありましたが、こちらは上手くいったかどうかわかりません。 

  2. 完全に余談になりますが、Kotlinの !! は「for NPE-lovers」と書いてありました :sweat_smile:でも、言語としての設計意図はあまり読み取れませんでした。 Javaとは違って勝手にNPEを投げることはない。NPEを投げて欲しいときは !! を明示的につけるのだ、ということですね :pray:  

  3. -Ounchecked オプションでビルドした場合はクラッシュしませんが、どういう動作になるかは未定義です。 

  4. この「!」はぼくの強い意思表示です:slight_smile: