栃木県のJavaエンジニア、WEBプログラマーのためのサイト

2014.12.22[Swift] 演算子オーバーロード

NSDecimalNumber

Double などの浮動小数点型は2進数の有限小数として内部保持されるため、値によっては近似値となり誤差が発生します。例えば Playground で試してみると、以下の例では 0.5 とならず、4.99999999999945 になっています。

"Double"

なので、誤差が発生すると困る場合は NSDecimalNumber などを用います。先程のコードを NSDecimalNumber で書き直してみると、今度は 0.5 になりました。

"NSDecimalNumber"

ただ、NSDecimalNumber だと通常のメソッド呼び出しで書かないとダメなのでとっても面倒。

演算子オーバーロード

そこで、演算子をオーバーロードして、ーと*で式を書けるようにしてみます。

"overload"

これですっきりしましたね。
他の演算子もオーバーロードしてみましょう。

四則演算子

func + (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> NSDecimalNumber {
    return lhs.decimalNumberByAdding(rhs)
}

func - (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> NSDecimalNumber {
    return lhs.decimalNumberBySubtracting(rhs)
}

func * (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> NSDecimalNumber {
    return lhs.decimalNumberByMultiplyingBy(rhs)
}

func / (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> NSDecimalNumber {
    return lhs.decimalNumberByDividingBy(rhs)
}

代入演算子

func += (inout lhs: NSDecimalNumber, rhs: NSDecimalNumber) {
    lhs = lhs.decimalNumberByAdding(rhs)
}

func -= (inout lhs: NSDecimalNumber, rhs: NSDecimalNumber) {
    lhs = lhs.decimalNumberBySubtracting(rhs)
}

func *= (inout lhs: NSDecimalNumber, rhs: NSDecimalNumber) {
    lhs = lhs.decimalNumberByMultiplyingBy(rhs)
}

func /= (inout lhs: NSDecimalNumber, rhs: NSDecimalNumber) {
    lhs = lhs.decimalNumberByDividingBy(rhs)
}

代入演算子の場合は lhs を書き換えるので、引数名の前に inout を付けます。

比較演算子

func == (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> Bool {
    return lhs.isEqualToNumber(rhs)
}

func != (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> Bool {
    return !lhs.isEqualToNumber(rhs)
}

func > (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> Bool {
    return lhs.compare(rhs) == NSComparisonResult.OrderedDescending
}

func < (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> Bool {
    return lhs.compare(rhs) == NSComparisonResult.OrderedAscending
}

func >= (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> Bool {
    let result = lhs.compare(rhs)
    return result == NSComparisonResult.OrderedDescending || result == NSComparisonResult.OrderedSame
}

func <= (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> Bool {
    let result = lhs.compare(rhs)
    return result == NSComparisonResult.OrderedAscending || result == NSComparisonResult.OrderedSame
}

比較演算子の場合は、Bool を返します。

アクセス制御

演算子のオーバーロードはグローバルスコープに書く必要があるためクラス内に定義出来ません。

class Foo {
    func + (lhs: NSDecimalNumber, rhs: NSDecimalNumber) -> NSDecimalNumber { // ERROR
        return lhs.decimalNumberByAdding(rhs)
    }
}

アクセス制限したい場合は、private を付けると同じソースコードファイル内からのみとなります。

private func += (inout a: NSDecimalNumber, b: NSDecimalNumber) {
    a = a.decimalNumberByAdding(b)
}

Double に変換

let a = NSDecimalNumber(double: 1.5)
a.doubleValue // => 1.5

doubleValue プロパティから取り出せます。

まとめ

  • 浮動小数点の演算は誤差が発生する。
  • 演算子のオーバーロードはグローバルスコープに書く。
  • アクセス制限したい場合は private を付ける。
  • 引数の値を書き換える場合は inout を付ける。

2015.04.13 追記

double / float から NSDecimalNumber を生成すると、double / float のインスタンスが生成された時点で精度が落ちてしまうので文字列から作りましょう。

let a = NSDecimalNumber(double: 1.5) // NG 
let b = NSDecimalNumber(string: "1.5") // OK

また、NSDecimalNumber から double へ変換してしまうと精度が落ちるので文字列を使いましょう。

let dn = NSDecimalNumber(string: "1.5")
dn.doubleValue // NG
dn.stringValue // OK