Swift Digest
SE-0123 | Swift Evolution

Disallow coercion to optionals in operator arguments

Proposal
SE-0123
Authors
Mark Lacey, Doug Gregor, Jacob Bandes-Storch
Review Manager
Chris Lattner
Status
Rejected

01 何が問題だったのか

Swift では、利便性のために非オプショナル型の値をオプショナル型へ暗黙に変換(coercion)できるようになっています。たとえば Int を受け取る側が Int? を期待していても、そのまま渡せます。

func consumesOptional(value: Int?) -> Int { ... }

let x: Int = 1
let y = consumesOptional(value: x) // Int が Int? に暗黙変換される

この暗黙変換は通常の関数呼び出しや代入では便利に働きますが、演算子の引数に対して適用されるときに不自然な挙動を招く場面がある、というのがこの提案の出発点でした。

問題となる具体例

比較演算子(<<=>>=)の引数でも同じ暗黙変換が働くため、次のコードはコンパイルエラーにならずに動いてしまいます。

let x = -1
let y: Int? = nil
print(y < x) // true

ここでは xInt? に持ち上げられ、Optional 同士の比較として評価されます。Swift 当時の仕様では「nil はあらゆる非 nil 値より小さい」と定義されていたため、結果は true になります。読者は ynil のときに比較が意味を持たないことを見落としやすく、バグの温床になります。

nil 合体演算子 ?? でも同様の問題があります。左辺が実は非オプショナルであっても、自動的にオプショナルに持ち上げられてコンパイルが通ってしまいます。

let z = 1
print(z ?? 7) // 1。`z` はそもそも Optional ではないのに `??` が書ける

z の宣言が離れた場所にあると、読み手は z がオプショナルだと誤解しかねません。また、著者が後から z をオプショナルにするつもりだったのに忘れた、というケースも考えられます。

当時のコンパイラは「非オプショナルとリテラル nil の比較」だけを特別扱いで警告する場当たり的な処理を持っていましたが、変数を介した場合には効かず、根本的な解決にはなっていませんでした。

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

演算子の引数に限って、値からオプショナルへの暗黙変換を禁止することが提案されました。通常の関数呼び出しや代入文での暗黙変換は従来どおり有効で、影響範囲は演算子の引数コンテキストだけに絞られます。また、implicitly unwrapped optional(Int! など)の変換もこれまでどおり動作します。

これにより、先ほどの比較は型エラーとして弾かれるようになります。

let x = -1
let y: Int? = nil
print(y < x) // error: Int? と Int は比較できない

修正するには、利用側で明示的にオプショナル性を扱う必要があります。

// y が nil のときは比較しない
if let y = y, y < x { ... }

// y が非 nil だと分かっているなら強制アンラップ
if y! < x { ... }

?? についても、左辺が非オプショナルのときはコンパイルエラーになります。この場合の修正は単純で、?? ごと消して左辺の値をそのまま使うだけです。

等価・恒等演算子は使い勝手を維持する

一方で、等価演算子(==!=)や恒等演算子(===!==)まで同じルールで縛ると、辞書の添字アクセスなどで頻出する「オプショナルと非オプショナルの等値比較」が書けなくなり、実用性を損ないます。

そこで、混在オプショナリティ用のオーバーロードを標準ライブラリに追加することが提案されました。

public func == <T: Equatable>(lhs: T?, rhs: T) -> Bool
public func == <T: Equatable>(lhs: T, rhs: T?) -> Bool

public func != <T: Equatable>(lhs: T?, rhs: T) -> Bool
public func != <T: Equatable>(lhs: T, rhs: T?) -> Bool

同様のオーバーロードが === / !==AnyObject 版)や、Any.Type に対する == / != にも追加されます。これにより、次のような既存コードは変更なしで動き続けます。

let x: Int? = 2
let y: Int = 3
if x == y { ... }

let dict: [String: Int] = [:]
if dict["key"] == y { ... }

ただし副作用として、非オプショナル値をリテラル nil と等価比較するコードも型検査を通るようになってしまいます。意味としては常に false を返すだけで安全ですが、見た目が奇妙になるため、将来的に別途診断で抑え込む余地があると提案では触れられていました。

let i = 1
if i == nil { // コンパイルは通るが、常に false
    print("should never happen")
}

既存コードへの影響

  • 順序比較演算子(<<=>>=)を使っている箇所のうち、片側がオプショナルになっていたコードは、if let / 強制アンラップ / Optional(...) でのキャストなどに書き換える必要があります。
  • ?? の左辺が非オプショナルだったコードは、?? を外すだけで済みます。
  • 等価・恒等演算子を使っているコードは原則そのままで構いません。

Proposal 内の小規模な実アプリでの調査では、必要な書き換えは数箇所程度で、Swift 3 への移行に対する影響は小さいと見積もられていました。

このProposalの結論

この提案は Rejected となりました。動機となっていた「オプショナル比較の直感に反する挙動」については、先行する SE-0121Optional に対する < などの順序比較オーバーロードそのものを標準ライブラリから削除する、というよりストレートな解決が採用されたためです。演算子引数全般にまたがる暗黙変換の禁止という大掛かりな言語ルールの変更まで踏み込む必要はない、と判断されました。