演算子のオーバーロード

クラスや構造体に既存の演算子について独自の振る舞いを与えることができます。これを演算子のオーバーロードと呼びます。

ここでは、分数を表す次のような構造体に演算子を実装することを考えてみます。


/* 分数 */
struct Fraction {
    var numerator: Int      // 分子
    var denominator: Int    // 分母
    // 分数形式の文字列で返す
    var asString: String {
        let hcf = findHCF()
        let str = "\(abs(numerator) / hcf) / \(abs(denominator) / hcf)"
        return numerator * denominator < 0 ? "- " + str : str
    }
    // 最大公約数を求める
    func findHCF() -> Int {
        for var i=max(numerator, denominator); i>=1; i-- {
            if numerator % i == 0 && denominator % i == 0 {
                return i
            }
        }
        return 1
    }
}

let f = Fraction(numerator: 6, denominator: 15)
print(f.asString)     // -> 2 / 5

 

2項演算子

上の構造体に対して、+演算子をオーバーロードしてみます。次の様になります。


/* 分数の足し算 */
func + (left: Fraction, right: Fraction) -> Fraction {
    let numerator = left.numerator * right.denominator + right.numerator * left.denominator
    let denomitor = left.denominator * right.denominator
    return Fraction(numerator: numerator, denominator: denomitor)
}

let f1 = Fraction(numerator: 2, denominator: 3)
let f2 = Fraction(numerator: 4, denominator: 5)
let f3 = f1 + f2
print(f3.asString)    // -> 22 / 15

+演算子は、オペランド(演算対象)を2つ持つ2項演算子です。これをSwiftではinfix(間に置くという意味)と表現します。上の例では演算対象として、演算子の左側をleft、右側をrightというFraction型の引数で受け取り、結果もFraction型で返しています。

演算子はこのように、型のメソッドとしてではなく、グローバル関数として定義します。

 

単項演算子

次は分数の符号を反転させる-演算子を定義します。これは単項演算子になりますが、オペランドの前に演算子がつくのでprefix(前に置くという意味)という修飾子が必要になります。


/* 符号の反転 */
prefix func - (fraction: Fraction) -> Fraction {
    return Fraction(numerator: -fraction.numerator, denominator: fraction.denominator)
}

let f1 = Fraction(numerator: 4, denominator: 10)
print(f1.asString)    // -> 2 / 5
let f2 = -f1
print(f2.asString)    // -> - 2 / 5
let f3 = -f2
print(f3.asString)    // -> 2 / 5

 

複合代入演算子

次は、代入演算に別の演算を組み合わせた複合代入演算です。+=演算子を使って他の値を加算できるようにします。


/* 分数の加算 */
func += (inout left: Fraction, right: Fraction) {
    left = left + right
}

var f1 = Fraction(numerator: 2, denominator: 5)
let f2 = Fraction(numerator: 3, denominator: 4)
f1 += f2
println(f1.asString)    // -> 23 / 20

左辺のオペランドの値は変更されるので、引数にinoutの指定が必要になります。+演算子は既に実装済みなので、+=演算子の実装の中で使用することができます。

この演算子を実際に使用する時には、非演算対象は値が変更されるので、定数ではなく、変数として宣言する必要があります。

ここで実装した++演算子を使ってさらに、値を1加算する++演算子を次のよにう実装できます。


prefix func ++ (inout fraction: Fraction) -> Fraction {
    fraction += Fraction(numerator: 1, denominator: 1)
    return fraction
}

var f = Fraction(numerator: 2, denominator: 3)
++f
println(f.asString)     // -> 5 / 3

このように独自に定義した型に対して様々な演算子をオーバーロードできますが、代入演算子(=)と3項演算子(a ? b : c)のオーバーロードはできません。

 

等価演算子

次は、値が等しいかどうかを判定する==演算子と!=演算子です。独自の型に対してこれらの演算子をSwiftが自動的に生成してくれることはありません。様々な意味を持つ型に対してそれらが等しい、或いは等しくないことを類推することができないためです。


/* 等価判定 */
func == (left: Fraction, right: Fraction) -> Bool {
    return left.asString == right.asString
}
func != (left: Fraction, right: Fraction) -> Bool {
    return !(left == right)
}

let f1 = Fraction(numerator: 2, denominator: 3)
let f2 = Fraction(numerator: 3, denominator: 5)
let f3 = Fraction(numerator: 4, denominator: 6)

println(f1 == f2)   // false
println(f1 == f3)   // true
println(f1 != f2)   // true

