LoginSignup
4
3

More than 5 years have passed since last update.

演算子のオーバーロードは無条件でオペランドを評価していること

Last updated at Posted at 2016-04-13

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 が消えてしまいました。

はい、?=? の演算子は、両側のオペランドを無条件で評価してから演算しているのです。なので自分は xnil でなければ 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

演算子に遅延評価するパラメーターが欲しいな…
もしくは上記のメソッドを分かりやすく表現出来る演算子があればな…

4
3
2

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
  3. You can use dark theme
What you can do with signing up
4
3