post Image
Swiftでの循環参照を避けるための決まり文句

やること

Swiftでコーディングしていると、クロージャによる書き方が頻出してきますし、どんどんクロージャで書きたくなってくると思います。

クロージャを使い出すとぶち当たるのが、self句の「循環参照」という問題です。
コーディング中は意外と表面化しませんが、実際にアプリとしての利用をし始めると影響が顕著になります。

循環参照の一番の問題は、「生成したインスタンスがメモリから解放されず、右肩上がりにメモリを消費し続けること」です。
アプリの動作は特に問題なさそうなのに、長い間利用し続けるとどんどん調子がおかしくなっていったり、ある時点で急にクラッシュする場合は、循環参照によるメモリリークの影響だと疑えます。

一番わかりやすいのは、UIViewController上で循環参照が発生し、画面遷移するたびにメモリ使用量が右肩上がりになっていく状態です。
画面遷移周りでこのようなメモリリークが発生しているとき、ほとんどの場合「UIViewController上での循環参照」が原因です。

循環参照は、気をつけていてもどこかで発生していて、
後々に膨大?なコードの中から「どこが循環参照しているんだ…」という犯人探し作業に追われることになります。

こんな循環参照探しに囚われたくないので、そもそも問題が起きないように書くようにしてしまいます。

前提

知識

  • Swiftでコードが書ける
  • 循環参照という言葉を知っている
  • @escaping を知っている
  • 「オプショナル型」という概念を知っている

結論

恐らく色々な方法はあると思いますし、色々な文化やコンテキストがあると思いますが、
私がたどり着いた結論は以下の通りです。

循環参照を避ける決まり文句
func someFunction() {
    // 1. クロージャの先頭で [weak self] を定義する。
    self.someEscapingFunction { [weak self] in
        // 2. クロージャの1行目で self をguard句で早期リターンする。
        guard let _ = self else { return }

        // 3-1. guard句によって self の存在は保証されるので、
        //      self参照時は「!:強制的アンラップ」で利用してOK。
        self!.otherFunction()

        // 3-2. 「?:オプショナル・チェイニング」 での参照もOK。
        //       guard句を使わない場合はこちらが安全。
        self?.otherFunction() 
    }
}

ポイント

1. [weak self] の定義

クロージャの先頭で [weak self] を定義します。
循環参照回避は [unowned self] という書き方もありますが、「どっちでもいい」とかは無いです。

unowned の使い所は「循環参照は避けたいけど、このときselfは絶対存在するんだよなぁ」というときだけです。
これがどういうことかを理解して、意識的に unowned を利用するという動機がない限りは weak を使う方が安全です。

2. guard let _ = self else { return } で早期リターン

クロージャ内の1行目で、[weak self] によってオプショナル型を解決します。
クロージャ内で参照されたselfがnilだった場合(=selfが既に解放されていた場合)には、何も処理を行わずに早期リターンさせます。

guard した self 自体は何も利用しないので、変数名を定義することはしません。
(定義して利用してもいいと思います。そのあたりは好みかと思います。)

変数名weakSelfで定義した例
guard let weakSelf = self else { return } 

weakSelf.otherFunction()
self!.otherFunction()
// 定義した weakSelf も使えるし 通常の self も使えるしで、混乱しないように注意します。

また if let で書く方法もありますが、ネストが深くなるので早期リターンの方がスッキリします。

ifletで定義した例
if let weakSelf = self {
   weakSelf.otherFunction()
   // ... 処理本文のネストが1つ増えてしまうのが気にくわない...
}

3. self! で利用

guard句による早期リターンによって、
処理の順番とか破棄のタイミングとか業務上の都合とかのふわふわした保証ではなく、
「クロージャ処理中にselfが存在することはコンパイル上で絶対100%保証される」ので、
クロージャ処理内では安心して self! による強制的アンラップでselfを利用します。

@escaping ではないときは…?

呼び出す関数が @escaping ではないときは、循環参照を気にする必要はありません。
例えば、「someArray.map({ $0 })」 や 「someArray.forEach { in } 」といった関数が、
わかりやすい @escapingではない の例です。

  • でも呼び出す関数が@escapingかどうかいちいち調べるのもしんどい…
  • 今はいいけど、後々にいつのまにか @escaping になるかもしれない…

等々、面倒臭い事情を気にしたくないなら、最初から全部 [weak self] でやってしまえばいいです。

unownedにも言えることですが、[weak self] を徹底することによる弊害は特にありません。
コードの書きっぷりが少し増えるくらいです。

それが嫌な人は、そもそもそれを気にできるくらいの技術力を持っているはずなので、
unowned, weak, @escaping 等々における弱参照・強参照を全て把握して、用途に応じて綺麗に書いてください。


補足: オプショナル型のアンラップという概念について

かいつまんで補足します。

ヌルポの問題

nullのインスタンスにアクセスするとクラッシュします。
いわゆるヌルポ(NullPointerException)の問題であり、特にJavaで顕著かと思います。

