Swiftのプロパティとinout参照を組み合わせたときの挙動が面白い


Swiftでプロパティと `inout` 参照を組み合わせた時の挙動が面白かったので、クイズ形式で紹介します。

バージョン: `Apple Swift version 3.1 (swiftlang-802.0.48 clang-802.0.38)`

# 問題

次のコードを見てください。 `Int` 型のプロパティ `age` を持つ `Cat` の `struct` です。

```swift
struct Cat {
    var age: Int = 333
}
```

次のコードを見てください。先程の `Cat` 型のプロパティと、引数で受けた `Int` 型の `inout` 参照に、引数で受けた値を代入するメソッド、 `updateInt` を持つ `Main` クラスです。

```swift
class Main {
    var cat: Cat = Cat()

    func updateInt(_ v: inout Int, _ x: Int) {
        v = x
    }
}
```

これに、 `main` メソッドを追加して、 `cat` の `age` を `updateInt` 経由で更新してみます。

```swift
class Main {
    ...

    func main() {
        updateInt(&cat.age, 777)
        print("cat.age at end of main = \(cat.age)")
    }
}

Main().main()
```

すると、出力は下記のとおりとなります。予想通りかと思います。

```text
cat.age at end of main = 777
```

## クイズ1

次のように、 `updateInt` の内部で、代入前後に `print` を入れたら、どのような出力が得られるでしょうか。

```swift
    func updateInt(_ v: inout Int, _ x: Int) {
        v = x
        print("cat.age at end of updateInt = \(cat.age)")
    }
```

## クイズ2

クイズ1の変更に加えて、次のように、 `Cat` の `age` に `didSet` を付けたら、どのような出力が得られるでしょうか。

```swift
struct Cat {
    var age: Int = 333 {
        didSet {
            print("cat didSet = \(age)")
        }
    }
}
```

# 答え

## クイズ1の答え

下記のようになります。

```text
cat.age at end of updateInt = 777
cat.age at end of main = 777
```

これは素直な結果だと思います。

## クイズ2の答え

下記のようになります。

```text
cat.age at end of updateInt = 333
cat didSet = 777
cat.age at end of main = 777
```

これは予想外の人も要るのではないでしょうか。クイズ1と違って、 `updateInt` 時点での結果が `333` になっています。さらに、 `didSet` のメッセージが、 `updateInt` のメッセージより **後に** 表示されています。

# 解説

## クイズ1の解説

SILを読むとわかります。下記は、 `main` メソッドの冒頭部です。以下のような挙動になっています。

- `%2 =` の行で、 `updateInt` メソッドのポインタを取得しています。
- `%3 =`, `%4 =` の行で、 定数 `777` を生成しています。
- `%5 =` から `%13 =` までの行で、 `Main` オブジェクトの `cat` プロパティのアドレスを取得しています。そのために `Main` クラスに内部生成されたメソッド `cat.materializeForSet` を呼び出しているため、行数が多いです。
- `%14 =` の行で、その `cat` の `age` プロパティのアドレスを取得しています。
- `%15 =` の行で、`%14` に入った `age` プロパティのアドレスと、 `%4` に入った `777` を、 `%2` に入った `updateInt` に渡して呼び出しています。

