Edit Page

演算子のオーバーロード(Operator overloading)

(訳注:オーバーロードは多重定義などとも訳される。同じシンボルに対して複数の関数の実装が対応する機能)

Kotlinではあらかじめ定義された演算子たちに対し、型ごとにカスタムな実装を提供することが出来ます。 これらの演算子にはシンボルの表現(+とか*)と、演算子の優先順位があらかじめ決まっています。 演算子を実装するには、対応する型のメンバ関数拡張関数で特定の名前のものを用意する必要があります。 これらの型は2項演算子なら左側の型となり、単項演算子ならその対象とする引数の型となります。

演算子をオーバーロードするには、対応する関数にoperator修飾子でマークする必要があります:

interface IndexedContainer {
    operator fun get(index: Int)
}

オーバーロードしたい演算子をオーバーライドするケースでは、operatorは省略することが出来ます:

class OrdersList: IndexedContainer {
    override fun get(index: Int) { /*...*/ }   
}

単項演算子

(訳注:unary operator)

単項の前置演算子

変換結果
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()

このテーブルが示すことは、コンパイラが式を、例えば+aという式を処理すると、以下のステップを実行する、ということだ:

  • aの型を決定する。ここではTと呼ぶことにする
  • operator修飾子がついていて引数が無いunaryPlus()という関数をレシーバTの関数として探す、つまりTのメンバ関数か拡張関数から探す
  • もし関数が存在しないか曖昧だったら、コンパイルエラーになる
  • もし関数が存在してその関数の戻りの型がRだったら、式+aは型Rを持つ

これらの演算子やそのほかの同類のものたちは、基本型については最適化がなされて関数呼び出しのオーバーヘッドは発生しません。

例として、単項演算子をどんな風にオーバーロード出来るかを以下に示す:

data class Point(val x: Int, val y: Int) operator fun Point.unaryMinus() = Point(-x, -y) val point = Point(10, 20) fun main() { println(-point) // "Point(x=-10, y=-20)" と出力 }

インクリメントとデクリメント

変換結果
a++ a.inc() + 以下を参照
a-- a.dec() + 以下を参照

inc()dec()関数は、値を返さなくてはなりません。 この返された値が++--が使われた対象の変数に代入されます。 これらの演算子は対象となるオブジェクトを変更してはいけません。

これらの後置の形式の演算子、例えばa++、にコンパイラが遭遇した時の解決手順は、以下の通りです:

  • aの型を決定する。ここではTとします。
  • レシーバの型Tに適用出来て、operator修飾子がついて、引数が無い関数でinc()という名前のものを探す。
  • 見つけた関数の返す型がTのサブタイプであることを確認する。

この式を計算するとどうなるかと言えば:

  • aの最初の値を一時的な変数、a0に格納する
  • a0.inc()の結果をaに代入する
  • a0を式の評価結果として返す

a--についても、これらのステップは同様です。

前置の形式の場合、つまり++a--aの場合、解決の手順は後置の形式と同様ですが、計算の結果が異なります:

  • a.inc()の結果をaに代入
  • aの新しくなった値をこの式の結果として返す

二項演算子

(訳注:Binary operations)

算術演算子

変換結果
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)
a..b a.rangeTo(b)
a..<b a.rangeUntil(b)

このテーブルにある演算に関しては、コンパイラは式を 変換結果 の列に単に解決するだけです。

以下はCounterクラスが指定された値から始まってオーバーロードされた+演算子でインクリメントされていく例です:

data class Counter(val dayIndex: Int) {
    operator fun plus(increment: Int): Counter {
        return Counter(dayIndex + increment)
    }
}

in演算子

変換結果
a in b b.contains(a)
a !in b !b.contains(a)

in!inもだいたい(訳注:算術演算と)同様ですが、引数の順番は逆になります。

インデックスアクセス演算子

変換結果
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, ..., i_n] a.get(i_1, ..., i_n)
a[i] = b a.set(i, b)
a[i, j] = b a.set(i, j, b)
a[i_1, ..., i_n] = b a.set(i_1, ..., i_n, b)

角括弧(訳注: []のこと)はgetset関数のうち適切な引数のものの呼び出しに変換されます。

invoke演算子

変換結果
a() a.invoke()
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, ..., i_n) a.invoke(i_1, ..., i_n)

カッコは適切な引数のinvokeの呼び出しに変換されます。

拡張代入

(訳注: Augmented assignments)

変換結果
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b)

これらの代入演算子、例えばa += bは、コンパイラは以下の手順を実行する:

  • もし右の列の関数があれば:
    • 対応する二項演算関数(plusAssign()の場合ならplus()がある、という意味)も存在して、aがミュータブルな変数で、plusの戻りの型がaのサブタイプなら、エラーを報告(曖昧)
    • 戻りの型がUnitなのを確認し、違ったらエラーを報告
    • a.plusAssign(b)というコードを生成
  • 上記のケース以外ならa = a + bというコードの生成を試みる(これはa + baのサブタイプであることを確認する型チェックも含む)

Kotlinでは、代入は式ではありません

EqualityとInequality演算子

変換結果
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

これらの演算子はequals(other: Any?): Booleanに対してのみ機能し、 これはカスタムなイコール判定の実装のためにオーバーライド出来ます。 同じ名前のどんな別の関数も呼ばれることはありません (例えば equals(other: Foo)などは呼ばれない)。

===!== (アイデンティティチェック)はオーバーロード出来ないので、この手の変換も存在しません。

==オペレーションは特別です。==nullをスクリーニングする複雑な式に変換されます。 null == nullはいつもtrueで、nullでないxに対してのx == nullはいつもfalseでx.equals()は呼び出されません。

比較演算子

変換結果
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

すべての比較演算子はcompareaToの呼び出しに変換され、compareToIntを返します。

プロパティの委譲演算子

provideDelegategetValuesetValue演算子の関数については、 委譲プロパティに解説があります。

名前あり関数の中置呼び出し

(訳注: infix call)

中置関数呼び出しの仕組みを使うことで、カスタムな中置演算のようなことが出来ます。