Allow using optional binding to upgrade self from a weak to strong reference
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 だったり ss、s、あるいはその場の思いつきの名前だったりと揺れが生じがちです。統一を強制するコンパイラ上の仕組みが無く、コードベースが大きくなるほどノイズになり、読み解きの負担を増やしていました。
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 のまま昇格させられる、というのが本提案の実質です。