Swiftの循環参照 | クロージャの場合

Swiftの循環参照 | クロージャの場合

循環参照の続きで、今回はクロージャ(Closures)の場合です。クロージャは関数をもっと一般化(抽象化?)したようなもので、実はクロージャも参照型であるということは前に説明しました。したがって、場合によりますが、クロージャでも循環参照が起こり得ます。

ここでは、どういう場合にクロージャでの循環参照(厳密に言うとクロージャとクラス間の循環参照)が起こるか、具体例を使って詳しく見ていきます。また、クロージャとクラス間の循環参照を解決するために必要なキャプチャリストの定義、その具体的な使い方についても簡単に説明します。

クロージャ(Closures)の循環参照

クロージャは参照型

クロージャ(closures)も参照型であるということは、以前クロージャのページで言及しました。ですから、実はクロージャでも循環参照(strong reference cycles)が起こり得ます。どういう場合に循環参照が起こるかというと、

(クラスインスタンスの)プロパティにクロージャを割り当て、さらにそのクロージャ本体から、元のインスタンスをキャプチャした場合

です。

なんか「イノベーションでブレイクスルーして」系の意味のないカタカナ語の羅列っぽいですが、一つ一つの言葉をちゃんと定義して意味を追っていけばそれほど難しくありません。「インスタンスをキャプチャ」と書きましたが、具体的にはインスタンス自身(selfプロパティ)だけじゃなく、インスタンスプロパティself.propertyや、インスタンスメソッドself.method()、などもあり得ます。文章で書くとややこしいですが、実際のサンプルコードを見ると分かると思います。

基本的にはクラス間の循環参照で説明した問題と同様ですが、今回説明する循環参照は、

クラスとクロージャの間に発生した循環参照

になります。

クロージャ・クラス間の循環参照の具体例

ここから、クラスとクロージャ間で起こる循環参照をどうやって解決するのか?という具体的な内容に踏み込んでいきます。結論から言うと、Swiftでは、クロージャのキャプチャリスト(closure capture list)という機能を使って循環参照を解決します。いきなり本題に入っても良いのですが、その前にそもそも

クラスとクロージャの間に起こる循環参照って何?

という部分を詳しく見ていきます。

// クロージャの循環参照
// マークダウン <-> html (タイトルタグにしか対応出来てないけど)
class Markdown {
    let tag: String
    let text: String

    // as markdown
    lazy var md: () -> String = {
        return "\(self.tag) \(self.text)"
    }

    func md2html() -> String? {
        switch self.tag {
            case "#":   return "h1"
            case "##":  return "h2"
            case "###": return "h3"
            default:    return nil
        }
    }

    lazy var html: () -> String = {
        if let tag = self.md2html() {
            return "<\(tag)>\(self.text)"
        } else {
            return self.text
        }
    }

    init(text: String, tag: String) {
        self.text = text
        self.tag = tag
    }

    deinit {
        print("Markdownクラスを破棄")
    }
}

公式マニュアルの具体例も分かりやすいですが、ほんの少し複雑な構造にしてみました。上記クラスMarkdownは、入力した文字列(text)とタグ(tag)から、markdown形式もしくはhtml形式の文字列を返すことの出来るクラスになっています。

では順番に見ていきます。

入力テキスト・タグ用の格納プロパティ | textとtag

入力したテキストとタグを保持するための格納プロパティ(stored properties)は説明不要だと思います。それぞれString型の定数で、初期化子(initializer)でちゃんと初期化されています。

class Markdown {
    let tag: String
    let text: String

    ....

    init(text: String, tag: String) {
        self.text = text
        self.tag = tag
    }
    
    ....
}

Markdown形式で返すプロパティmd

Markdownクラスは、後2つプロパティを持っています。1つはmdというlazyプロパティで、与えられたテキストと(markdown用の)タグを組み合わせて出力するためのプロパティです。

// as markdown
lazy var md: () -> String = {
    return "\(self.tag) \(self.text)"
}

mdプロパティはクロージャを参照しており、クロージャ本体では単にtagtextプロパティを並べて出力しているだけです。また、mdプロパティの「型」は() -> String、つまり

関数型(function types)で、パラメータは無く、戻り値がString

です。

mdプロパティのカスタマイズについては、後でhtmlプロパティと同時に説明します。

