【Swift】キャプチャリストって何のために使うの?

こんにちは!Swift勉強中の「ゆとりくん」です!
今回の疑問は「キャプチャリストは何のために用いるのか」です。調査した内容をまとめましたので、ぜひ参考にしてみてください!

当ブログでは記事内で紹介する参考文献にアフィリエイトリンクを付与する場合がございます。それらはすべて著者が実際に利用している文献ですので、安心して参照いただけます。

※本記事は、著者が学習した内容をまとめた内容となります。
内容の精査には注意を払っていますが、誤りがある場合がございますので予めご了承ください。誤りについてはフォームからご一報いただけると幸いです。

スポンサーリンク
目次

環境

この記事は以下のバージョン時点での情報をまとめています

【XCode】15.3
【Swift】5.10
【iOS】17.1.2
【macOS】Sonoma 14.2

学習中に謎のコードに遭遇……!!

事の発端は、『Hacking with iOS – UIKit Edition』の学習中に遭遇した以下のコードでした。

UIAlertAction(title: "Submit", style: .default){ [weak self, weak uac] action in
   //ここにクロージャの処理内容が記載されていた
    //selfとuac(UIAlertController)にアクセスする
}
ゆとりくん

やばい……なんで配列がクロージャ定義の前にあるんだ……??
しかも”weak”がついてる。これ何だっけ……😂

ということで、調べてみることにしました。

キャプチャリストとは何か

書籍『Swift実践入門』を参照すると、配列[weak self, weak uac]の正体は”キャプチャリスト”だと判明しました。
以下が、参考にした箇所です。

キャプチャリストを定義するには、クロージャの引数の定義の前に[]を追加し、内部にweakキーワードもしくはunownedキーワードと変数名もしくは定数名の組み合わせを , 区切りで列挙します。

石川洋資, 西山勇世. Swift実践入門(第3版 第2刷). 技術評論社. 2021. p296

冒頭のコードをもう一度見直してみると、

UIAlertAction(title: "Submit", style: .default){ [weak self, weak uac] action in
   //ここにクロージャの処理内容を記載する
    //処理中では、selfとuac(UIAlertController)にアクセスする
}

配列[weak self, weak uac]は、クロージャの引数の定義の前に配置されています。
また、配列の内部にはweakキーワードが付与された変数名が ,区切りで列挙されています。
よって、『Swift実践入門』の説明と特徴が合致するため、配列[weak self, weak uac]はキャプチャリストだと言えそうです。

キャプチャリストとはどのようなものなのでしょうか。

公式ドキュメントの説明を読むと「クロージャが”キャプチャ”する参照型の変数や定数について、”弱参照”または”強参照”を明示的に指定するためのもの」だということが分かります。

ゆとりくん

どうやら「キャプチャリスト」を理解するためには、
「キャプチャ」「弱参照」「強参照」についても知っておく必要がありそうだ……。


ということで、これらについてもそれぞれ調べてみることにしました。

キャプチャとは何か

再び『Swift実践入門』を参照しました。本に記載されている以下の例が分かりやすかったです。

let greeting: (String) -> String
do{
    let symbol = "!"
    greeting = { user in
        return "Hello, \(user)\(symbol)"
    }
}

greeting("Ishikawa") // Hello, Ishikawa!
symbol // symbolは別のスコープで定義されているためコンパイルエラー

 定数greetingはdo文のスコープ外で宣言されているためdo文の外からも利用できますが、定数symbolはdo文のスコープ内で宣言されているためdo文の外からは利用できません。しかし、定数greetingに代入されたクロージャは、内部でsymbolを利用しているにも関わらず、do文の外で実行できています。これは、クロージャがキャプチャによって、自分自身が定義されたスコープの変数や定数への参照を保持することで実現されています。

石川洋資, 西山勇世. Swift実践入門(第3版 第2刷). 技術評論社. 2021. p141

要するにキャプチャとは「あるクロージャが、自身が定義されたスコープからアクセスすることのできる定数・変数への参照を保持する機能」だと言えそうです。

いつもお世話になっている必読書

強参照・弱参照とは何か

強参照と弱参照の違いについては、以下を参照しました。

(前略)クラスのインスタンスへの参照には、強参照と弱参照の2種類があります。強参照は参照カウントを1つカウントアップし、弱参照はカウントアップしません。

石川洋資, 西山勇世. Swift実践入門(第3版 第2刷). 技術評論社. 2021. p292

要するに”強参照””弱参照”とは、特定のオブジェクトへの参照方法を指す用語であり、その違いは参照時に”参照カウント”を加算するか否かのようです。

“参照カウント”……。また新しい用語が出てきてしまいました。

参照カウントとは何か