Javaのヌルポ
String someString = null;

someString.isEmpty(); // ヌルポ!!クラッシュ!!

これは、
nullかもしれないインスタンスなのに、コンパイル上何の警告もなくそのインスタンスへのアクセスを許すために起きる問題です。
なので、コーディング者はそのオブジェクトがnullなのかどうかを常にビクビクすることになります。

javaのヌルポにビクビクしたコード
String someString = "nullじゃないよ";

// nullじゃなかったときだけだぜ... 頼むぜ...
if (someString != null) {
   someString.isEmpty();
}

ヌルポを回避する解決策としてのオプショナル型

オプショナル型は、nullかもしれないインスタンスにはそういう印をつけて、コンパイル時点でわかるようにしよう、という感じのものです。(適当)
(詳しくはオプショナル型そのものについての背景や歴史を参照してください。)

Swiftにはこのオプショナル型という概念があるため、
Swiftの変数定義で 後ろに「?:オプショナル型の印」が付かない通常の変数は、絶対nullにはならないことが(コンパイル上は)保証されます。

swiftは易々とnullになれない
// ❌ コンパイルエラーになる
let someString: String = nil

// ⭕️ ゆえに、変数宣言の段階で null にならないことが保証される
let someString2: String = ""

someString2.isEmpty // 少なくとも絶対ヌルポは発生しない

オブジェクトがnullになり得る場合は、オプショナル型として明示的に宣言する必要があります。

swiftでnullになり得る場合
// 変数宣言の段階で、オプショナル型の印(=>?)をつける
let someOptionalString: String? = nil

// ❌ null かもしれないので そのままアクセスするとコンパイルエラーになる
someOptionalString.isEmpty

オプショナル型へのアクセス方法

オプショナル型の変数へは、オプショナル型へのアクセス用の方法でアクセスすることになります。

1. 「if let」で nullではないことを保証してからアクセス

// 変数宣言の段階で、オプショナル型の印(=>?)をつける
let someOptionalString: String? = nil

// ⭕️ 「if let」で、 null かもしれないのをnullではないことを保証してからアクセスする
if let hogeString = someOptionalString {
    // hogeStringという変数名で、nullになり得ない通常のString型として再宣言できた場合だけアクセスできる    
    hogeString.isEmpty   
}

2. guard句 でnullではないことを保証してからアクセス

// 変数宣言の段階で、オプショナル型の印(=>?)をつける
let someOptionalString: String? = nil

// ⭕️ 「guard let ~ else { ~ }」で、 null かもしれないのをnullではないことを保証してからアクセスする。
//        ただしguard句はその場で処理を抜けてしまうのが常なので、利用する場合は関数の先頭などになる場合が多い。
guard let hogeString = someOptionalString else { return }

// hogeStringという変数名で、nullになり得ない通常のString型としてアクセスできる    
hogeString.isEmpty   

3. 「!」や「?」で、nullかもしれない状態のままアクセスする

オプショナル型に直接アクセスする場合は、変数の後ろに「!」や「?」をつけてアクセスします。

3-1. 「nullのはずがない!!」という強い意志でアクセス

タイトルの通りですが、イメージ的には、
「nullかもしれないオブジェクトだけど、nullのはずがないという強い意志でアクセスする!!!」
というビックリマークがたくさん付く感じの意気込みでアクセスします。

// 変数宣言の段階で、オプショナル型の印(=>?)をつける
let someOptionalString: String? = nil

// ⭕️ nullかもしれないけど「ここでnullにはならない!」というコーディング者の強い意志によってアクセスする
someOptionalString!.isEmpty   

もちろん、上記の例では、someOptionalString は null なので、
コンパイルは通りますが someOptionalString!.isEmpty の処理に来た時点でヌルポでクラッシュします。

何らかの前提がない限り「nullでないことが保証されない」ので、コーディング時は注意する必要があります。

3-2. 「nullかもしれない…?」という薄弱な意志でアクセス

タイトルの通りですが、イメージ的には、
「このオブジェクト…nullかもしれない…?」
というハテナマークが付く感じのおっかなびっくり加減でアクセスします。

// 変数宣言の段階で、オプショナル型の印(=>?)をつける
let someOptionalString: String? = nil

// ⭕️ nullかもしれないけど、nullでもまぁクラッシュしなけりゃいいや...というコーディング者の薄弱な意志でアクセスする
someOptionalString?.isEmpty   

コンパイルは通りますし、 someOptionalString?.isEmpty の処理に来た時点でもクラッシュはしません。
someOptionalString? のアクセス時にnullだった場合、そのアクセス上の処理は全てなかったものとしてキャンセルされます

この場合、「処理がキャンセルされる」のが曲者です。

「コンパイルは通るしクラッシュもしないし処理も走ってるっぽいけど、想定通りに動作してないぞ?」

という場合は、行なっているつもりの処理が実は全部キャンセルされてるだけの場合があります。
こういった潜在的なバグは問題箇所の特定が難しくなるので、注意が必要です。


『 Swift 』Article List