```text
// Main.main() -> ()
sil hidden @_TFC1b4Main4mainfT_T_ : $@convention(method) (@guaranteed Main) -> () {
// %0                                             // users: %21, %60, %59, %15, %13, %9, %8, %2, %1
bb0(%0 : $Main):
  debug_value %0 : $Main, let, name "self", argno 1, loc "b.swift":18:10, scope 15 // id: %1
  %2 = class_method %0 : $Main, #Main.updateInt!1 : (Main) -> (inout Int, Int) -> () , $@convention(method) (@inout Int, Int, @guaranteed Main) -> (), loc "b.swift":19:9, scope 16 // user: %15
  %3 = integer_literal $Builtin.Int64, 777, loc "b.swift":19:29, scope 16 // user: %4
  %4 = struct $Int (%3 : $Builtin.Int64), loc "b.swift":19:29, scope 16 // user: %15
  %5 = alloc_stack $Builtin.UnsafeValueBuffer, loc "b.swift":19:19, scope 16 // users: %28, %24, %9
  %6 = alloc_stack $Cat, loc "b.swift":19:19, scope 16 // users: %27, %7
  %7 = address_to_pointer %6 : $*Cat to $Builtin.RawPointer, loc "b.swift":19:19, scope 16 // user: %9
  %8 = class_method %0 : $Main, #Main.cat!materializeForSet.1 : (Main) -> (Builtin.RawPointer, inout Builtin.UnsafeValueBuffer) -> (Builtin.RawPointer, Builtin.RawPointer?) , $@convention(method) (Builtin.RawPointer, @inout Builtin.UnsafeValueBuffer, @guaranteed Main) -> (Builtin.RawPointer, Optional<Builtin.RawPointer>), loc "b.swift":19:19, scope 16 // user: %9
  %9 = apply %8(%7, %5, %0) : $@convention(method) (Builtin.RawPointer, @inout Builtin.UnsafeValueBuffer, @guaranteed Main) -> (Builtin.RawPointer, Optional<Builtin.RawPointer>), loc "b.swift":19:19, scope 16 // users: %11, %10
  %10 = tuple_extract %9 : $(Builtin.RawPointer, Optional<Builtin.RawPointer>), 0, loc "b.swift":19:19, scope 16 // user: %12
  %11 = tuple_extract %9 : $(Builtin.RawPointer, Optional<Builtin.RawPointer>), 1, loc "b.swift":19:19, scope 16 // user: %16
  %12 = pointer_to_address %10 : $Builtin.RawPointer to [strict] $*Cat, loc "b.swift":19:19, scope 16 // user: %13
  %13 = mark_dependence %12 : $*Cat on %0 : $Main, loc "b.swift":19:19, scope 16 // users: %23, %14
  %14 = struct_element_addr %13 : $*Cat, #Cat.age, loc "b.swift":19:19, scope 16 // user: %15
  %15 = apply %2(%14, %4, %0) : $@convention(method) (@inout Int, Int, @guaranteed Main) -> (), loc "b.swift":19:32, scope 16
  ...
```

`updateInt` メソッドには `cat` プロパティの `age` プロパティのアドレスが渡されているので、 `updateInt` で代入した時点で値が更新され、 `print` の時点では `777` になっていたわけです。自然ですね。

## クイズ2の解説

こちらもSILを見ていきます。初めの方は同じ動作です。

- `%2 =` の行で `updateInt` メソッドのポインタを取得しています。
- `%3 =`, `%4 =` の行で、定数 `777` を生成しています。
- `%5 =` から `%13 =` までの行で、 `cat` プロパティのアドレスを取得しています。

ここから違いが出ます。

- `%14 =` の行でスタック上に `Int` 型の領域を確保しています。
- `%15 =`, `%16 =`, その次の `store` の行で、 `cat` の `age` の値をその変数に書き込んでいます。
- そして `%18 =` の行で、確保した `Int` と `777` で `updateInt` を呼び出しています。 `cat` の `age` プロパティはここでは渡されていません。
- `%19 =` の行で、その `Int` の値を取得しています。
- `%20 =` の行で、 `Cat` の `age` のセッターを取得しています。
- `%21 =` の行で、取得した値でそのセッターを呼び出しています。

