Swift Digest
SE-0079 | Swift Evolution

Allow using optional binding to upgrade self from a weak to strong reference

Proposal
SE-0079
Authors
Evan Maloney
Review Manager
Status
Implemented (Swift 4.2)

01 何が問題だったのか

クロージャの中で self を参照するとき、強い参照サイクルを避けるために [weak self] でキャプチャするのは定番のパターンです。例えば、ビューコントローラがネットワークリクエストを発行し、結果をクロージャで受け取るようなケースでは、クロージャが生存していることでビューコントローラが不要に生き残らないように、self を弱参照でキャプチャします。

典型的には、クロージャの冒頭で弱参照を一度だけ強参照に昇格させ、その後はオプショナルでない参照として使いたくなります。しかし Swift 4.1 までは、self という名前そのものを強参照の束縛先に使うことができず、別名を付けるしかありませんでした。

networkRequest.fetchData() { [weak self] result in
    guard let strongSelf = self else { return }

    switch result {
    case .Succeeded(let data):
        strongSelf.processData(data)
    case .Failed(let err):
        strongSelf.handleError(err)
    }
}

別名の揺れがコードを汚す

強参照側の名前は言語仕様としては任意なので、プロジェクトや開発者によって strongSelf だったり sss、あるいはその場の思いつきの名前だったりと揺れが生じがちです。統一を強制するコンパイラ上の仕組みが無く、コードベースが大きくなるほどノイズになり、読み解きの負担を増やしていました。

self という意味のある名前を失う

Swift にはもともと、オプショナル束縛で同じ名前を再利用してシャドーイングする慣用があります。

// foo はここではオプショナル
if let foo = foo {
    // このスコープ内の foo は非オプショナル
}
// foo はここで再びオプショナル

同じことを self に対してもやりたい、というのが自然な発想ですが、self は言語上の特別な識別子で、ユーザー定義の変数のように再束縛できませんでした。結果として、クロージャの中では本来 self と書けばよい場所で、意味の薄い別名(strongSelf など)を経由せざるを得ず、コードの意図が読み取りにくくなっていました。

バッククォート回避はコンパイラのバグに依存していた

当時の Swift コンパイラには、バッククォートで囲めば self に代入できてしまう挙動があり、次のような書き方が一部で広まっていました。

guard let `self` = self else {
    return
}

しかしこれはコンパイラのバグであり、将来修正される可能性があると当時の Swift チームから明言されていたため、恒久的な解決策として依存することはできない状況でした。

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

弱参照としてキャプチャされた self を、オプショナル束縛によってそのまま self という名前で強参照に昇格できるようにします。別名を用意する必要がなくなり、クロージャの中でも一貫して self と書き続けられるようになります。Swift 4.2 で実装されています。

if let self = self / guard let self = self が書ける

self が弱参照となっているスコープでは、if または guard のオプショナル束縛で self を束縛できます。先ほどの例は次のように書き直せます。

networkRequest.fetchData() { [weak self] result in
    guard let self = self else { return }

    switch result {
    case .Succeeded(let data):
        self.processData(data)
    case .Failed(let err):
        self.handleError(err)
    }
}

if let を使った形でも同様に書けます。

networkRequest.fetchData() { [weak self] result in
    if let self = self {
        switch result {
        case .Succeeded(let data):
            self.processData(data)
        case .Failed(let err):
            self.handleError(err)
        }
    }
}

いずれの場合も、束縛されたあとの self は通常のオプショナル束縛と同じスコープルールに従います。強参照の self が生きている間は弱参照の self をシャドーイングし、スコープを抜けると再び外側の弱参照 self が見えます。

挙動のまとめ

  • 強参照の self は、クロージャで [weak self] キャプチャしたことによってオプショナルになった self からのみ束縛できます。
  • 束縛後の self は他のオプショナル束縛と同じく、そのスコープ内でのみ有効です。
  • 強参照 self のスコープを抜けると、再び弱参照 self(オプショナル)が可視になります。

制限事項

安全のため、次の制約が課されます。

  • self が弱参照になっていないスコープでこの束縛を行おうとするとコンパイルエラーになります。例えば通常のメソッド本体では、self はそもそもオプショナルではないため、この構文は使えません。
  • 束縛には let のみが使えます。var let self = self のように var で束縛することはできません。この機能の対象はオブジェクト参照(クラスのインスタンス)であり、値型ではないため、let でも self のミュータブルなメソッドを呼ぶうえでの実害はありません。

既存のコードに破壊的な影響はなく、これまで strongSelf などの別名を使っていたコードもそのまま動き続けます。新しく書くコードで、別名を介さずに self のまま昇格させられる、というのが本提案の実質です。