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

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

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

問題

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

struct Cat {
    var age: Int = 333
}

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

class Main {
    var cat: Cat = Cat()

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

これに、 main メソッドを追加して、 catageupdateInt 経由で更新してみます。

class Main {
    ...

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

Main().main()

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

cat.age at end of main = 777

クイズ1

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

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

クイズ2

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

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

答え

クイズ1の答え

下記のようになります。

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

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

クイズ2の答え

下記のようになります。

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 = の行で、その catage プロパティのアドレスを取得しています。
  • %15 = の行で、%14 に入った age プロパティのアドレスと、 %4 に入った 777 を、 %2 に入った updateInt に渡して呼び出しています。
// 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 の行で、 catage の値をその変数に書き込んでいます。
  • そして %18 = の行で、確保した Int777updateInt を呼び出しています。 catage プロパティはここでは渡されていません。
  • %19 = の行で、その Int の値を取得しています。
  • %20 = の行で、 Catage のセッターを取得しています。
  • %21 = の行で、取得した値でそのセッターを呼び出しています。
// 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 メソッドの内部からはセッターを呼び出すための情報がありません。この整合性を取るために、いったん中間変数を経由して結果を受け取った後、メソッド呼び出しの直後にプロパティへの代入を行う、という挙動にしていると考えます。


『 Swift 』Article List