ザクっと理解するKotlin 演算子のオーバーロード
operator overloading(演算子のオーバーロード)について、Kotlinの公式文章をわかりやすくまとめています。
unary operations(単項演算子)やbinary operations(バイナリー演算子、二項演算子)の区切りで説明しています。
unary operations(単項演算子)とは、簡単に言うと自分自身に対する操作で、例えば1から-1への変換(-a)やインクリメント(a++)を指します。
binary operations(バイナリー演算子、二項演算子)とは、自分と対象が必要で、例えば足し算(a + b)や累算代入(a += b)などを指します。
コードも簡単なものを使うようにしていますが、質問等あればコメントにお願いします。
演算子のオーバーロードとは
事前に定義されている演算子(+、- など)をカスタムすることができます。
まずはこちらを見てください。
data class Point(val x: Int, val y: Int)
fun main() {
val point1 = Point(1, 2)
val point2 = Point(3, 4)
val sum = point1 + point2
println(sum) // 結果: エラー
val negative = -point1
println(negative) // 結果: エラー
}
val sum = point1 + point2
でPointデータクラス同士の足し算を、
val negative = -point1
でPointデータクラスの値のマイナスへの変換を行っていますが、通常はエラーになります。
これは自作のPointでの足し算やマイナス変換をする演算子は事前に用意されていないからです。
しかし演算子のオーバーロード(多重定義)をすればこの問題を解決できます。
data class Point(val x: Int, val y: Int)
// plusについては後述しています
operator fun Point.plus(other: Point): Point {
return Point(this.x + other.x, this.y + other.y)
}
// unaryMinusについては後述しています
operator fun Point.unaryMinus(): Point {
return Point(-this.x, -this.y)
}
fun main() {
val point1 = Point(1, 2)
val point2 = Point(3, 4)
val sum = point1 + point2
println(sum) // 結果: Point(x=4, y=6)
val negative = -point1
println(negative) // 結果: Point(x=-1, y=-2)
}
こちらは演算子のオーバーロードをしたコードです。
operator修飾子を関数につけることで演算子のオーバーロードができます。
Point.plus(other: Point)がPoint同士の足し算(point1 + point2)、Point.unaryMinus()がPointの値のマイナスへの変換(-point1)を可能にしています。
次のセクションからは、このような演算子と関数の対応表を紹介しています。
単項演算
単項プレフィックス演算子
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
このテーブルは、例えば+aの場合、コンパイラが下記の順で動くことを意味します。
・aの型を決定します(今回はTとします)
・operator付きでパラメータが無いunaryPlus()関数を探します(メンバー関数もしくは拡張関数)
・関数が存在しないか曖昧な場合はコンパイルエラーになります
・関数が存在し、戻り値の型がRの場合は+aの型はRです
data class Point(val x: Int, val y: Int)
operator fun Point.unaryMinus(): Point {
return Point(-this.x, -this.y)
}
fun main() {
val point = Point(1, 2)
val negative = -point
println(negative) // 結果: Point(x=-1, y=-2)
}
-pointの場合
・pointの型を決定します(Point): 8行目
・operator付きでパラメータの無いunaryMinus()関数を探します: 3行目
・関数があり、曖昧ではないためコンパイルエラーはでません: 3行目
・-pointの戻り値の型はPointです: 3行目
インクリメントとディクリメント(増加と減少)
a++ | a.inc() |
a– | a.dec() |
a++の場合、コンパイラが下記の順で動くことを意味します。
・aの型を決定します(今回はTとします)
・operator付きでパラメータが無いinc()関数を探します
・関数の戻り値の型がTのサブタイプであることを確認します
また、inct()とdec()関数は++や–が使われている変数に代入するための値を返す必要があります。
その際の動きは下記の順になります。
・aの初期値を一時保管場所であるa0に保管します
・a0.inc()の結果をaに代入します
・式の結果としてa0を返します
data class Point(var x: Int, var y: Int)
operator fun Point.inc(): Point {
return Point(this.x++, this.y++)
}
operator fun Point.dec(): Point {
return Point(this.x--, this.y--)
}
fun main() {
var point = Point(1, 2)
println(point) // 結果: Point(x=1, y=2)
var increasedPoint = point++
println(increasedPoint) // 結果: Point(x=2, y=3)
var decreasedPoint = point--
println(decreasedPoint) // 結果: Point(x=0, y=1)
}
++aや–aでも、コンパイラは同じ動きで、値の返し方は次の通りです。
・a.inc()の結果をaに代入します
・式の結果としてaの新しい値を返します
二項演算
算術演算子
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) |
算術演算子では、コンパイラは単純に左の式を右の関数に読み替えるだけです。
data class Point(var x: Int, var y: Int)
operator fun Point.plus(other: Point): Point {
var arg01 = this.x + other.x
var arg02 = this.y + other.y
return Point(arg01, arg02)
}
fun main() {
var point1 = Point(1, 2)
var point2 = Point(5, 8)
println(point1 + point2) // 結果: Point(x=6, y=10)
}
in演算子
a in b | b.contains(a) |
a !in b | !b.contains(a) |
こちらもコンパイラは単純に左の式を右の関数に読み替えるだけです。
data class Point(var x: Int, var y: Int)
operator fun Point.contains(other: Point): Boolean {
if (this.x == other.x && this.y == other.y) {
return true
} else {
return false
}
}
fun main() {
var point1 = Point(1, 2)
var point2 = Point(1, 2)
var point3 = Point(10, 10)
println(point1 in point2) // 結果: true
println(point1 in point3) // 結果: false
}
こちらのコードではin演算子をオーバーライドし、Pointのxとyが等しいならtrue、それ以外ならfalseを返すようにしています。
インデックスアクセス演算子
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に読み替えられます。
累算代入
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 = a + bのコード出力を試みます(a + bはaのサブタイプであるかの型チェックも含まれています)
data class Point(var x: Int, var y: Int)
operator fun Point.plusAssign(other: Point): Unit {
this.x += other.x
this.y += other.y
}
fun main() {
val point1 = Point(1, 2)
val point2 = Point(1, 2)
point1 += point2
println(point1)
// 代入文は式ではないため下記はエラーです
// println(point1 += point2)
}
イコールとノットイコール演算子
a == b | a?.equals(b) ?: (b === null) |
a != b | !(a?.equals(b) ?: (b === null)) |
これらの演算子はequals(other: Any?): Boolean関数のみで動作します。
この関数はカスタムの等価性チェックを提供するためにオーバーライドできます。
同じ名前の他の関数(例: equals(other: Point)など)では動作しないので注意してください。
data class Point(var x: Int, var y: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Point) return false
return x == other.x && y == other.y
}
}
fun main() {
val point1 = Point(1, 2)
val point2 = Point(1, 2)
println(point1 == point2)
}
==演算子は特殊で、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 |
全ての比較演算子はcompareToに変換され、Intを返す必要があります。
data class Point(var x: Int, var y: Int)
operator fun Point.compareTo(other: Point): Int {
return this.x.compareTo(other.x) + this.y.compareTo(other.y)
}
fun main() {
val point1 = Point(1, 2)
val point2 = Point(1, 2)
println(point1 > point2) // 結果: false
}
point1とpoint2のxとyの値を比べ、point1のどちらかが大きければtrueを返すようにしています。