今回は、僕がSwiftのIUOについて誤解していたことについてまとめます。
同じ勘違いをしている方が他にもいると思うので、学んだことを共有しようと思います。
問題(1)
ご存知の通り、IUOは型名に!
をつけた型です。
var text: String!
text = "Hello"
print(text.count) // 5
それでは、次の例ではどうなるでしょうか?
var text: String!
text = "Hello"
print(text) // 何と表示される?
Hello
と表示されると思うかもしれません。
それは不正解で、Optional("Hello")
と表示されます。
問題(2)
ご存知の通り、IUOは型名に!
をつけた型です。
var text: String!
print(text.count) // クラッシュ
上の例では、値をセットする前にアクセスしてしまっているので、クラッシュします。
それでは、次の例ではどうなるでしょうか?
var text: String!
print(text) // どうなる?
これも値をセットする前にアクセスしているので、クラッシュすると思うかもしれません。
それは不正解で、nil
が表示されます。
そもそもIUOとは?
String?
とString!
の違いは何でしょうか? 今まで僕はこう考えていました。
-
String?
... 文字列かnilのどちらかが入っているという直和型(Optional)。 -
String!
... 必ず文字列が入っていると確信しているのでnilにはならない。
しかし、これは大きな誤解です。
Swift.orgでは、IUOは次のように説明されています。
You can think of an implicitly unwrapped optional as giving permission for the optional to be force-unwrapped if needed.
訳:IUOは、必要であれば強制的にアンラップされることを許可していると考えるといい。
この一文が全てを物語っているのですが、分かりやすくコードを使って説明しましょう。
IUOはオプショナル型
そもそも、String?
もString!
も同じオプショナル型です。
var a: String? = "test"
var b: String! = "test"
print(a) // Optional("test")
print(b) // Optional("test")
つまり両者のデータ構造には、何の違いもありません。
?
と!
の違いはどこにあるのか
String?
とString!
の間に違いが現れるのは、メンバにアクセスする時だけです。
var a: String? = "test"
var b: String! = "test"
print(a?.count) // Optional(4)
print(a!.count) // 4
print(b.count) // 4
String?
ではメンバにアクセスする時、?.
と!.
の2通りの方法がありますよね。!.
は強制アンラップといって、a
に値が入ってなければクラッシュさせることで、オプショナルを解除(アンラップ)します。
この!.
の!
を省略できるようにしたものがIUOです。
これが、IUOが「Implicitly Unwrapped Optional(暗黙的アンラップ型)」と名付けられた所以です。
必要であればアンラップされる
既に述べたSwift.orgの引用文にある「必要であれば」という言葉も、IUOの挙動を理解する上で重要です。
You can think of an implicitly unwrapped optional as giving permission for the optional to be force-unwrapped if needed.
訳:IUOは、必要であれば強制的にアンラップされることを許可していると考えるといい。
以下のコードを使って説明します。
var a: String! = "hoge"
var b: String? = a // b=Optional("hoge") されない
var c: String = a // c="hoge" される
func f(s: String) { print(s) }
print(a) // Optional("hoge") されない
f(s: a) // hoge される
a
の値が使われている2, 3, 5, 6行目をそれぞれ見ていきましょう。
- 2行目、
b
に代入するときはOptionalのままでもいいので、アンラップされません。 - 3行目、
c
に代入するときはOptionalのままではいけないので、暗黙的に強制アンラップされます。 - 5行目、
print
の引数はOptional可なので、アンラップされません。 - 6行目、
f
の引数はOptionalではないので、暗黙的に強制アンラップされます。
この例から、アンラップが必要ない場面では、IUOのアンラップは行われないことが分かります。
初期値なしの非オプショナル型
IUOがオプショナルで、暗黙的に強制アンラップしているという事実から、必要もなくIUOの使うのは避けた方がいいです。
例えば、イニシャライザで初期化されるようなプロパティは初期値なしの非オプショナル型を使うといいです。
class Main {
- let text: String!
+ let text: String
init(text: String) {
self.text = text
}
func main() {
print(text.count) // 5
}
}
Main(text: "Hello").main()
初期値なしの非オプショナル型は、もし初期化漏れなどで、値が入っていないままアクセスされる危険があるときは、実行する前からエラーを出して教えてくれるので便利です。
下の例のように、関数内のローカル変数でも使えます。
let num = 123
let r: String
if num.isMultiple(of: 2) {
r = "偶数"
} else {
r = "奇数"
}
print(r) // 奇数
もしこのif文の条件網羅やr
の初期化に漏れがあった場合、コンパイラが実行前エラーで教えてくれます。
IUOを回避する一つのパターンとして覚えておきたいです。