Xcode
iOS
Swift

Swiftのプロパティ監視(willSet/didSet)の挙動について調べてみた

More than 1 year has passed since last update.

公式の英文資料を読んでもいまいち理解できなかった(英語が読めなかった)のでいろいろ試してみる事にしました。

  • Xcode6 beta5 + Playground
  • 更新: 2015/03/06, Xcode6.3 beta3, Playground

基本的な使い方

ストアド・プロパティにwillSetとdidSetを仕掛ける事で、プロパティの変更前/後で何か処理を書く事ができます。
(※グローバル変数やローカル変数にも仕掛ける事ができます。)

class Person {
    var age: Int = 0 {
        willSet {
            println("age willSet:\(age) -> \(newValue)")
        }
        didSet {
            println("age didSet :\(oldValue) -> \(age)")
        }
    }
}

let p = Person()
p.age = 20
実行結果
age willSet:0 -> 20
age didSet :0 -> 20

newValue/oldValueにそれぞれ新しい値/古い値が入っています。
これらは定数なので書き換える事はできません。

newValue/oldValueの名前の変更は可能です。

class Person {
    var age: Int = 0 {
        willSet(newAge) {
            println("age willSet:\(age) -> \(newAge)")
        }
        didSet(oldAge) {
            println("age didSet :\(oldAge) -> \(age)")
        }
    }
}

didSet内ではプロパティの書き換えが可能

willSet内ではageの値を書き換えようとすると警告が出ますが(変えても意味が無い)、didSetでは変更しても問題無いようです。
変な値が入ってきたときに修正できますね。

class Person {
    var age: Int = 0 {
        willSet {
            println("age willSet:\(age) -> \(newValue)")
        }
        didSet {
            println("age didSet :\(oldValue) -> \(age)")
            if age < 0 {
                age = 0
            }
        }
    }
}

let p = Person()
p.age = -5
println("p.age=\(p.age)")
実行結果
age willSet:0 -> -5
age didSet :0 -> -5
p.age=0

didSet内でageを書き換えても、willSet/didSetが再び呼ばれることは無いようですね。

init()内でプロパティを初期化(または変更)してもwillSet/didSetは呼ばれない

class Person {
    init() {
        age = 5 // ←これ
        age = 10 // ←これ
    }

    var age: Int = 0 {
        willSet {
            println("age willSet:\(age) -> \(newValue)")
        }
        didSet {
            println("age didSet :\(oldValue) -> \(age)")
        }
    }
}

let p = Person()
p.age = 20
実行結果
age willSet:10 -> 20
age didSet :10 -> 20

init()内から別関数を呼んで、そちらからプロパティを変更する場合はwillSet/didSetは呼ばれる

class Person {
    init() {
        changeAge() // ←これ
    }

    var age: Int = 0 {
        willSet {
            println("age willSet:\(age) -> \(newValue)")
        }
        didSet {
            println("age didSet :\(oldValue) -> \(age)")
        }
    }

    func changeAge() {
        age = 100
    }
}

let p = Person()
実行結果
age willSet:0 -> 100
age didSet :0 -> 100

もちろん外部からchangeAgeを呼んでもwillSet/didSetは呼ばれます。

class Person {
    var age: Int = 0 {
        willSet {
            println("age willSet:\(age) -> \(newValue)")
        }
        didSet {
            println("age didSet :\(oldValue) -> \(age)")
        }
    }

    func changeAge() {
        age = 100
    }
}

let p = Person()
p.changeAge() // ←これ
実行結果
age willSet:0 -> 100
age didSet :0 -> 100

同じ値が突っ込まれても、willSet/didSetは呼ばれる

class Person {
    var age: Int = 0 {
        willSet {
            println("age willSet:\(age) -> \(newValue)")
        }
        didSet {
            println("age didSet :\(oldValue) -> \(age)")
        }
    }
}

let p = Person()
p.age = 0
p.age = 0
実行結果
age willSet:0 -> 0
age didSet :0 -> 0
age willSet:0 -> 0
age didSet :0 -> 0

プロパティをインクリメント/デクリメントしてもwillSet/didSetは呼ばれる

class Person {
    var age: Int = 0 {
        willSet {
            println("age willSet:\(age) -> \(newValue)")
        }
        didSet {
            println("age didSet :\(oldValue) -> \(age)")
        }
    }
}

let p = Person()
p.age++
p.age--
実行結果
age willSet:0 -> 1
age didSet :0 -> 1
age willSet:1 -> 0
age didSet :1 -> 0

構造体のメンバを部分的に書き換えてもwillSet/didSetは呼ばれる

import CoreGraphics

class Person {
    var location: CGPoint = CGPointMake(0, 0) {
        willSet {
            println("location willSet:\(location) -> \(newValue)")
        }
        didSet {
            println("location didSet :\(oldValue) -> \(location)")
        }
    }
}