Html形式で返すプロパティhtml

もう1つのプロパティはhtmlというlazyプロパティです。間にmarkdownからhtmlに変換するためのメソッドmd2html()を挟んでいます。

// markdownをhtmlに変換
func md2html() -> String? {
    switch self.tag {
        case "#":   return "h1"
        case "##":  return "h2"
        case "###": return "h3"
        default:    return nil
    }
}

メソッド自体は単純で、switch文を使ってmarkdownタグに対応したhtmlタグを返しているだけです。今はタイトルにしか対応していないので、それ以外の場合はnilを返しています。

クロージャを動的カスタマイズすることを考えると、敢えてメソッドとしてmd2html()を用意しない方が良いかもしれません

このmd2html()メソッドを使って、与えられたテキストとmarkdownタグから、html文を出力しているのがhtmlプロパティです。

// 出力
lazy var html: () -> String = {
    if let tag = self.md2html() {
        return "<\(tag)>\(self.text)"
    } else {
        return self.text
    }
}

nilになることがあるので、optional bindingで場合分けをしています。このhtmlプロパティもクロージャを参照しており、型はmdプロパティと同様に() -> Stringです。

クロージャのカスタマイズ

mdhtmlは一見インスタンスメソッドのようです。しかし、こいつらは(() -> Stringという型の)クロージャを参照していますので、mdhtmlのデフォルトプロパティとしてのクロージャを、別のクロージャで動的に置き換えることが出来ます。

例えばmdプロパティに関して、今はmarkdownタグを前置きにしか出来ませんが、タグで挟みたいこともあります(強調とかイタリック体とか)。そのような場合には、新たにクロージャを用意してやれば

// 強調タグの場合、両側から挟む必要がある
var strong = Markdown(text: "強調", tag:"*")
strong.md = {
    return "\(strong.tag)\(strong.text)\(strong.tag)"
}
print(strong.md())
//"*強調*"と表示

という具合に、動的にmdプロパティの実装を変更出来ます。

なぜlazyプロパティになっているのか?

mdhtmlプロパティは、なんでlazyプロパティなのか?

という疑問が湧くと思います(私もそう思いました)。結論から言うと、プロパティ内部でself(ここでは自身が持つインスタンスプロパティ)へアクセスしているからです。

初期化のページで説明しましたが、Swiftでは1段回目の初期化が終わるまではselfプロパティ(及びインスタンスプロパティ、インスタンスメソッド)にはアクセス出来ません

上記の例では、mdhtmlプロパティ内部で、Markdownクラスが持つインスタンスプロパティへアクセスしています。lazy指定することにより、mdプロパティ自体の初期化はそれが初めて使われるまで保留されます。言い換えると、mdプロパティは初期化が終わった後に呼び出されることが保証されます(htmlプロパティに関しても同様)。

公式マニュアルでも「なぜlazyプロパティなのか?」という部分に関しての注意書きがあります。一応「HTML出力として必要になった場合に始めて表示される」という説明を根拠としている部分もありますが、肝になるのはクロージャ内部でselfを参照しているという部分です。プロパティ内部からself経由でアクセスする場合、lazy指定以外方法がないのかもしれませんが、勉強不足なので未だよく分かりません。

“NOTE

The asHTML property is declared as a lazy property, because it is only needed if and when the element actually needs to be rendered as a string value for some HTML output target. The fact that asHTML is a lazy property means that you can refer to self within the default closure, because the lazy property will not be accessed until after initialization has been completed and self is known to exist.”

抜粋:: Apple Inc. “The Swift Programming Language”。 iBooks https://itun.es/jp/jEUH0.l

lazy指定を外すと、

//error: use of unresolved identifier 'self'

という感じで怒られます。

循環参照 | 終了子は実行されない

少し寄り道しましたが、実際にインスタンスを作ってみると、プロパティ自体はちゃんと動いているのが分かります。

var markdown: Markdown? = Markdown(text: "タイトル", tag:"#")
print(markdown!.md())
print(markdown!.html())
//"# タイトル"
//"<h1>タイトル</h1>"

nilを代入して終了子が呼び出されたかどうか確認したいので、markdown変数はMarkdownのオプショナル型で定義しています。

試しにmarkdown変数にnilを代入してみます。

// 終了子は実行されない
markdown = nil
//何も表示されない

