Swiftでプロパティと inout
参照を組み合わせた時の挙動が面白かったので、クイズ形式で紹介します。
バージョン: Apple Swift version 3.1 (swiftlang-802.0.48 clang-802.0.38)
問題
次のコードを見てください。 Int
型のプロパティ age
を持つ Cat
の struct
です。
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
メソッドを追加して、 cat
の age
を updateInt
経由で更新してみます。
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の変更に加えて、次のように、 Cat
の age
に didSet
を付けたら、どのような出力が得られるでしょうか。
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 =
の行で、そのcat
のage
プロパティのアドレスを取得しています。 -
%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
の行で、cat
のage
の値をその変数に書き込んでいます。 - そして
%18 =
の行で、確保したInt
と777
でupdateInt
を呼び出しています。cat
のage
プロパティはここでは渡されていません。 -
%19 =
の行で、そのInt
の値を取得しています。 -
%20 =
の行で、Cat
のage
のセッターを取得しています。 -
%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
メソッドの内部からはセッターを呼び出すための情報がありません。この整合性を取るために、いったん中間変数を経由して結果を受け取った後、メソッド呼び出しの直後にプロパティへの代入を行う、という挙動にしていると考えます。