let p = Person()
p.location.x = 5 // ←これ
p.location.x++ // ←これ
実行結果
location willSet:(0.0,0.0) -> (5.0,0.0)
location didSet :(0.0,0.0) -> (5.0,0.0)
location willSet:(5.0,0.0) -> (6.0,0.0)
location didSet :(5.0,0.0) -> (6.0,0.0)

配列/辞書に変更を加えてもwillSet/didSetは呼ばれる

structだから一緒と言えば一緒なんでしょうけど(^^;

class Test {
    var array: [Int] = [] {
        willSet {
            println("array willSet:\(array) -> \(newValue)")
        }
        didSet {
            println("array didSet :\(oldValue) -> \(array)")
        }
    }
}

let t = Test()
t.array.append(0) //t.array += 0 この構文はbeta5で廃止されたので代わりにappendで。
t.array[0] = 1
t.array += [2, 3, 4]
t.array.append(5)
t.array = []
実行結果
array willSet:[] -> [0]
array didSet :[] -> [0]
array willSet:[0] -> [1]
array didSet :[0] -> [1]
array willSet:[1] -> [1, 2, 3, 4]
array didSet :[1] -> [1, 2, 3, 4]
array willSet:[1, 2, 3, 4] -> [1, 2, 3, 4, 5]
array didSet :[1, 2, 3, 4] -> [1, 2, 3, 4, 5]
array willSet:[1, 2, 3, 4, 5] -> []
array didSet :[1, 2, 3, 4, 5] -> []

どんな書き方で変更を加えても大丈夫そうですね。

配列に格納されている構造体のメンバを部分的に書き換えてもwillSet/didSetは呼ばれる

日本語が難しくなってきましたね。

import CoreGraphics

class Test {
    var array: [CGPoint] = [] {
        willSet {
            println("array willSet:\(array) -> \(newValue)")
        }
        didSet {
            println("array didSet :\(oldValue) -> \(array)")
        }
    }
}

let t = Test()
t.array.append(CGPointMake(0, 0))
t.array[0].x = 1 // ←これ
実行結果
array willSet:[] -> [(0.0,0.0)]
array didSet :[] -> [(0.0,0.0)]
array willSet:[(0.0,0.0)] -> [(1.0,0.0)]
array didSet :[(0.0,0.0)] -> [(1.0,0.0)]

オブジェクト型のプロパティのプロパティを書き換えてもwillSet/didSetは呼ばれない

こちらは予想通り駄目でした。

class Car {
    init(name: String = "") {
        self.name = name
    }

    var name: String
}

class Person {
    var car: Car = Car() {
        willSet {
            println("car willSet:\(car.name) -> \(newValue.name)")
        }
        didSet {
            println("car didSet :\(oldValue.name) -> \(car.name)")
        }
    }
}

let p = Person()
p.car.name = "FAIRLADY Z" // ←これ
実行結果

弱参照(weak var)がnilになるタイミングではwillSet/didSetは呼ばれない

  • 追記: 2015/03/06, Xcode6.3 beta3, Playground

そりゃそうですね。

class Hoge: Printable {
    var description: String { return "Hello!" }
}

class Test {
    weak var hoge: Hoge? {
        willSet {
            println("hoge willSet:\(hoge) -> \(newValue)")
        }
        didSet {
            println("hoge didSet :\(oldValue) -> \(hoge)")
        }
    }
}

let test = Test()

if true { // ダミーブロック
    let strongColor = Hoge()
    test.hoge = strongColor
}

println(test.hoge)
実行結果
hoge willSet:nil -> Optional(Hello!)
hoge didSet :nil -> Optional(Hello!)
nil

overrideした場合、親のwillSet/didSetは暗黙的に呼ばれる

  • 追記: 2015/03/06, Xcode6.3 beta3, Playground

willSetは子→親、didSetは親→子の順番で呼ばれる。

class Parent {
    var age: Int = 0 {
        willSet {
            println("Parent age willSet:\(age) -> \(newValue)")
        }
        didSet {
            println("Parent age didSet :\(oldValue) -> \(age)")
        }
    }
}

class Child: Parent {
    override var age: Int {
        willSet {
            println("Child age willSet:\(age) -> \(newValue)")
        }
        didSet {
            println("Child age didSet :\(oldValue) -> \(age)")
        }
    }
}

let child: Parent = Child()

child.age = 5
実行結果
Child age willSet:0 -> 5
Parent age willSet:0 -> 5
Parent age didSet :0 -> 5
Child age didSet :0 -> 5

感想

使い道はいろいろありそうで、デバッグ時にログを吐いておくようにすれば、いつどこで何が変わったか判りやすそうですね。
まだプライベート変数のようなものは実装されていませんので、整合性を保つ的な処理もdidSetでやるのが良いのでしょうかね。

今回はシングルスレッドで検証しましたが、複数スレッドからいろいろアクセスしたときに、ちゃんと期待した結果になるか(またはなるようにはどうするのか)はまた機会があれば調べたいと思います。