この記事の要点
- Swift コンパイラに implicitly unwrapped optional(IUO、暗黙的にアンラップされる optional)の新しい実装が入り、SE-0054 の実装が完了しました。これにより型チェックの一貫性が高まり、コンパイラ内部の特別扱いが減りました。
- もっとも目につく変化は、
T!として宣言した値について、診断(diagnostic)メッセージがT!ではなくT?と表示するようになった点です。T!は「型T?を持ち、かつ必要なときに暗黙的にアンラップしてよい」という印(フラグ)が付いた宣言にすぎない、という新しいメンタルモデルに沿っています。 - IUO はもはや
Optional<T>とは別の型ではなくなったため、いくつかの場面でソース互換性に影響が出ます。as T!などの型としての!の使用、型推論で IUO が推論されなくなる点、mapの呼び出し先の変化、in-out 引数のオーバーロード、ImplicitlyUnwrappedOptionalのエクステンションなどです。
暗黙的なアンラップは型ではなく宣言の一部になった
implicitly unwrapped optional は、式をコンパイルするために必要なときに自動でアンラップされる optional です。宣言するには、型名のあとに ? ではなく ! を付けます。
多くの人は IUO を「通常の optional とは別の型」として捉えていました。実際、Swift 3 ではそのとおりに動いていました。var a: Int? は a を Optional<Int> 型にし、var b: String! は b を ImplicitlyUnwrappedOptional<String> 型にしていました。
新しいメンタルモデルでは、! を「? の同義語であり、加えてその宣言された値を暗黙的にアンラップしてよいというフラグを付けるもの」と考えます。言い換えると、String! は「この値は Optional<String> 型を持ち、かつ必要なら暗黙的にアンラップしてよいという情報も持つ」と読めます。
これが新しい実装と一致します。T! と書かれた箇所はすべて、コンパイラはその値を T? 型として扱い、宣言の内部表現に「必要に応じて暗黙的にアンラップしてよい」というフラグを立てます。その結果、T! で宣言された値に関する診断は T! ではなく T? と表示されるようになります。
ソース互換性への影響
ほとんどのプロジェクトは互換性の問題なくビルドできますが、SE-0054 に沿いつつ従来のコンパイラとは挙動が変わるケースがあります。
型としての ! の使用
as T! のような型強制(coercion)は SE-0054 で禁止されました。Swift 4.1 ではこれらに対して deprecation 警告が出ます。多くの場合、as T! を as T? に置き換えるか、強制自体を削除すればコンパイルできます。
互換性のため特別扱いも用意されています。x as T! と書くと、コンパイラはまず x as T? として型チェックを試み、それが失敗した場合にのみ (x as T?)! として optional を強制アンラップします。ただしこの形式の型強制は依然として deprecated 扱いで、この特別扱いも将来の Swift で削除される可能性があります。最終的には as T? への置き換え、または不要な強制の削除が推奨されます。
型強制は、より一般的な問題「! を型の一部として使う」ことの特殊ケースです。! を型の一部として使えるのは、次の 3 か所だけです。
- プロパティ宣言
- 関数宣言の パラメータ
- 関数宣言の 戻り値
それ以外の場所では ! はエラーとすべきですが、Swift 4.1 より前のリリースでは一部を見逃していました。
let fn: (Int!) -> Int! = ... // error: not a function declaration!
新しい実装では、こうした箇所の ! を ? と同じように扱い、deprecated である旨の診断を出します。
IUO として宣言された値に対する map の呼び出し
以前は次のようなコードは、values を強制アンラップしてから配列の map(_:) を呼んでいました。
class C {}
let values: [Any]! = [C()]
let transformed = values.map { $0 as! C }
新しい実装では ! が ? の同義語であるため、コンパイラはここで Optional<T> の map(_:) を呼ぼうとします。
let transformed = values.map { $0 as! C } // Optional.map を呼ぶ。$0 の型は [Any]
この場合 warning: cast from '[Any]' to unrelated type 'C' always fails が出ます。型チェック自体は通るため、values の強制アンラップは行われません。意図どおりにするには、optional chaining か強制アンラップを使います。
let transformed = values?.map { $0 as! C } // transformed の型は Optional<[C]>
let transformed = values!.map { $0 as! C } // transformed の型は [C]
ただし、Optional 側の map(_:) では型チェックが成功しようがない次のような場合は、従来どおり values が強制アンラップされ、配列の map(_:) が呼ばれます。
let values: [Int]! = [1]
let transformed = values.map { $0 + 1 }
型でないものは型として推論できない
IUO は optional とは別の型ではなくなったため、型として(あるいは型の一部として)推論されることがなくなりました。代入の右辺が IUO として宣言された値であっても、左辺に推論される型は「optional である」ことだけを表します。
var x: Int!
let y = x // y の型は Int?
func forcedResult() -> Int! { ... }
let getValue = forcedResult // getValue の型は () -> Int?
func id<T>(_ value: T) -> T { return value }
let z = id(x) // z の型は Int?
func apply<T>(_ fn: () -> T) -> T { return fn() }
let w: Int = apply(forcedResult) // 失敗。apply() がアンラップされていない Int? を返すため
この変化は、AnyObject ルックアップ、try?、switch でも気づくことがあります。
AnyObject ルックアップ
AnyObject ルックアップの結果は IUO として扱われます。ルックアップしたプロパティ自身も IUO として宣言されていると、2 段階の暗黙的アンラップが発生します(以下では property が UILabel! として宣言されているとします)。
func getLabel(object: AnyObject) -> UILabel {
return object.property // 両方の optional を強制し、UILabel になる
}
一方、if let や guard let は 1 段階分しかアンラップしません。次の例では、従来は label が UILabel! と推論されていましたが、新しい実装では UILabel? と推論されます。
// label は UILabel? と推論される
if let label = object.property {
functionTakingLabel(label) // UILabel を期待する箇所に UILabel? を渡すためエラー
}
明示的な型注釈を付けると解決できます。
if let label: UILabel = object.property {
functionTakingLabel(label) // okay
}
try?
try? も optional を 1 段階追加するため、IUO を返す関数と組み合わせると、2 段目のアンラップを明示する必要が出ることがあります。
func test() throws -> Int! { ... }
if let x = try? test() {
let y: Int = x // error: x は Int?
}
if let x: Int = try? test() { // Int と明示
let y: Int = x // okay。x は Int
}
if let x = try? test(), let y = x { // okay。x は Int?、y は Int
...
}
switch
Swift 4.1 は次のコードを output を IUO として扱うことで受け付けていました。
func switchExample(input: String!) -> String {
switch input {
case "okay":
return "fine"
case let output:
return output // optional を暗黙的にアンラップし、String を生成
}
}
新しい実装では output は IUO でない String? と推論されます。コンパイルを通すには、値を強制アンラップするか、nil / 非 nil を明示的にパターンマッチします。
func switchExample(input: String!) -> String {
switch input {
case "okay":
return "fine"
case let output?: // 非 nil のケース
return output // okay。output は String
case nil:
return "<empty>"
}
}
optional と IUO による in-out 引数のオーバーロード
Int! が Int? の同義語になったため、in-out 引数が optional か IUO かだけが違うオーバーロードは意味をなさなくなりました。Swift 4.1 では警告でしたが、新しい実装ではエラーになります。IUO 版(Int!)のオーバーロードは削除する必要があります。
func someKindOfOptional(_: inout Int?) { ... }
func someKindOfOptional(_: inout Int!) { ... } // 新実装ではエラー
なお、IUO として宣言した値を、plain な optional を期待する関数の in-out 引数として渡すこと(およびその逆)は可能なので、重複したオーバーロードを削除しても呼び出し側はそのまま動きます。
ImplicitlyUnwrappedOptional のエクステンション
ImplicitlyUnwrappedOptional<T> は Optional<T> への unavailable な型エイリアスになり、この型にエクステンションを作るコードはコンパイルできなくなりました。
// error: 'ImplicitlyUnwrappedOptional' has been renamed to 'Optional'
extension ImplicitlyUnwrappedOptional {
nil のブリッジ
nil 値をブリッジするとき、実行時エラーになるのではなく nil が NSNull にブリッジされるようになりました。
import Foundation
class C: NSObject {}
let iuoElement: C! = nil
let array: [Any] = [iuoElement as Any]
let ns = array as NSArray
let element = ns[0] // Swift 4.1 では nil を含む IUO のブリッジで Fatal error
if let value = element as? NSNull, value == NSNull() {
print("pass") // 新実装ではこちらに到達する
} else {
print("fail")
}
まとめ
IUO は Optional<T> とは別個の型ではなくなるよう再実装され、型チェックの一貫性が高まり、コンパイラの特別扱いが減りました。IUO は主に Objective-C の API を取り込むときに目にするほか、@IBOutlet プロパティの宣言や、「完全に初期化されるまでアクセスしない」と確信できる場面で便利に使えます。とはいえ、基本的には暗黙的なアンラップを避け、if let / guard let による明示的なアンラップを使い、安全だと確信できるときだけ ! による強制アンラップを使うのが望ましいでしょう。
関連リンク
- SE-0054(
ImplicitlyUnwrappedOptional型を廃止する) — この再実装が完了させた Proposal - Swift 4.1 リリース — この変更を含む Swift 4.1 の公式リリース告知