まくまくKotlinノート
演算子をオーバーロードする (operator)
2019-07-16

Kotlin では、++= などの演算子をオーバーロードして、独自の振る舞いを定義することができます(Java では演算子のオーバーロードはできません)。

ここでは、様々な演算子のオーバーロードの例を示すために、下記のような複素数を保持する簡単なデータクラスを使用することにします。

data class Complex(val re: Int, val im: Int)

二項演算子のオーバーロード (Binary arithmetic operators)

Kotlin で a + b のような演算を可能にする二項演算子を定義するには、plusminus などの 演算子関数 を実装します。

使用する演算子 定義する演算子関数
+ plus
- minus
* times
/ div
% mod

下記は、+ 演算子をオーバーロードする例です。 演算子関数を定義するときは、モディファイアとして operator を付けることに注意してください(同名の通常関数の定義と区別するためです)。

data class Complex(val re: Int, val im: Int) {
    operator fun plus(other: Complex): Complex {
        return Complex(re + other.re, im + other.im)
    }
}

これにより、独自オブジェクト同士の + 演算子による演算が可能になります。

val c1 = Complex(1, 2)
val c2 = Complex(3, 4)
println(c1 + c2)  //=> Complex(re=4, im=6)

上記では、メンバ関数として演算子関数を定義しましたが、拡張関数として定義することもできます。 この方法であれば、既存の外部ライブラリが提供するクラスの演算子をオーバーロードすることが可能です。

operator fun Complex.plus(other: Complex): Complex {
    return Complex(re + other.re, im + other.im)
}

ここまでは、Complex インスタンス同士の演算を定義していましたが、別の型との演算を定義することもできます。 次の例では、各プロパティを、指定した Int 値倍に変化させる * 演算子 (times) を定義しています。

data class Complex(val re: Int, val im: Int) {
    operator fun times(scale: Int): Complex {
        return Complex(re * scale, im * scale)
    }
}

fun main() {
    val c1 = Complex(1, 2)
    val c2 = c1 * 10
    println(c2)  // => Complex(re=10, im=20)
}

このように違う型同士の演算を定義した場合、演算子の左右の値は交換可能ではない ことに注意してください。 例えば、10 * Complex(1, 2) という演算を行いたいのであれば、Int.times(c: Complex) という演算子関数を定義しておく必要があります。

二項演算子の戻り値を、元のオブジェクトの型とは別のものにすることもできます。

operator fun Char.times(n: Int) = toString().repeat(n)

fun main() {
    println('A' * 3)  //=> "AAA"
}

複合代入演算子のオーバーロード (Compound assignment operators)

Kotlin では、二項演算子の + (plus) を定義すると、自動的に複合代入演算子である += も使用できるようになります。 ただし、この場合は変数の値を変更できるようにするために、val ではなく var で変数定義しておく必要があります。

var c = Complex(1, 2)
c += Complex(3, 4)
println(c)  // => Complex(re=4, im=6)

一方、コレクション系クラスやビルダ系クラスでは、要素の追加を行うための += 演算子のみを定義したいことがあります。 このようなケースでは、plusAssignminusAssign という名前の演算子関数を定義します。

使用する演算子 定義する演算子関数
+= plusAssign
-= minusAssign
*= timesAssign
/= divAssign
%= remAssign

下記は、MutableCollection による拡張関数定義の抜粋です。

public inline operator fun <T> MutableCollection<in T>.plusAssign(element: T) {
    this.add(element)
}

public inline operator fun <T> MutableCollection<in T>.minusAssign(element: T) {
    this.remove(element)
}

つまり、a += b という記述は、a = a.plus(b) あるいは a = a.plusAssign(b) という呼び出しのどちらかにマッピングされることになります。 plusplusAssign の両方が定義されていると、a += b という呼び出しはコンパイルエラーになります。 もちろん、a = a.plus(b) のように関数名を明示して呼び出せばエラーにはなりませんが、通常は plusplusAssign を同時に定義しないようにすべきでしょう。

単項演算子のオーバーロード (Unary operators)

下記のような単項演算子をオーバーロードすることもできます。

使用する演算子 定義する演算子関数
+x unaryPlus
-x unaryMinus
!x not
++x, x++ inc
--x, x-- dec

次の例では、各プロパティの符号を反転する - 演算子を定義しています。

data class Complex(val re: Int, val im: Int) {
    operator fun unaryMinus() = Complex(-re, -im)
}

fun main() {
    val c1 = Complex(1, 2)
    val c2 = -c1
    println(c2)  // => Complex(re=-1, im=-2)
}

前置インクリメント (++x)、後置インクリメント (x++) を使用するには、inc 演算子関数を 1 つ定義するだけで OK です。 デクリメントに関しても同様です。

data class Complex(val re: Int, val im: Int) {
    operator fun inc() = Complex(re + 1, im + 1)
}

fun main() {
    var c = Complex(1, 2)
    println(c++) // => Complex(re=1, im=2)
    println(++c) // => Complex(re=3, im=4)
}

比較演算子のオーバーロード (Comparison operators)

==!=><>=<= などの比較演算子を使用したい場合は、これまでの例とは異なり、Java の頃から存在する equals メソッドや、compareTo メソッドを定義することで使用できるようになります。

同値の比較(==、!=)

すべてのオブジェクトに存在する equals メソッドをオーバーライドしておくと、== 演算子と != 演算子を使用できるようになります。

class Complex(val re: Int, val im: Int) {
    override fun equals(other: Any?): Boolean {
        if (other === this) return true
        if (other !is Complex) return false
        return other.re == re && other.im == im
    }
}

fun main() {
    val c1 = Complex(1, 2)
    val c2 = Complex(1, 2)
    println(c1 == c2) // => true
    println(c1 != c2) // => false
}

ここでは、参考のために equals メソッドの実装例を示しましたが、Kotlin では data プレフィックスを付けてデータクラスを定義すると、自動的に equals メソッドも実装されるため、下記のように equals メソッドの定義を省略できます。

data class Complex(val re: Int, val im: Int)

大小の比較(>、<、≧、≦)

同様に、Comparable インタフェース(の compareTo メソッド)を実装しておくと、オブジェクト同士を >< 演算子で比較できるようになります。

※複素数同士の大小比較はナンセンスですが、ここでは、実数部→虚数部の順で比較することにします。

data class Pos(val x: Int, val y: Int) : Comparable<Pos> {
    override fun compareTo(other: Pos): Int {
        return if (x == other.x) {
            y - other.y
        } else {
            x - other.x
        }
    }
}

fun main() {
    val c1 = Complex(1, 2)
    val c2 = Complex(2, 1)
    val c3 = Complex(1, 3)
    println(c1 < c2) // => true
    println(c1 > c2) // => false
    println(c1 <= c3) // => true
}

ちなみに、上記の比較関数では reim プロパティの順に値を比較していますが、このような定型処理は下記のように、Kotlin の compareValuesBy 関数を使って簡単に記述することができます。

override fun compareTo(other: Complex): Int {
    return compareValuesBy(this, other, Complex::re, Complex::im)
}

Kotlin の Char クラスや String クラスも Comparable インタフェースを実装しているので、下記のような大小比較が可能になっています。

println('A' < 'B') // => true
println("AAA" < "BBB") // => true
2019-07-16