Swift Digest
SE-0345 | Swift Evolution

既存オプショナル変数をシャドーするためのif let短縮構文

if let shorthand for shadowing an existing optional variable

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

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

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) のように「同じ変数の型だけが絞り込まれる」振る舞いとは異なります。

03 今後の見通し

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

optional キャストへのショートハンド

optional binding と同じ発想で、optional キャストもショートハンド化できる可能性があります。たとえば

if let foo as? Bar { ... }

if let foo = foo as? Bar { ... }

と等価にする、という拡張が考えられます。

将来の borrow 系 introducer との連携

“A roadmap for improving Swift performance predictability” では、コピーを伴わずに既存の変数を借用するための新しい introducer として ref(イミュータブルな借用)と inout(ミュータブルな借用)が議論されています。これらが導入された場合、let / var と揃える形で optional binding condition にも対応させ、

if ref foo = foo { ... }
if inout foo = &foo { ... }

のように書けるようにすることが想定されています。さらに本Proposalのショートハンドも自然に拡張でき、

if ref foo { ... }
if inout &foo { ... }

のような書き方が可能になります。

ネストしたメンバのアンラップ

本Proposalでは、let / var の直後には妥当な識別子ひとつしか書けず、if let foo.bar { ... } のようにネストしたメンバを直接アンラップすることはできません。これを将来的に許容する案として、次のようなものが挙げられています。

ひとつは、内側のスコープに導入する変数名をコンパイラが自動合成するアプローチです。たとえば if let foo.bar であれば、新しい非 optional 変数を bar あるいは fooBar といった名前で導入する、といった案です。

もうひとつは、上記の借用 introducer ref / inout と組み合わせるアプローチです。借用は元のストレージへの排他的アクセスをコンパイラが保証するため、内側のスコープで新しい識別子を導入する必要がありません。これを利用すれば、新しい変数やコピーを作らずにネストしたメンバを直接アンラップでき、たとえば次のように書けるようになる可能性があります。

// `mother.father.sister` は optional

if ref mother.father.sister {
  // `mother.father.sister` を非 optional かつイミュータブルとして扱える
}

if inout &mother.father.sister {
  // `mother.father.sister` を非 optional かつミュータブルとして扱える
}