実際やってみると分かりますが、nil代入後には何も表示されませんから、終了子は呼び出されてないです。

今、Markdownインスタンスは、mdプロパティ(またはhtmlプロパティ)でクロージャを強参照しています(下図の右矢印)。一方で、クロージャ本体ではMarkdownのインスタンスプロパティ(self.tag等)を「キャプチャ」することで、Markdownインスタンスを強参照しています(下図の左矢印)。

[図解]クロージャの循環参照

図示するとこんな感じで、Markdownクラスインスタンスとクロージャ() -> String間で相互に強く参照していますから、これは循環参照です。したがって、markdown変数からMarkdownインスタンスへの参照を外しても循環参照が残るので、終了子が呼び出されません(Markdownインスタンスが破棄されない)。

クロージャの循環参照を解決する方法

クロージャ(とクラス間)の循環参照を解決するためには、クロージャ定義の際にキャプチャリスト(capture list)というものを同時に定義する必要があります。キャプチャリストを使うことで、クロージャ本体でキャプチャする参照を、弱参照にするか非所有参照にするかを宣言することが出来ます。

キャプチャリストの定義

上記の具体例で使ったクロージャを流用すると、弱参照と非所有参照の場合にそれぞれ、

//弱参照の場合
lazy var md: () -> String = {
    [weak self] in
    ....
}

//非所有参照の場合
lazy var md: () -> String = {
    [unowned self] in
    ....
}

という風に書きます。キャプチャリストの定義は角括弧[]内に書きます。クラス間の循環参照の場合と同様に、弱参照にはweak、非所有参照にはunownedを使います。

もしクロージャにパラメータや戻り値が明記されている場合は、キャプチャリストをその前に置きます。例えば、

lazy var md: () -> String = {
    [unowned self] () -> String in
    ....
}

という感じでしょうか。

また、キャプチャリストで名前付きの値(変数や定数のような感じ)をキャプチャすることも可能です。

// self.textをtextとしてキャプチャ。参照はunowned。
lazy var md: () -> String = {
    [unowned text = self.text] in
    // クロージャ本体で使う時はtextとして使える。
    ....
}

クロージャ本体で何度も同じプロパティを使う場合には便利かもしれません。

弱参照と非所有参照どっちを使う?

使い方はクラス間の循環参照の場合と全く同じです。箇条書きにすると、

  • キャプチャした参照が絶対にnilにならない場合は非所有参照
  • nilになる可能性がある場合は弱参照

です。nilになる場合(弱参照の場合)は必ずオプショナル型で、参照先のインスタンスのメモリ解放と同時に(自動的に)nilになります。

今回の具体例Markdownクラスでは、キャプチャした参照であるself.textself.tagnilになることはありません。したがって、適切な参照は非所有参照になります。

該当部分だけ書き直すと、

// 非所有参照のキャプチャリスト
lazy var md: () -> String = {
    [unowned self] in
    return "\(self.tag) \(self.text)"
}

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

となります。繰り返しになりますが、キャプチャリスト[unowned self]は、

selfプロパティは(強参照ではなく)非所有参照です

と明示していることになります。

最初の実行例を再び試してみると分かりますが、

var markdown: Markdown? = Markdown(text: "タイトル", tag:"#")
print(markdown!.md())
print(markdown!.html())

// 終了子が呼び出される
markdown = nil
//"Markdownクラスを破棄"と表示

markdown変数にnilを代入した後に、ちゃんと終了子が呼び出されていることが分かります。

キャプチャリストで非所有参照を使用したケースを図示すると、

[図解]クロージャの循環参照をキャプチャリストで解消

上図のようになります。このケースでは、クロージャによってキャプチャされたselfが非所有参照(unowned self)になっています。

今、Markdownインスタンスを強く参照しているのはmarkdown変数だけですから、markdownnilを代入(変数を破棄)すると、Markdownインスタンスに対する強参照が1つもなくなります。したがって、その時点でMarkdownインスタンスが破棄されるので、終了子からの出力がちゃんと表示されます。

まとめ

  • クロージャも参照型なので、クラス・クロージャ間での循環参照は起こり得る
  • キャプチャリストでクロージャの循環参照を解決することが出来る
  • キャプチャリストは[weak self]のように角括弧内に定義
  • 名前付きキャプチャリストも可能。例)[weak text = self.text]