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

More than 3 years have 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でやるのが良いのでしょうかね。

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