Swift Digest
SE-0345 | Swift Evolution

if let shorthand for shadowing an existing optional variable

Proposal
SE-0345
Authors
Cal Stephens
Review Manager
Doug Gregor
Status
Implemented (Swift 5.7)

01 何が問題だったのか

Swift では optional の値をアンラップする手段として、if let foo = foo { ... } の形で既存の変数を同名でシャドウする書き方が非常に広く使われてきました。しかしこの書き方は、同じ識別子を = の左右に2回書かなければならないため、特に変数名が長くなると繰り返しが冗長になります。

たとえば次のように、意味のある長い名前を付けた optional を複数同時にアンラップしようとすると、1行がかなり読みづらくなります。

let someLengthyVariableName: Foo? = ...
let anotherImportantVariable: Bar? = ...

if let someLengthyVariableName = someLengthyVariableName,
   let anotherImportantVariable = anotherImportantVariable {
    ...
}

これを避けるために if let a = someLengthyVariableName のように短い別名を付け直す書き方も見かけますが、利用箇所で変数が何を表すのかが分かりづらくなるという別の問題が生じます。本来であれば、記述の冗長さを理由に短い名前を選ばざるを得ない状況自体を減らしたいところです。

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

optional binding condition において = の右辺を省略できるようにし、省略した場合はコンパイラが同名の既存変数をシャドウする初期化式を自動的に補う、というショートハンドが導入されます。

let foo: Foo? = ...

if let foo {
    // ここでの `foo` は非 optional な `Foo`
}

これは次のように展開されるのと等価です。

if let foo = foo {
    ...
}

適用範囲

このショートハンドは、既存の optional binding condition が使えるすべての制御構文で利用できます。let / var のどちらも書けます。

if let foo { ... }
if var foo { ... }

else if let foo { ... }
else if var foo { ... }

guard let foo else { ... }
guard var foo else { ... }

while let foo { ... }
while var foo { ... }

複数の束縛を並べたときの読みやすさの改善が、この機能の主な効果です。

let someLengthyVariableName: Foo? = ...
let anotherImportantVariable: Bar? = ...

if let someLengthyVariableName, let anotherImportantVariable {
    ...
}

型注釈

通常の optional binding と同じく、明示的な型注釈も書けます。

if let foo: Foo { ... }

は次のように展開されます。

if let foo: Foo = foo { ... }

識別子のみが書ける

let / var の直後に書ける要素は、妥当な識別子ひとつ に限定されます。メンバアクセスのような式は書けません。

if let foo.bar { ... } // 🛑 unwrap condition requires a valid identifier

これは、書かれた名前が「アンラップ対象の式」と「新しく導入される非 optional 変数の名前」を同時に表すための制約です(同様の慣習はクロージャのキャプチャリスト [foo] などにも見られます)。

self のプロパティ

既存の optional binding と同じく、暗黙的な self 経由のプロパティも対象にできます。if let emailAddress と書けば、self.emailAddress をアンラップして同名の非 optional なローカル変数を導入したのと同じ扱いになります。

struct UserView: View {
  let name: String
  let emailAddress: String?

  var body: some View {
    VStack {
      Text(name)

      // `if let emailAddress = emailAddress { ... }` と等価。
      // `self.emailAddress` がアンラップされる。
      if let emailAddress {
        Text(emailAddress)
      }
    }
  }
}

意味論は従来のまま

if let foo はあくまで if let foo = foo の糖衣です。新しい foo は内側のスコープに 別の変数として 導入され、従来どおり値のコピーが作られます。内側での変更は外側の optional には影響しません。Kotlin の if (foo != null) のように「同じ変数の型だけが絞り込まれる」振る舞いとは異なります。

Future Directions

本Proposalのスコープには含まれませんが、今後の方向性として次のような拡張が議論されています(いずれも speculative で、実現を約束するものではありません)。

  • if let foo as? Bar { ... } のような optional キャストへのショートハンド化。
  • 将来の borrow 系 introducer(ref / inout)への同様のショートハンド対応。これに関連して、if ref mother.father.sister { ... } のようにネストしたメンバを新しい識別子を導入せずにアンラップする案も示されています。