iOS・Android

【Swift】キャプチャリスト:クロージャにおける変数の振る舞いとメモリ管理

iOS・Android
この記事は約9分で読めます。

はじめに

Swiftにおけるクロージャは、面白い機能の一つです。
独立した機能を持ったコードブロックで、名前を持たない関数のようなものとして捉えることができます。

以下はクロージャを簡単に実装した例です。

func function() -> String {
    return "Function"
}

let closure = {() -> String in
    return "Closure"
}

var a: () -> String

// closureは関数と同等に扱える。
a = function
a = closure

Swiftにおいて、クロージャは関数ポインターと統合されています。そのため、関数のように動作しますし、関数と同等に扱うことができます。さらに参照型なので、呼び出し都度一貫性を持って値を保持します。これにより、複数の場所で呼び出すことができます。

一方で、関数と明確な違いもあります。
「キャプチャリスト」は、クロージャと関数の違いの一つです。
クロージャは自分が定義されたコンテキストの変数や定数を「キャプチャ」することができます。これは、クロージャがスコープ外の変数や定数の値を「記憶」して、適切な文脈で使用することを実現する機能です。

クロージャはデフォルトで周囲のスコープの定数と変数をstrong(強参照)でキャプチャします。この時、キャプチャリストは暗黙に存在しているので、意識されることはあまりないかもしれません。


キャプチャリストの面白さは、キャプチャされた変数の振る舞いが、値型・参照型に応じて変化することと、メモリ管理との関連性にあります。

本稿では、値型と参照型における振る舞いの違いと、キャプチャリストとメモリ管理の関係性について解説します。

値型の変数をキャプチャする

以下は、キャプチャリストを使用してaというInt型(値型)変数の現在の値(この場合は0)を指定している例です。
クロージャが実行されるタイミングに関わらず、クロージャ内で使われるaの値はキャプチャリストで指定された時点の値が保持されます。

var a = 0
 var b = 0
 let closure = { [a] in
     print(a, b)
 }
 
 a = 10
 b = 10
 closure()
 
 // print "0 10"

内部スコープ内のaはクロージャが作成された時点のaの値( = 0 )をキャプチャしています。
クロージャ内部のaの値変化は、クロージャ外部のaの値変化に作用しません。

一方、キャプチャリストに指定しなかったbは、クロージャが実行されるタイミングで、現在の値を参照しています。

要するに、元の変数とクロージャ内の変数が別のメモリ領域を参照しているため、元の変数の値が変更されても、クロージャ内のキャプチャされた値は変化しないのです。

こうした特徴は、特定の状態の変数の値を、クロージャに固定したい場合に有用です。

値型のキャプチャリストの利用例

具体例として以下のシナリオを考えてみましょう:

  1. ユーザーがプロフィール画像を変更するために、画像Aを選択
  2. 画像Aをサーバー側に送信、非同期リクエストA実行
  3. リクエスト進行時、ユーザーが別の画像Bを選択
  4. アプリは画像Bをサーバー側に送信、非同期リクエストB実行

できれば、画像Bのレスポンス返却時のみUIを更新したいですね。
キャプチャリストを用いることで、上述の挙動を実現することができます。

リクエスト前に「変更をリクエストしたプロフィール写真URL」をキャプチャリストでキャプチャします。(クロージャが作成された時点の状態)
レスポンスが帰ってきた段階で、「変更をリクエストしたプロフィール写真URL」と「サーバーに新規に登録された写真URL」を比較します。一致した場合、UIの更新を行います。

このストーリーを箇条書きで表現すると以下になります:

  1. 初期の状態をキャプチャ:
    ユーザーが新しい画像Aを選択して変更をリクエスト。
    リクエストが発行される際に、その時点で選択されている画像のURLをクロージャのキャプチャリストに保存。
  2. 非同期リクエストA実行:
  3. ユーザーの選択の変更:
    リクエストの進行中に、別の画像Bを選択。
  4. 非同期リクエストB実行:
  5. レスポンスの処理:
    レスポンスがサーバから帰ってきたとき、キャプチャリストで保存した初期の状態と、サーバから返された新しい画像のURLを比較&更新処理

このように、キャプチャリストを用いることで、非同期の処理が完了した際に安全にUIを更新することができます。そして、ユーザーの最新の選択に基づくUIの更新を保証し、不要なUIの更新を回避できます。

