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