ここでは単純に、分数の文字列表現が等しいかどうかで判定しています。また、!=演算子は、==演算の結果を反転させています。

 

独自の演算子

Swiftでは既存の演算子だけでなく新たな演算子を定義することもできます。
ここでは分数の逆数を返す、~~演算子を定義してみます。~~は既存の演算子ではないので、グローバルに使用できるよう に、operatorキーワードを使って 宣言する必要があります。


prefix operator ~~ {}

演算子の種類に応じて、operator の前に、prefix(前に置く), infix(間に置く), postfix(後に置く)の何れかを指定します。ここでは、prefixを指定しています。

実装は次の様になります。


prefix func ~~ (inout fraction: Fraction) -> Fraction {
    fraction = Fraction(numerator: fraction.denominator, denominator: fraction.numerator)
    return fraction
}

var f = Fraction(numerator: 3, denominator: 5)
~~f
println(f.asString)     // -> 5 / 3

 

優先度と結合性

独自のinfix演算子を定義する場合は、その優先度と結合性を指定することができます。

優先度は、オベランドの両側に演算子がある場合に、どちらの演算子の演算を優先するかという指定です。また、結合性はオペランドの両側に優先度が同じ演算子がある場合にどちらを優先するかという指定です。


5 + 2 * 6 / 3

という計算式の場合、2の両側にある「+」と「*」では「*」の方が優先度が高いので、右側の演算が先に行われます。次に6の両側にある「*」と「/」の優先度は同じですが、結合性はどちらも左側への結合になっているので、「2 * 6」が先に計算されます。ここまでで次のようになります。


5 + 12 / 3

「+」と「/」では「/」の方が優先度が高いので、「12 / 3」が先に計算されて、次の様になります。


5 + 4

最後に「5 + 4」が計算され、結果は9になります。

別の例を見てみます


let a = 1000 / 20 / 5

「/」は左側への結合性を持つ演算子です。上の式は以下と同じです。


let a = ((1000 / 20) / 5)

もし「/」が右側への結合性を持っていたとすると次のような演算の順序になり結果が異なることになります。


let a = (1000 / (20 / 5))

Swiftで予め定義されている演算子で右側への結合性をもつ演算子は、代入系の演算子の他は「??」と3項演算子の「? :」だけです。それ以外は左側への結合性か、結合性の無い演算子になります。


var a: Int?
var b: Int?

let c = a ?? b ?? 10    // 10

独自の演算子に対して結合性が指定されない場合、デフォルト値の「無し」が設定されます。また、優先度が指定されない場合、デフォルト値は100です。

次に明示的に結合性と優先度を指定する例を示します。


/* 絶対値を返す演算子 */
infix operator |-| { associativity left precedence 140 }

/* 2つの分数の絶対値を返す */
func |-| (left: Fraction, right: Fraction) -> Fraction {
    let f = left + (-right)
    return Fraction(numerator: abs(f.numerator), denominator: abs(f.denominator))
}

let f1 = Fraction(numerator: -3 , denominator: 4)
let f2 = Fraction(numerator: 2, denominator: 5)
let f3 = f1 |-| f2
println(f3.asString)    // -> 23 / 20

ここで指定している、左側への結合と140という優先度は、加算(+)や減算(-)と同じ設定です。

 

優先度と結合性の一覧を以下に記します。

演算子 意味 結合性 優先度
<< ビット左シフト 無し 160
>> ビット左シフト
* 乗算 150
/ 除算
% 剰余
&* オーバーフロー無視乗算
&/ オーバーフロー無視除算
&% オーバーフロー無視剰余
& ビット単位AND
+ 加算 140
- 減算
&+ オーバーフロー加算
&- オーバーフロー減算
| ビット単位OR
^ ビット単位XOR
..< 半開範囲 無し 135
... 閉範囲
is 型チェック 無し 132
as 型キャスト
< 小なり 無し 130
<= 以下
> 大なり
>= 以上
= 等号
!= 不等号
=== 同一
!== 不同
~= パターンマッチ
&& 論理積 120
|| 論理和 110
?? Nil融合 110
? : 3項演算 100
= 代入 90
*= 乗算代入
/= 除算代入
%= 剰余代入
+= 加算代入
-= 減算代入
<<= ビット左シフト代入
>>= ビット右シフト代入
&= ビット単位AND代入
^= ビット単位XOR代入
|= ビット単位OR代入
&&= 論理積代入
||= 論理積代入

 

prefixやpostfixの演算子を定義する際には優先度を指定しませんが、オペランドにprefixとpostfixの両方の演算子が指定された場合、postfixの方が先に演算されます。