LoginSignup
355

More than 5 years have passed since last update.

posted at

updated at

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

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

  • 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でやるのが良いのでしょうかね。

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

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
What you can do with signing up
355