Swift の演算子オーバーロードって非常に便利な機能の一つです。おかげでいろんな演算を簡単に定義できてしまいます。例えば下記のようなこと:
var x = 1
var y: Int? = 2
if let y = y {
x = y
}
という、オプショナル変数 y
がもし nil
でなければその値を x
に代入せよ、という操作、いちいち if let
をやるより、演算子を一つ定義してやったほうが便利だったりします:
// 演算子定義
infix operator =? {
associativity right
precedence 90
assignment
}
func =? <T> (inout lhs: T, rhs: T?) {
if let rhs = rhs {
lhs = rhs
}
}
// 利用
x =? y
とこのように、あれだけ面倒な if let
が =?
一つで解決してしまいました。
ところが今日気づいたことだけど、演算子って実は無条件でオペランドを評価しているようです。まあ大体の場合これは何の問題もないけど、ところがしかし、mutating
動作がオペランドに入ってると思わぬ罠が潜んでいます。
例えば下記のようなことをやりたい:
var x: Int? = 0
var queue: [Int] = [1, 2, 3, 4]
func retrieveFirst() -> Int? {
if let first = queue.first {
queue.removeFirst()
return first
} else {
return nil
}
}
if x == nil {
x = retrieveFirst()
}
という、自前のキューを作っていわゆる FIFO の動作を実装したい時、まず自分自身が nil
であるかどうかを判定して、nil
であればキューに項目があるかどうかを判定し、あれば先頭項目を持ってきてその先頭項目をキューから消す、という動作。これはこのコードのままだったら特に何の問題もないけど、さっきの if let
の例と同じように、いちいち if x == nil
書くの面倒だから演算子定義したい、てなるとこのままではちょっと大変なことになります。例を見てみましょう:
// 注:このコードは間違った動作です
infix operator ?=? {
associativity right
precedence 130
assignment
}
func ?=? <T> (inout lhs: T?, rhs: T?) {
if lhs == nil {
lhs = rhs
}
}
var x: Int? = 0
var queue: [Int] = [1, 2, 3, 4]
func retrieveFirst() -> Int? {
if let first = queue.first {
queue.removeFirst()
return first
} else {
return nil
}
}
x ?=? retrieveFirst()
このコードを実行してみるとどうなると想像されますか?まず x
は ?=?
で代入しようとしても自分自身は nil
でないから特に何の値も代入されず 0
のままです。これは問題ありません。ところが queue
を見てみると、何と [2, 3, 4]
になってしまったのです。先頭の 1
が消えてしまいました。
はい、?=?
の演算子は、両側のオペランドを無条件で評価してから演算しているのです。なので自分は x
が nil
でなければ retrieveFirst()
を実行しないつもりで演算子作っても、それが無条件で実行されてしまうからキューの先頭項目が無条件で一つなくなるのです。
というわけで、こう言った遅延評価?が望ましい場合は、演算子オーバーロードではなく、普通に関数かメソッドで作ったほうが無難っぽいです(個人的には inout
が入る関数があまり好きではありませんが…)
extension Array {
mutating func retrieveFirstItemTo (inout target: Element?) {
guard target == nil else {
return
}
if let first = self.first {
self.removeFirst()
target = first
} else {
target = nil
}
}
}
func retrieveFirst <T> (inout from queue: Array<T>, inout to target: T?) {
guard target == nil else {
return
}
if let first = queue.first {
queue.removeFirst()
target = first
} else {
target = nil
}
}
var x: Int? = 0
var queue: [Int] = [1, 2, 3, 4]
// どっちも結果は同じ
queue.retrieveFirstItemTo(&x)
x
queue
retrieveFirst(from: &queue, to: &x)
x
queue
演算子に遅延評価するパラメーターが欲しいな…
もしくは上記のメソッドを分かりやすく表現出来る演算子があればな…