演算子のオーバーロード(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
を持つ
これらの演算子やそのほかの同類のものたちは、基本型については最適化がなされて関数呼び出しのオーバーヘッドは発生しません。
例として、単項演算子をどんな風にオーバーロード出来るかを以下に示す:
インクリメントとデクリメント
式 | 変換結果 |
---|---|
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) |
角括弧(訳注: []
のこと)はget
とset
関数のうち適切な引数のものの呼び出しに変換されます。
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 + b
がa
のサブタイプであることを確認する型チェックも含む)
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
の呼び出しに変換され、compareTo
はInt
を返します。
プロパティの委譲演算子
provideDelegate
、 getValue
、 setValue
演算子の関数については、
委譲プロパティに解説があります。
名前あり関数の中置呼び出し
(訳注: infix call)
中置関数呼び出しの仕組みを使うことで、カスタムな中置演算のようなことが出来ます。