仕事でSwiftのコードをよくレビューするのですが、どうやら暗黙的アンラップ型(Implicity Unwrapped Optional)をいつ使うべきか迷う方が多いようです。
以下、文中では「暗黙的アンラップ型」で表記を統一します。また、Optional.None
はnil
という表現で統一します。説明を簡潔にするために一部正確でない表現をしている箇所があります。
復習
暗黙的アンラップ型は、 Optional型だけれど、アクセスするときには明示的なアンラップがいらない(強制的にアンラップされる)型 です。
以下のように!
を型の後ろにつけて宣言します。
var foo: String! = "Hello"
print(foo.lowercaseString) // hello
これは以下のコードと等価です。
var foo: String? = "Hello"
print(foo!.lowercaseString) // hello
つまり、暗黙的アンラップ型は以下の特徴を持っています。
- 実体はOptional型
- 変数にアクセスするときに強制的にアンラップされる。(
!
やif-let
を用いたアンラップが不要)
注意点
前述の"Optional型のコード"を見れば分かるとおり、変数アクセス時に強制的にアンラップ(!
)されるので、 変数にアクセスした時にnil
が入っていた場合はクラッシュするという点に注意が必要です。
例えば、以下のコードはクラッシュします。
var foo: String! = nil
print(foo.lowercaseString) // クラッシュ!
つまり、逆に言えば実際にアクセスするときにはnil
でないことを 自分で保証しなければならないということです。
この 自分でというところがポイントで、(通常の)Optional型であればコンパイル段階でnil
チェックを強制できるところを、暗黙的アンラップ型は半信半疑(これはnil
じゃないよね・・・絶対に・・・?)で利用しなければならないということです。
中間考察
ふむ、どうやら暗黙的アンラップ型は 10億ドルの誤りと呼ばれるnull参照の時代に逆戻りしているようにも見えてしまいます。
つまり、暗黙的アンラップ型の変数に 安全にアクセスするコードは以下のようになるでしょう。
var foo: String! = nil
if foo != nil {
print(foo.lowercaseString) // こんなはずじゃなかったのに・・・
}
Swiftは10億ドルの誤りと呼ばれるnull
(およびnull
チェック)を無くすために、言語仕様としてOptional型を用意し、安全を謳ったのではなかったのでしょうか?
暗黙的アンラップ型は必要か?
さきほどの中間考察だと、どうやら暗黙的アンラップ型は非常に不利な立場に立たされているように見えます。
「暗黙的アンラップ型は本当に不要なのか?」
その答えは当然ながらNoで、あると便利なので言語仕様としてわざわざ用意されています。
便利である理由
暗黙的アンラップ型が便利である理由は、IBOutletのコードを読めば理解できると思います。
以下はUIButton
をIBOutlet接続した例です。
@IBOutlet weak var button: UIButton!
見てのとおり暗黙的アンラップ型が使われています!
Optional型でも良かったかもしれませんが、その場合はアクセスする際に毎回アンラップする必要が発生します。
@IBOutlet weak var button: UIButton?
if let btn = button {
btn.backgroundColor = UIColor.redColor()
}
これはいささか面倒ですね、なぜって 絶対に値が入っていることが分かっているのにアンラップが必要になってしまうわけですから。
お、何やら少し見えてきましたね。
次の疑問
さて、そうすると次の疑問が湧いてきます。
「絶対に値が入っていることが保証されるIBOutletが、なぜnil
が入る可能性のある暗黙的アンラップ型で宣言されているのか?」
だってそうでしょう、絶対に値が入っているならそもそもOptionalである必要は皆無のはずです。
しかし、Optionalを使わないようにコードを書き換えてもコンパイルエラーとなります。
@IBOutlet weak var button: UIButton // 末尾の?を削除
// 以下のコンパイルエラーとなる
// @IBOutlet property has non-optional type 'UIButton'
これはどういうことかというと、 Optional型でない以上、インスタンスが生成されたタイミングで値が入っていなければならないけれど、Swiftのイニシャライザ(つまりインスタンス生成時)では値が確定できないためです。
なぜなら、StoryboardまたはxibからViewControllerが生成される仕組みは、Swiftの通常のインスタンス生成の仕組みからは外れたものだからです。
暗黙的アンラップ型をいつ使うべきか?
ようやくタイトルへと回帰できました。
・・・といっても答えはすでに出ているようなものですね。
暗黙的アンラップ型は、 Swiftのインスタンス生成の仕組みから外れているけれど、実際にインスタンスが利用できるようになったタイミングでは必ず初期化されていることが保証できる変数に対して使うべきでしょう。
その最たる例がIBOutletですが、Storyboardからインスタンスを生成したあとに確実に初期化する変数に利用するのもありだと思います。
var message: String! // create()で確実に初期化されるので利便性を考えて暗黙的アンラップ型で宣言
class func create(message: String) -> ViewController {
let storyboard = UIStoryboard(name: "foo", bundle: nil)
let viewController: ViewController =
storyboard.instantiateInitialViewController() as! ViewController
viewController.message = message // 初期化
return viewController
}
といっても上記の例であってもmessage
の初期化が漏れればクラッシュするわけで、絶対に安全というわけには行きません。ですので、この辺りはさじ加減かと思います。
個人的にUIViewControllerにおいては、 viewDidLoad以降であれば確実に値が入っていなければならない変数については暗黙的アンラップ型を適用して良いと感じています。
利用の手引
これまでの議論を踏まえて、定数・Optional型・暗黙的アンラップ型の使い分けを考えると以下の順で考えていくと良いかと思います。
- 定数
- Optional型
- 暗黙的アンラップ型
まず **インスタンス生成時に値が確定できるのであれば「1.定数」**を使うべきでしょう。
次にインスタンス生成時に値が確定できない、つまり **アクセスするときにnil
である可能性がある場合は「2. Optional型」**を使うべきでしょう。
そして最後に、「2. Optional型」を適用したけれど、 変数にアクセスするときにはnil
でないことが保証できる特別なケースにおいて「3.暗黙的アンラップ型」の適用を考えましょう。
まとめ
- 暗黙的アンラップ型の実体はOptional型
- 暗黙的アンラップ型にアクセスするときは強制アンラップ(
!
)される - IBOutletとかで使われている
- インスタンス利用時に必ず初期化されることが保証できる場合に暗黙的アンラップ型を使うと便利
- 定数、Optional型、暗黙的アンラップ型、の順で適用を考えると良い(かも)
- ご利用は計画的に
P.S.
個人的には(Javaのような)厳格で一貫性のあるコードよりも書きやすいコードを目指している(ように見える)Swiftに暗黙的アンラップ型を用意したのは良い判断だと感じています。
・・・むしろ必然?