参照カウントについて理解するためには、Swiftにおけるクラスのメモリ管理について知っておく必要があります。

Swiftではクラスのメモリ管理のために、ARC(Automatic Reference Counting)という方式を採用しています。ARCについては、公式ドキュメントを参照しました。

To make sure that instances don’t disappear while they’re still needed, ARC tracks how many properties, constants, and variables are currently referring to each class instance. ARC will not deallocate an instance as long as at least one active reference to that instance still exists.

(ARCは、インスタンスが必要なまま消えてしまわないよう、各クラス インスタンスを参照しているプロパティ、定数、変数の数を追跡します。 ARC は、インスタンスへのアクティブな参照が少なくとも 1 つ存在する限り、インスタンスの割り当てを解除しません。)

The Swift Programming Language(5.10)-Automatic Reference Counting-How ARC Working

つまり、ARCは各クラスのインスタンスを参照しているプロパティ・定数・変数の個数(=参照カウント)を監視しており、参照カウントが0になったインスタンスのメモリを解放するということです。

弱参照・強参照とは何か

ここで再び、先ほど引用した強参照・弱参照に関する説明を思い出してみましょう。

(前略)クラスのインスタンスへの参照には、強参照と弱参照の2種類があります。強参照は参照カウントを1つカウントアップし、弱参照はカウントアップしません。

石川洋資, 西山勇世. Swift実践入門(第3版 第2刷). 技術評論社. 2021. p292

つまり、プロパティ・定数・変数からインスタンスを参照する際に、参照カウントを加算する参照方法が”強参照”であり、参照カウントを加算しない参照方法が”弱参照”であるということが分かりました。

ゆとりくん

強参照と弱参照のどちらを選ぶかによって、ARCがクラスインスタンスのメモリを解放するタイミングに違いが生じるんだね。

ここまでの内容まとめ

すこし情報が多くなってきたので、ここまでで分かったことをまとめておきます。

特定のオブジェクトを参照するとき・・・

何も付与せず変数・定数・プロパティの宣言をする場合(例 var apple = ⚪︎⚪︎): デフォルトの強参照が利用される。参照先のインスタンスのメモリが解放されないよう、ARCの参照カウントを加算して参照する。

weakキーワードを付与した場合(例 weak var apple = ⚪︎⚪︎): 弱参照となる。ARCの参照カウントを加算しないため、参照先のインスタンスが参照時にすでに解放されている可能性がある。解放されたインスタンスを参照すると、nilとなる

unownedキーワードを付与した場合(例 unowned var apple = ⚪︎⚪︎): 弱参照となる。ARCの参照カウントを加算しないため、参照先のインスタンスが参照時にすでに解放されている可能性がある。解放されたインスタンスを参照すると、実行時エラーとなる

そして、

クロージャによるキャプチャ時の参照方法を制御したいとき・・・

クロージャの引数の定義の前にキャプチャリスト([]の内部にweak・unownedキーワードと、変数名・定数名を組み合わせ , 区切りで列挙した配列)を付与する。

(例)
let exampleClosure = {[weak object1, unowned object2] () -> Void in
//object1, object2を弱参照で(ARCの監視する参照カウントを加算せずに)キャプチャする
}

以上の内容を踏まえた上で、もう一度、冒頭で取り上げたコードを見てみましょう。

UIAlertAction(title: "Submit", style: .default){ [weak self, weak uac] action in
   //ここにクロージャの処理内容が記載されていた
    //selfとuac(UIAlertController)にアクセスする
}

このコードは、

  • 配列[weak self, weak uac]はキャプチャリストである。
  • このキャプチャリストは、トレイリングクロージャとして定義されたクロージャが、selfおよびuac変数を弱参照でキャプチャすることを指示している。
  • クロージャはselfとuacを弱参照でキャプチャしているため、クロージャ内の処理でselfとuacを参照する際、参照先のインスタンスがARCによって解放されている可能性がある。
  • weakキーワードでの弱参照を行なっているため、解放されたインスタンスを参照すると、nilが返ってくる。

という意味だったことが分かりました!

ゆとりくん

意味は分かった!けど、あと1点だけモヤモヤするな……。

そのモヤモヤの正体は「そもそも、なぜ弱参照を用いる必要があるのか」が分かっていないことです。
最後にこの点をまとめておきます。

なぜ弱参照を用いるのか

その答えは、一定の条件下では強参照が問題を引き起こすからです。

以下は、強参照が引き起こす問題について公式ドキュメントが図を用いて説明してくれているものです。
「アパートの部屋を借りる人」を表すPersonクラスと「人に貸し出されるアパートの部屋」を表すApartmentクラスを例に、強参照が引き起こす”循環参照”という問題と、その解消法を説明してくれています。

