[Swift] シングルトンのdeinitはいつ呼ばれるの？

# はじめに

[シングルトンというデザインパターン](https://ja.wikipedia.org/wiki/Singleton_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3)があって、その是非はともかく、Swiftでも用いられることがありますよね。
仮に、シングルトンパターンを採用したクラスに`deinit`を実装したとします。その時にシングルトンの`deinit`がいつ呼ばれるのか気になりませんか？私は気になりました。

結論から書くと、「普通にしてたら呼ばれない」です。逆に言えば「工夫さえすれば呼ぶことはできる」といったところです。

シングルトンの`deinit`を呼びたい場面がどの程度あるのかわかりませんが、この記事ではそこのところを確認していきます。


# シングルトンの実装

Swiftにおいてシングルトンの実装自体は難しいものではありません：

```Swift:library.swift
public class MyClass {
  private init() {}
  public static let singleton = MyClass()
}
```

これだけです。イニシャライザを`private`にして、`public`なstatic定数として`singleton`を用意すればシングルトンパターンの完成です。

ただ、今回は`deinit`を実装したいので、次のようにしてみます：

```Swift:library.swift
public class MyClass {
  private init() { print("MyClassのinitが呼ばれたよ。") }
  public static let singleton = MyClass()
  deinit { print("MyClassのdeinitが呼ばれたよ。") }
}
```

こうすれば`deinit`が呼ばれるタイミングがわかるようになります(ついでに`init`が呼ばれるタイミングも分かりやすいようにしておきました)。
この記事では、`MyClass`を実装したライブラリを別のコードから呼び出す想定で考えてみます。


## ライブラリの作成

では、この"library.swift"を"MyLibrary"というモジュール名でコンパイルしてみましょう：

```console:console
$ swiftc -emit-library -emit-module -module-name MyLibrary library.swift
$ ls
MyLibrary.swiftdoc	MyLibrary.swiftmodule	libMyLibrary.dylib	library.swift
```


## ライブラリの利用

次のような"main.swift"を用意します：

```Swift:main.swift
import MyLibrary

print("START")
let _ = MyClass.singleton
print("END")
```

あとは、コンパイルして実行すれば…

```console:console
$ swiftc -I. -L. -lMyLibrary main.swift && ./main
START
MyClassのinitが呼ばれたよ。
END
```

やっぱり`deinit`は呼ばれません。
当然と言えば当然なのですが、シングルトンのオブジェクトは`MyClass.singleton`に保持されたままなので、いわゆるretain countが0にならないために`deinit`が呼ばれないのです。
たとえば、`singleton`を`public static var singleton: MyClass? = MyClass()`とオプショナルな変数として宣言して、プログラムの最後に`MyClass.singleton = nil`とすれば`deinit`は呼ばれます。でも、どこからでも`nil`を代入できるようじゃ、安全性が担保できませんね…。
どうしましょう？


# シングルトンの変数へ`nil`を安全かつ自動的に代入したい

となると、まず、
オプショナルな変数は`private`にして、シングルトンを得るgetterだけ`public`にすることを考えましょう:

```Swift:library.swift
public class MyClass {
  private init() { print("MyClassのinitが呼ばれたよ。") }
  deinit { print("MyClassのdeinitが呼ばれたよ。") }

  private static var _singleton: MyClass? = nil

  public static var singleton: MyClass {
    if _singleton == nil {
      _singleton = MyClass()
    }
    return _singleton!
  }
}
```

ここまでくれば、シングルトンが要らなくなった時に`_singleton`に`nil`を代入すればOKです。

…でも、要らなくなった時っていつ？そして、どうやって？

まずシングルトンが要らなくなるのは一般的にはプログラムの終了ではないでしょうか。となるとプログラムの終了時に`_singleton`に`nil`を代入するよう設定できれば良さそうです。プログラムの終了時に実行となると…C言語の標準ライブラリに`int atexit(void (*function)(void))`という便利な関数がありますね！
さて、`atexit()`に「`_singleton`を`nil`にするクロージャ」を渡してみると…：

```Swift:library.swift
// C言語の標準ライブラリを使えるようにする
#if os(Linux)
import Glibc
#else
import Darwin
#endif


public class MyClass {
  private init() { print("MyClassのinitが呼ばれたよ。") }
  deinit { print("MyClassのdeinitが呼ばれたよ。") }

  private static var _singleton: MyClass? = nil

  public static var singleton: MyClass {
    if _singleton == nil {
      _singleton = MyClass()
      atexit({ _singleton = nil }) // ⛔️ a C function pointer cannot be formed from a closure that captures context
    }
    return _singleton!
  }
}
```

ダメでした…。変数をキャプチャするようなクロージャを渡すことはできないそうです。
という訳で、やむを得ないので、関数を渡すようにします：

```Swift:library.swift
#if os(Linux)
import Glibc
#else
import Darwin
#endif

private func _releaseSingleton() {
  MyClass._singleton = nil
}

public class MyClass {
  private init() { print("MyClassのinitが呼ばれたよ。") }
  deinit { print("MyClassのdeinitが呼ばれたよ。") }
  
  fileprivate static var _singleton: MyClass? = nil
  
  public static var singleton: MyClass {
    if _singleton == nil {
      _singleton = MyClass()
      atexit(_releaseSingleton)
    }
    return _singleton!
  }
}
```

これでいけそうです。
では実行結果を試してみましょう：

```console:console
$ swiftc -emit-library -emit-module -module-name MyLibrary library.swift
$ swiftc -I. -L. -lMyLibrary main.swift && ./main
START
MyClassのinitが呼ばれたよ。
END
MyClassのdeinitが呼ばれたよ。
```

できました！
シングルトンの`deinit`がちゃんと呼ばれました。


# おわりに

以上のように、シングルトンの`deinit`が呼ばれるよう設計することはできました。
ただ、"main.swift"の`let _ = MyClass.singleton`を`let s = MyClass.singleton`とすると途端に`deinit`が呼ばれなくなります。`s`がシングルトンを保持したままになるのでretain countが0にならないんですね。プログラムが終わればメモリはシステムが全て回収するのでメモリリークにはならないはずですが、ライブラリを使用する側の実装によっては結局`deinit`が呼ばれなくなってしまう可能性があることは注意が必要そうです。


## 使用例
一時ファイルを操作するためのライブラリである[SwiftTemporaryFile](https://github.com/YOCKOW/SwiftTemporaryFile)における[`TemporaryDirectory`](https://github.com/YOCKOW/SwiftTemporaryFile/blob/3a9b0d1290624a1299a37443da0ec25f520f129b/Sources/TemporaryFile/TemporaryDirectory.swift#L122-L137)でデフォルトの一時ディレクトリをプログラム終了時に削除するために使用してみました。