公式の英文資料を読んでもいまいち理解できなかった(英語が読めなかった)のでいろいろ試してみる事にしました。
- 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でやるのが良いのでしょうかね。
今回はシングルスレッドで検証しましたが、複数スレッドからいろいろアクセスしたときに、ちゃんと期待した結果になるか(またはなるようにはどうするのか)はまた機会があれば調べたいと思います。