順を追って見てみましょう。

STEP
PersonインスタンスとApartmentインスタンスを作成し、変数に格納する

Personインスタンス・Apartmentインスタンスを作成し、それぞれを変数john・変数unit4Aに格納します。このとき変数に格納されるクラスインスタンスへの参照は、デフォルトで強参照が用いられるため、ARCはPersonインスタンス・Apartmentインスタンスの参照カウントをそれぞれ+1します(矢印のstrong部分)。

STEP
プロパティにそれぞれのインスタンスを格納する

PersonインスタンスのapartmentプロパティにApartmentインスタンスへの参照を、ApartmentインスタンスのtenantプロパティにPersonインスタンスへの参照をそれぞれ格納し、インスタンス同士で参照し合う状況を作ります。

プロパティからの参照もデフォルトでは強参照となるので、ARCはPersonインスタンス・Apartmentインスタンスの参照カウントをそれぞれさらに+1します。

その結果、下の図のように、Personインスタンス・Apartmentインスタンス共に合計2つの強参照を受ける(矢印が2本向いている)状態となります。

STEP
変数をnilにする(循環参照問題の発生)

変数johnおよび変数unit4Aをnilにします。
すると、変数から各インスタンスに伸びていた強参照の矢印がなくなります。

しかしインスタンス同士の強参照は残ったまま、参照カウントとして計上され続けています。よってARCはまだインスタンスが使用中だと判断し、割り当てたメモリ領域を解放しません。

こうして不要なはずのメモリが削除されず、メモリリークが発生します。この問題を循環参照と呼びます。

STEP
弱参照を用いる(循環参照の解消)

この循環参照を解消するために、弱参照が活躍します。

ApartmentインスタンスのtenantプロパティにPersonインスタンスの参照を格納する際、weak var tenant = <Person instance>のように記述し、弱参照を用います。

すると、STEP2でApartmentインスタンスからPersonインスタンスに向いていた矢印が、強参照から弱参照に変更されます。

この状態でSTEP3のようにjohnをnilにすると、Personインスタンスに向けられる矢印は弱参照(weakの矢印)1本のみとなります。ARCは弱参照を参照カウントとして加算しないため、Personインスタンスの参照カウントが0となり、メモリを解放します。

Personインスタンスが解放されたことにより、PersonインスタンスからApartmentインスタンスに向いていた強参照の矢印もなくなります。

この状態で変数unit4Aをnilにすれば、Apartmentインスタンスの参照カウントも0となり、ARCはApartmentインスタンスを解放できます。

 

以上が、強参照が引き起こす問題と弱参照を用いるべき理由の説明となります。

上記の説明はクラスのインスタンス同士の参照が循環参照を招く例でしたが、これと同じような状態がクロージャがクラスのインスタンスをキャプチャする場合にも起こり得ます。

この図の場合、クロージャでキャプチャリストを利用しクラスインスタンスへの参照を弱参照にする(青からオレンジへの矢印をweakにする)ことで、循環参照を解消することができますね!

そしてこの図を用いれば、冒頭のコードがキャプチャリストを利用していた理由を説明できます。

UIAlertAction(title: "Submit", style: .default){ [weak self, weak uac] action in
   //ここにクロージャの処理内容が記載されていた
    //selfとuac(UIAlertController)にアクセスする
}

クロージャがself(UIViewControllerクラスのインスタンス)およびuac(UIAlertControllerクラスのインスタンス)をキャプチャする際に、インスタンスとクロージャ間で循環参照が生じないようにキャプチャリストを用いて弱参照でキャプチャしていた、ということですね!

ゆとりくん

これでようやく、全ての謎が解けたー!

おわりに

原理は理解できましたが、循環参照が発生しそうな箇所を正確に見極めるのは、今の自分にはまだ難しそうです。

しかし、『Hacking with iOS – UIKit Edition』の中にこのような記述を見つけ、気が楽になりました。

In fact, Swift makes it so easy that you will use its solution even when you’re not sure if there’s a cycle simply because you might as well.

(実際、Swift を使用すると非常に簡単になるため、サイクルがあるかどうかわからない場合でも、単にそうするかもしれないという理由だけでそのソリューションを使用することになります。)

Hacking with iOS – UIKit Edition p232

ひとまず、

クロージャから参照型の変数や定数をキャプチャするときには、キャプチャリストを用いて弱参照でキャプチャする!

そうすれば循環参照の発生を抑制することができる!

これらを意識して、今後の実装を行いたいと思います。

誤りに気づいたら、逐一更新していきたいと思います。
ここまでご覧くださり、ありがとうございました!

参考文献

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次