Swift Digest
SE-0054 | Swift Evolution

Abolish ImplicitlyUnwrappedOptional type

Proposal
SE-0054
Authors
Chris Willmore
Review Manager
Chris Lattner
Status
Implemented (Swift 4.2)

01 何が問題だったのか

Swift 2 までの ImplicitlyUnwrappedOptional<T>(略して IUO、シンタックスシュガーは T!)は、型システム上 Optional<T> とは別の独立した型として扱われていました。値は Optional と同じく .some(x) / .none を持ちますが、非オプショナル型が要求される文脈に出現すると暗黙に強制アンラップされる、という点だけが異なります。

IUO は主に Objective-C 由来の nullability 未注釈な API を Swift から扱うため、あるいはイニシャライザでの deferred initialization を書きやすくするための過渡的な仕組みとして導入されました。しかし、IUO を独立した型として持ち続けることで、以下のような問題が生じていました。

IUO が意図せずコード中を伝播する

IUO 型の値を letvar に代入したり、配列リテラルに入れたりすると、変数や要素の型もそのまま T! として推論されます。結果として、宣言箇所では明示されていないのに、型推論を介して IUO があちこちに広がってしまいます。

// f() の戻り値は T! と宣言されている
let x = f()      // Swift 2 では x: Int!
let a = [f()]    // Swift 2 では a: [Int!]

こうして広がった IUO 値を非オプショナル型が要求される文脈で使うと暗黙にアンラップされるため、中身が nil だったときのクラッシュ箇所が宣言箇所から遠く離れてしまい、原因を追いにくくなっていました。より安全なデフォルトは、IUO が滲み出した先では通常の Optional として扱い、利用者に明示的なアンラップを促すことです。

型システム内に IUO 型が存在することの複雑さ

ImplicitlyUnwrappedOptional<T>Optional<T> と並んで独立した型として存在することで、ネストされた IUO 型([Int!](Int!, Int!) など)や、typealias X = Int! のような型エイリアスが書けてしまいます。これらは本来「暗黙アンラップ可能であることを示すマーカー」でしかなく、型として独立させておく意味が薄いにもかかわらず、コンパイラは型チェック以降のあらゆる段階で IUO を特別扱いし続ける必要がありました。

IUO は本来「宣言の性質」であるべき

暗黙アンラップは「この宣言から取り出した値は強制アンラップしてよい」という宣言単位の性質であって、型そのものの性質ではありません。型システムに IUO を埋め込んでしまっていたことが、伝播の問題と実装の複雑さ、両方の根本原因になっていました。

02 どのように解決されるのか

ImplicitlyUnwrappedOptional<T> 型そのものを型システムから廃止し、代わりに「この宣言はオプショナル型を持ち、必要に応じて暗黙アンラップしてよい」という宣言に付く属性として表現し直します。人間が直接書くことはありませんが、コンパイラ内部ではこの属性を @_autounwrapped と呼びます。

ソースコード上は、これまで通り宣言の型に ! を付ける構文が使えます。

  • プロパティ / 変数宣言
  • イニシャライザ宣言(init!
  • 関数・メソッドの戻り値
  • サブスクリプト宣言
  • 引数宣言(可変長引数を除く)

宣言は T? + 属性、使用側は文脈で決まる

T! と宣言された値は、実体としては Optional<T>(つまり T?)を持ち、加えて @_autounwrapped 属性が付いている、という扱いになります。この宣言を参照したときの型は使用側の文脈で決まります。

  1. まず T? として型チェックを試みる。
  2. T? のままでは型が合わない場合に限り、暗黙に T へ強制アンラップする。

つまり、式全体として T? で成立するなら T? のまま、どうしても非オプショナル型でないと成立しないときだけ T に落ちます。IUO 属性は「この参照から取り出した値の最終的な型は、TT? のどちらかにしかならない」という形で閉じ込められ、宣言の外へはオプショナルとして伝播します。

func f() -> Int! { return 3 }    // 戻り値は Int? + IUO 属性

let x1 = f()           // x1: Int?   (Int? として通るのでそのまま)
let x2: Int? = f()     // x2: Int? = .some(3)
let x3: Int! = f()     // x3: Int? + IUO 属性
let x4: Int  = f()     // x4: Int = 3 (Int が要求されるので強制アンラップ)

let a1 = [f()]         // a1: [Int?] = [.some(3)]
let a3: [Int]  = [f()] // a3: [Int]  = [3]

nil を返した場合は、非オプショナル文脈に流れ込んだ瞬間にトラップします。

func g() -> Int! { return nil }

let y1 = g()           // y1: Int? = nil
let y4: Int = g()      // トラップ(強制アンラップで nil)
let b3: [Int] = [g()]  // トラップ

ジェネリック引数に渡したときも、型変数は Int? に束縛されます。

func p<T>(x: T) { print(x) }
p(f())  // "Optional(3)" と表示される(T = Int?)

if let での束縛もこれまで通り機能します。

if let x5 = f() {
  // x5: Int = 3
}
if let y5 = g() {
  // 実行されない
}

ネストした IUO 型と型エイリアスの禁止

IUO が宣言属性になったことに伴い、型の一部として書くことはできなくなります。

  • ネストした IUO 型([Int!](Int!, Int!) など)は不正になります。要素型としては [Int?][Int] のどちらかに書き分ける必要があります。
  • typealias X = Int! のように、型エイリアスに IUO 情報を載せることもできません。Objective-C の typedef から取り込まれる型エイリアスも同様です。たとえば次の Objective-C 宣言は、

      typedef void (^ViewHandler)(NSView *);
    

    Swift 側では次のように取り込まれます。

      typealias ViewHandler = (NSView?) -> ()
    

    引数型が NSView! ではなく NSView? になる点に注意が必要です。

  • 素の ImplicitlyUnwrappedOptional<T> を型名として書いていた箇所は、後置の ! 記法に書き換えます。

従来の使い方はほとんど維持される

Objective-C API の nullability 未注釈や、stored property の deferred initialization といった、IUO が本来想定していた用途はそのまま使い続けられます。

struct S {
  var x: Int!           // 後から初期化される IUO プロパティ
  init() {}
  func initLater(x someX: Int) { self.x = someX }
}

一方で、IUO 宣言から取り出した値は既定で Optional として扱われるため、暗黙アンラップが意図せず連鎖していくことはなくなります。IUO を使いたい箇所では引き続き使え、使わなくてよい箇所では自然に Optional に収束する、というバランスに整えられます。

移行時の注意

この変更は一部の既存コードに影響します。

  • 右辺の T! 式から型推論で T! になっていた変数は、新しいルールでは T? になります。その変数を非オプショナル文脈で使っている箇所はコンパイルエラーになり、必要に応じて ! での強制アンラップを促されます。
  • 明示的に書かれたネストした IUO 型([Int!] など)は [Int?] あるいは [Int] に書き換える必要があります。なお、ネストしていない通常の IUO 宣言は従来通り動作します。
  • 素の ImplicitlyUnwrappedOptional<T> を書いていた箇所は後置 ! に書き換えます。