Kotlin Multiplatform、Kotlin NativeでたくさんオブジェクトがFreezeした話

Kotlin MultiplatformではKtorというAPIクライアントが現状デファクトスタンダードになっています。
このクライアントをラップしつつ、テストできるようにするには以下のように
`MockEngine` というのを使い、そして `HttpClientEngineConfig`というのを渡します。

commonTest内

```kotlin
@Test
fun config() = runBlocking {
    val myClient = MyClient(MockEngine) { // ← APIの返り値を設定するため、MockEngineを使う
        addHandler {
            respond("test") // ← APIが何を返すかを設定する
        }
    }

    val result = myClient.get()

    assertEquals("test", result) // 返り値を確認
}

class MyClient<T : HttpClientEngineConfig>(
    engine: HttpClientEngineFactory<T>,
    configBlock: T.()
    -> Unit,
) {
    private val headerValue = "fuga"
    private val httpClient = HttpClient(engine) {
        defaultRequest {
            header("hoge", headerValue)
        }
        engine(configBlock)
    }

    suspend fun get(): String {
        return httpClient.get<String>("http://example.com")
    }
}
```

JVMではテストが通りますが、**これだけでiOSでのテストでは、もうExceptionによって動かなくなりました。**
59行目はコンストラクタで入れているだけですが、もう失敗してしまいます。

```
59行目: private val httpClient = HttpClient(engine) {
```

```kotlin
kotlin.native.concurrent.InvalidMutabilityException: mutation attempt of frozen data.ApiTest.MyClient@e4d0b8d8
	at <global>.MutationCheck(Unknown Source)
	atdata.ApiTest.MyClient#<init>(.../data/ApiTest.kt:59)
```

今後のアップデートで治る可能性もあるので、Ktorとcoroutinesのバージョンを記載しておきます。

```
+--- io.ktor:ktor-client-core:1.5.0
|    +--- org.jetbrains.kotlin:kotlin-stdlib-common:1.4.20 -> 1.4.21
|    +--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2-native-mt
```


# InvalidMutabilityException？Freeze？
Kotlin Nativeにはまだ自分も馴染みが深くないですが、Kotlin Nativeで安全に複数スレッドからオブジェクトを扱うために、freezeという仕組みがあります。
以下のようにFreezeしているものを変更しようとするとエラーになります。つまり今回はFreezeしているのでエラーになっていました。
`Any.freeze()`メソッドを呼ぶとFreezeさせることができます。

```kotlin
    @Test
    fun freezeTest() {
        val b = B()
        b.variable = "bbb" // ここでInvalidMutabilityException！
    }

    class B(
        var variable: String = "aaaa",
    ) {
        init {
            freeze()
        }
    }

```

## 参照しているだけでFreezeされるのか？

そんなことはありません。`Any.isFrozen`というのが用意されており、今のFreezeしているのかを状態を確認できます。

```kotlin
    @Test
    fun freezeTest() {
        val a = A(B())
        println("a.isFrozen:" + a.isFrozen)
        println("b.isFrozen:" + b.isFrozen)
    }

    class A(val b: B)
    class B(
        var variable: String = "aaaa",
    ) {
        init {
            freeze()
        }
    }
```

以下のようにbだけがfreezeされています。

```kotlin
a.isFrozen:false
b.isFrozen:true
```

## ではなぜMyClientはFreezeされたか？

もう少し例を寄せてラムダを渡してみます。以下ではaはfreezeされておらず、うまく動いています。

```kotlin
a.isFrozen:false
b.isFrozen:true
```

```kotlin
    @Test
    fun freezeTest() {
        val a = A()
        println("a.isFrozen:" + a.isFrozen)
        println("b.isFrozen:" + a.b.isFrozen)
    }

    class A() {
        val b = B { // ラムダを渡す
            "config"
        }
    }

    class B(
        var configVariable: () -> String,
    ) {
        var variable: String = configVariable()

        init {
            freeze()
        }
    }
```

しかし、Aクラスにあるブロック内で他の変数を参照するとどうなるでしょうか。。？
**これで同じエラーが発生しました。**

```kotlin
    @Test
    fun freezeTest() {
        val a = A()
        println("a.isFrozen:" + a.isFrozen)
        println("b.isFrozen:" + a.b.isFrozen)
    }

    class A() {
        val externalVariable = "external variable"
        val b = B {
            externalVariable // ← ここでAの変数を参照する
        }
    }

    class B(
        var configVariable: () -> String,
    ) {
        var variable: String = configVariable()

        init {
            freeze()
        }
    }
```

```
kotlin.native.concurrent.InvalidMutabilityException: mutation attempt of frozen data.ApiTest.A@74015898
```

# なぜ起きたか？

**このブロックの中で外の変数などを参照することでクラスAのインスタンスへの参照が発生し、それによってAもFreezeされます。参照があると再帰的にフリーズされるので、リスナやラムダなどにより、インスタンスのFreezeが広がっていきます。**

```kotlin
    class A() {
        val externalVariable = "external variable"
        val b = B {
            externalVariable // ← ここでAの変数を参照する
        }
    }
```

# どうすると直るか

ラムダ内から参照しなくなれば治ります。
例えば以下のようにします。

```kotlin
    class A() {
        val externalVariable = "external variable"
        val b :B
        init {
            val sharedExternalVariable = externalVariable
            b = B {
                sharedExternalVariable
            }
        }
    }
```

# これを防ぐには？
Kotlin Nativeには `fun Any.ensureNeverFrozen()` メソッドがあり、これを呼び出すことで、freezeされたときにexceptionを吐くことができるので、これをいろんなクラスのinitで呼び出しまくります。
これによって意図しないfreezeを防ぎます。
ただ、Kotlin/Nativeの関数なので、Multiplatformで呼べないのでTouchlabのStatelyがmultiplatform用のメソッドを用意してくれているのでそれを使っていくことができます
https://github.com/touchlab/Stately

```kotlin
    class A() {
        init {
            ensureNeverFrozen()
```