```text
// Main.main() -> ()
sil hidden @_TFC1b4Main4mainfT_T_ : $@convention(method) (@guaranteed Main) -> () {
// %0                                             // users: %27, %45, %44, %18, %13, %9, %8, %2, %1
bb0(%0 : $Main):
  debug_value %0 : $Main, let, name "self", argno 1, loc "b.swift":17:10, scope 19 // id: %1
  %2 = class_method %0 : $Main, #Main.updateInt!1 : (Main) -> (inout Int, Int) -> () , $@convention(method) (@inout Int, Int, @guaranteed Main) -> (), loc "b.swift":18:9, scope 20 // user: %18
  %3 = integer_literal $Builtin.Int64, 777, loc "b.swift":18:29, scope 20 // user: %4
  %4 = struct $Int (%3 : $Builtin.Int64), loc "b.swift":18:29, scope 20 // user: %18
  %5 = alloc_stack $Builtin.UnsafeValueBuffer, loc "b.swift":18:19, scope 20 // users: %35, %30, %9
  %6 = alloc_stack $Cat, loc "b.swift":18:19, scope 20 // users: %34, %7
  %7 = address_to_pointer %6 : $*Cat to $Builtin.RawPointer, loc "b.swift":18:19, scope 20 // user: %9
  %8 = class_method %0 : $Main, #Main.cat!materializeForSet.1 : (Main) -> (Builtin.RawPointer, inout Builtin.UnsafeValueBuffer) -> (Builtin.RawPointer, Builtin.RawPointer?) , $@convention(method) (Builtin.RawPointer, @inout Builtin.UnsafeValueBuffer, @guaranteed Main) -> (Builtin.RawPointer, Optional<Builtin.RawPointer>), loc "b.swift":18:19, scope 20 // user: %9
  %9 = apply %8(%7, %5, %0) : $@convention(method) (Builtin.RawPointer, @inout Builtin.UnsafeValueBuffer, @guaranteed Main) -> (Builtin.RawPointer, Optional<Builtin.RawPointer>), loc "b.swift":18:19, scope 20 // users: %11, %10
  %10 = tuple_extract %9 : $(Builtin.RawPointer, Optional<Builtin.RawPointer>), 0, loc "b.swift":18:19, scope 20 // user: %12
  %11 = tuple_extract %9 : $(Builtin.RawPointer, Optional<Builtin.RawPointer>), 1, loc "b.swift":18:19, scope 20 // user: %22
  %12 = pointer_to_address %10 : $Builtin.RawPointer to [strict] $*Cat, loc "b.swift":18:19, scope 20 // user: %13
  %13 = mark_dependence %12 : $*Cat on %0 : $Main, loc "b.swift":18:19, scope 20 // users: %15, %29, %21
  %14 = alloc_stack $Int, loc "b.swift":18:19, scope 20 // users: %19, %17, %33, %18
  %15 = load %13 : $*Cat, loc "b.swift":18:19, scope 20 // user: %16
  %16 = struct_extract %15 : $Cat, #Cat.age, loc "b.swift":18:19, scope 20 // user: %17
  store %16 to %14 : $*Int, loc "b.swift":18:19, scope 20 // id: %17
  %18 = apply %2(%14, %4, %0) : $@convention(method) (@inout Int, Int, @guaranteed Main) -> (), loc "b.swift":18:32, scope 20
  %19 = load %14 : $*Int, loc "b.swift":18:19, scope 20 // user: %21
  // function_ref Cat.age.setter
  %20 = function_ref @_TFV1b3Cats3ageSi : $@convention(method) (Int, @inout Cat) -> (), loc "b.swift":18:19, scope 20 // user: %21
  %21 = apply %20(%19, %13) : $@convention(method) (Int, @inout Cat) -> (), loc "b.swift":18:19, scope 20
  ...
```

このように、 `cat.age` を直接渡すのではなく、いったん中間変数を確保してそれを `updateInt` に渡して結果を受け取った後、 `cat.age` にそれを代入するコードになっていました。だから、 `updateInt` の中では `333` のままであり、 `didSet` はその後で呼び出されたのです。

# 考察

このような挙動になっている理由を考察します。 `didSet` が定義された事で `age` プロパティへの代入においてセッターを呼び出す必要が生じました。しかし、 `inout` 参照はまさに値への参照であり、型としては `Int` へのポインタをとるようになっています。そのため、 `updateInt` メソッドの内部からはセッターを呼び出すための情報がありません。この整合性を取るために、いったん中間変数を経由して結果を受け取った後、メソッド呼び出しの直後にプロパティへの代入を行う、という挙動にしていると考えます。