Swift Digest
SE-0269 | Swift Evolution

Increase availability of implicit self in @escaping closures when reference cycles are unlikely to occur

Proposal
SE-0269
Authors
Frederick Kellison-Linn
Review Manager
Ted Kremenek
Status
Implemented (Swift 5.3)

01 何が問題だったのか

エスケープするクロージャの中でクラスのインスタンスの self を暗黙に省略しようとすると、Swift のコンパイラは必ず self. を明示するように要求してきました。

class Test {
  var x = 0
  func execute(_ work: @escaping () -> Void) {
    work()
  }
  func method() {
    execute {
      // error: reference to property 'x' in closure requires
      //        explicit 'self.' to make capture semantics explicit
      x += 1
    }
  }
}

このルールは、エスケープするクロージャが self を強く捕捉して参照循環を生むことに、書き手を気付かせる目的で設けられています。しかし実際の使い勝手には以下のような問題があります。

  • 同じクロージャの中で self のメンバを何度も使うと、self. を大量に繰り返すことになり、ノイズが増えて読みにくくなります。初回の self. で「ここで self を捕捉している」という意図は十分伝わっているのに、2回目以降の self. は同じ警告を繰り返しているだけで、情報としては冗長です。
  • SwiftUI や Combine、PromiseKit のようにエスケープするクロージャを多用するコードでは、「すべての箇所で self. を書く」か「揺れを許容する」かを選ばされます。Apple の推奨スタイルは可能な限り self. を省略することなので、非同期コードだけ self. だらけになるのはちぐはぐです。
  • 値型 (structenum) では参照循環は起きようがないのに、値型の self にも同じ制約が課されていました。そのため値型のメソッドからエスケープするクロージャを作ると、無意味な self. を書かされることになります。
  • エラーに対するフィックスイットが「とりあえず self. を付ける」ものしかないため、参照循環が実際に問題になりうる場面でも、書き手がフィックスイットを機械的に適用して警告を無視する習慣がついてしまいます。

つまり、現在のルールは「self の捕捉を意識させたい」という本来の目的に対して過剰に鳴りすぎており、逆に警告の価値を下げてしまっている、というのが問題です。

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

エスケープするクロージャの中でも、書き手が self を捕捉する意図を明確にしている場合や、そもそも参照循環が起きない場合には、self. を省略して暗黙に書けるようにします。具体的には次の2つのケースです。

クロージャのキャプチャリストで self を明示した場合

キャプチャリストに [self] を書いておけば、そのクロージャの本体では self. を省略できます。

class Test {
  var x = 0
  func execute(_ work: @escaping () -> Void) {
    work()
  }
  func method() {
    execute { [self] in
      let foo = doFirstThing()
      performWork(with: bar)
      doSecondThing(with: foo)
      cleanup()
    }
  }

  func doFirstThing() -> Int { 0 }
  func performWork(with: Int) {}
  func doSecondThing(with: Int) {}
  func cleanup() {}
  var bar: Int { 0 }
}

self を捕捉する」という意図は [self] の1箇所で表明されるので、本体では毎回 self. を書かなくて済みます。従来どおり各参照に self. を付ける書き方も引き続き使えます。

この扱いは [self] のほか、[unowned self][unowned(safe) self][unowned(unsafe) self][self = self] のように self という名前で直接捕捉している場合にも適用されます。一方、[y = self] のように別名で捕捉した場合は、暗黙の self は有効になりません。

コンパイラのフィックスイットも追加され、暗黙の self を使おうとしてエラーになった箇所では、「[self] in を挿入する」選択肢が提示されるようになります。既存のキャプチャリストがあれば self, を差し込み、パラメータ宣言があれば [self] を適切な位置に挿入する、といった形でケースごとに調整されます。

self が値型の場合

self の型が structenum のような値型であれば、そもそも参照循環は起きません。そのため値型のメソッド内のエスケープするクロージャでは、キャプチャリストへの明示も self. の明示も不要になります。

struct Test {
  var x = 0
  func execute(_ work: @escaping () -> Void) {
    work()
  }
  func method() {
    execute {
      print(x) // self. も [self] も不要
    }
  }
}

ネストしたクロージャでの注意

暗黙の self が有効になるのは、最も内側のエスケープするクロージャself を捕捉しているときだけです。外側のクロージャで [self] を書いても、内側のエスケープするクロージャで暗黙の self を使うには、内側でも改めて [self] を書く必要があります。

execute { [self] in
  execute {
    // error: 内側のクロージャが self を捕捉していないので
    //        x に self. が必要
    x += 1
  }
}

weak self の扱い

[weak self] で捕捉した場合は、この提案の対象外です。弱参照された self に対して裸の x と書いたときに何を意味させるか(self?.x 相当にするのか、別の記法を導入するのか、guard で強参照に束ね直すことを促すのか)は設計上の論点が多く、この提案では踏み込まないことになっています。[weak self] で捕捉しているクロージャの中で暗黙の self を使おうとすると、「weak キャプチャは暗黙の self を有効にしない」という旨のノートが添えられます。また、[self = "hello"] のように self と同名の別の変数を捕捉した場合も、同様に暗黙の self は使えず、その旨のノートが出ます。

今後の見通し

この提案のスコープ外ですが、self. を書かずに済むケースをさらに整理することと並行して、参照循環を見逃しているケースを塞いでいく方向も議論されています。たとえば、execute(inc) のように self のメソッドをそのまま関数値として渡すと、現在は self が捕捉されるのにエラーにならない、といった穴があります。また、weak self で捕捉したときの扱いも将来の検討課題として残されています。いずれも本提案で実現を約束するものではありません。