LoginSignup
10

More than 5 years have passed since last update.

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

Posted at

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 メソッドの内部からはセッターを呼び出すための情報がありません。この整合性を取るために、いったん中間変数を経由して結果を受け取った後、メソッド呼び出しの直後にプロパティへの代入を行う、という挙動にしていると考えます。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10