🤼

関数の引数にクロージャを指定する際にデフォルトのパラメータとしてOptoinalではなくカラのクロージャを利用するほうがメリットが大きい

2022/01/03に公開

はじめに

この文章は書籍Effective Swiftのための下書きの一部「デフォルト動作のためにデフォルト引数を利用する」の項目をアルファ版として公開してみます。

なお、下書きのためこちらのタイミングで非公開にすることはあります。

その他、もしかしたら私の勘違いがあったりするかもしれません。できればコメントください。

気になっているのは、Optional型の仕様を雰囲気で知ってるだけなので、公開して見返しつつ修正していくかもしれません。

デフォルトのパラメータを利用する際にカラのクロージャを利用する

クロージャを関数のデフォルト引数として定義して省略可能にしたい場合、
Optionalのクロージャにするのではなく、
カラのクロージャをデフォルト引数とすることを考えた方が良いでしょう。
その理由のひとつは、Optionalのクロージャは暗黙的にescaping動作をするため、
selfの明記を必要とします。
selfを明記させることで循環参照を避けるために強参照を避ける手段を考慮することになります。

その他大きな理由ではないですが、クロージャがOptionalな場合にはForce Unwrap(!)してもコンパイルエラーにはならず、予期せずクラッシュする原因にもなるでしょう。

2種類のメソッドのみを持ち、ランダムな整数を引数に渡すだけの型を利用する例を示します。

/// Boolを引数に取るクロージャの関数を実行するだけの型。
/// 必須のクロージャとOptionalなクロージャ2つのメソッドを持つ。
struct BoolInteractor {
    func perform(closure: (Bool) -> () = { _ in }) {
        closure(Bool.random())
    }

    func execute(closure: ((Bool) -> ())? = nil) {
        closure?(Bool.random()) // closure!()にするのはミス
    }
}

/// BoolInteractorを実行すだけの型
class Person {
    private let firstName: String
    private let interactor: BoolInteractor

    init(
        firstName: String,
        interactor: BoolInteractor = BoolInteractor()
    ) {
        self.firstName = firstName
        self.interactor = interactor
    }

    func f1() {
        // どちらも引数のクロージャを省略できる
        interactor.perform()
        interactor.execute()
    }

    func f2() {
        interactor.perform { _ in
            // もちろんselfを省略できるのでエラーにならない
            print("\(firstName) ")
        }

        interactor.execute { _ in
            // selfを明示しないとコンパイルエラー
            // このselfは強参照しているため、
            // ぱっと見では強参照を避けたくなるはず。
            print("\(self.firstName)")
        }
    }
}

Personのメソッドf1()はどちらも引数が省略できてることを示すだけのサンプルです。
メソッドf2()は非Optionalのクロージャperformの実行と、Optionalのクロージャを引数とするexecuteの記述を比較しています。
executeはPersonのプロパティをクロージャ内に記載するとselfがなければコンパイルエラーとなります。
これはクロージャをOptionalにするとそのクロージャが@escaping修飾子をつけられた時と同じように、
クロージャをescapingしているためです。
このようなコードの場合、クロージャが実行されている瞬間は循環参照をしている状態になるでしょうが、クロージャ自体が参照型の何かに保持されているわけではないためメモリリークはしません。
しかしexutecteメソッドのクロージャ内でselfを明記しなければいけないため、
メモリリークを避ける必要がなくても循環参照を避けるよう強参照を避けたくなり、
無駄の多いコードとなってしまうわけです。
このことから特に大きな理由がない場合は非Optionalとして省略したい場合はからのクロージャとするほうが、
Optionalのクロージャとするようりもメリットが大きいでしょう。

クロージャのescapingとは何か

関数の外にクロージャの処理がescapingされるということは、
コンパイラがクロージャにキャプチャする変数が何かを推測することになります。

escapingされてないクロージャでは、例えば先程の例にある非Optionalなperformメソッドの呼び出し箇所では、firstName変数がどこからキャプチャされるかはselfがなくても分かりきっているわけです。

TODO: 図

escapingされているということは、クロージャが関数の外にあるわけですから、キャプチャするものが何かを明示してあげる必要性があるわけです。

TODO: 図

var fistName 外の世界の

// クロージャが関数の外にescapingされるということは、
// クロージャ内部にキャプチャする変数が何かを明確にする必要がある
closure {
  print("\(firstName)") // firstNameは外のなの?的に推測するためにselfがいる
}

なぜクロージャをOptionalにするとescaping扱いするのか

おそらくクロージャをOptionalにするだけでescapingしてしまうのか理解すれば、
Optionalのクロージャを理解して避けたくなるはずですので簡単に解説します。
escaping扱いするその理由は、そもそもOptionalとは列挙型のOptional<T>にラップされた型であるためです。
つまり先程の例(Bool) -> ()クロージャはOptional<(Bool) -> ()>です。

struct BoolInteractor {    
    // func execute(closure: ((Bool) -> ())? = nil) と同じ
    func execute(closure: Optional<(Bool) -> ()> = nil) {
        closure?(Bool.random())
    }
}

クロージャが関数の外にescapingされているのは、つまりOptional型がクロージャを持っているためです。

Optional型がクロージャを保持するという意味を明確にするため、
enumのOptionalを作っていければ良いのですが
説明をシンプルにするためにクロージャを保持する構造体MyWrapper型を作って利用する例を示します。

struct MyWrapper {
    private var closure: ((Bool) -> ())?
    init(closure: ((Bool) -> ())?) {
        self.closure = closure
    }

    func callAsFunction(_ flag: Bool) {
        closure?(flag)
    }
}

struct BoolInteractor {
    func call(closure: MyOptional) {
        closure(Bool.random())
    }
}

class Person {
    private let firstName: String
    private let interactor: BoolInteractor

    init(
        firstName: String,
        interactor: BoolInteractor = BoolInteractor()
    ) {
        self.firstName = firstName
        self.interactor = interactor
    }

    func f2() {
        let myWrapper = MyWrapper { _ in
            // selfがないとコンパイルエラー
            print("\(self.firstName)")
        }

        interactor.call(closure: myWrapper)
    }
}

MyWrapper型では内部でクロージャをプロパティで保持していることで、
実際のクロージャはescapingされているわけです。
これと同じようにOptional<T>型はクロージャを内部で保持します。
そのためにOptionalなクロージャは関数の外にescapingされるわけです。
構造体であるMyWrapper型も、列挙型であるOptional<T>も値型なために参照カウントが上がらず、破棄されることで循環参照が続かずにメモリリークすることはありません。

Discussion