参照型の変数をキャプチャする

以下は、キャプチャリストを使用してxというclassインスタンス(参照型)変数の値を指定している例です。

class SimpleClass {
     var value: Int = 0
 }
 var x = SimpleClass()
 var y = SimpleClass()
 let closure = { [x] in
     print(x.value, y.value)
 }
 
 x.value = 10
 y.value = 10
 closure()
 // print"10 10"

この時、クロージャ内部のxと外部のxは両方とも参照型なので、同じオブジェクトを参照します。

そのため、元の参照が指すオブジェクトの状態が変化すると、クロージャ内のキャプチャされた参照を通じてその変化が反映されます。(一貫性が保持される)

クロージャの循環参照を回避するためにキャプチャリストを使用する

循環参照を防ぐ目的でキャプチャリストを使用してみましょう。
循環参照とは、2つ以上の参照型インスタンスが、互いに強参照を保持し合っている状態を指します。これにより、どちらのインスタンスもメモリから解放されず、メモリリークが発生します。

クラスのインスタンスプロパティにクロージャを割り当て、そのクロージャがインスタンス(self)をキャプチャした場合、循環参照が発生します。
これはクロージャがインスタンスを強参照でキャプチャし、インスタンスもまたそのクロージャを強参照で保持しているためです。これは、クロージャがclassと同様に参照型であるために発生します。
言葉で言ってもよくわからないと思うので、サンプルコードを共有します。

 class HTMLElement {
     // nameは見出し要素の場合は "h1"、段落要素の場合は "p"、改行要素の場合は "br" など、要素の名前を示す
     let name: String
     // textはHTML要素内でレンダリングされるテキストを表す
     let text: String?
     
     // asHTMLはname と text を結合するクロージャを参照している
     lazy var asHTML: () -> String = {
         if let text = self.text {
             return "<\(self.name)>\(text)</\(self.name)>"
         } else {
             return "<\(self.name) />"
         }
     }
     
     // イニシャライザ
     init(name: String, text: String? = nil) {
         self.name = name
         self.text = text
     }
 
     deinit {
         print("\(name) のインスタンス割り当てが解除されました")
     }
 
 }

上記コードにおいて、asHTML プロパティはインスタンスメソッドのように使用されていますが、実際はクロージャプロパティです。
asHTML にアクセスされたとき、そのクロージャがメモリに保存されます。
それ以後、asHTML を呼び出すたびに、クロージャは再実行され、その都度新たな結果を計算して返します。
上記のHTMLElementクラスは、HTMLElementインスタンスと、asHTML 値に使用されるクロージャとの間に循環参照を引き起こします。

このとき、キャプチャリストを使用することで強参照の解決を図ることができます。
asHTMLのクロージャを宣言する際、弱参照または非所有参照であると宣言するのです。

弱参照は、参照先のオブジェクトが解放された後、キャプチャされた参照がnilになる可能性がある場合に使用します。要するに、弱参照は、classインスタンスよりクロージャがより長く存続する状況を想定しています。
これは、非同期処理のコールバックやデリゲートパターンなど、クラスインスタンスが解放された後でもクロージャが実行される可能性がある状況で選択されます。
クロージャとキャプチャするインスタンスが常に相互に参照し、常に同時に割り当てが解除される場合は、クロージャ内のキャプチャを非所有参照(unowned)として定義します。
キャプチャされた参照がnilになる可能性がないのであれば、非所有参照を使用するのがベターです。

asHTMLインスタンスが存続している時、HTMLElementインスタンスは必ず存在します。
今回のケースでは、非所有参照が適切ですね。

 lazy var asHTML: () -> String = {
      [unowned self] in
      if let text = self.text {
          return "<\(self.name)>\(text)</\(self.name)>"
      } else {
          return "<\(self.name) />"
      }
  }

終わりに

かなり細かい内容になってしまいましたが、本稿だけでもキャプチャリストの面白さを感じていただけたのではないでしょうか。キャプチャリストを理解することで、より安全なコーディングができるようになります。
本稿が理解の一助となれば幸いです。

ここまで読んでくださりありがとうございました。

参考:

Documentation


https://docs.swift.org/swift-book/documentation/the-swift-programming-language/expressions/

Documentation