Kotlinのオペレータオーバーロード

Kotlinでのオペレーターのオーバーロード

1. 概要

このチュートリアルでは、Kotlinが演算子のオーバーロードをサポートするために提供する規則について説明します。

2. operatorキーワード

Javaでは、演算子は特定のJavaタイプに関連付けられています。 たとえば、JavaのStringと数値型は、それぞれ連結と加算に+演算子を使用できます。 他のJavaタイプは、独自の利益のためにこの演算子を再利用できません。 それどころか、Kotlinは、制限されたOperator Overloadingをサポートするための一連の規則を提供します。

単純なdata classから始めましょう:

data class Point(val x: Int, val y: Int)

このデータクラスをいくつかの演算子で拡張します。

事前定義された名前のKotlin関数を演算子we should mark the function with the operator modifier.に変換するために、たとえば、“+”演算子をオーバーロードできます。

operator fun Point.plus(other: Point) = Point(x + other.x, y + other.y)

このようにして、2つのPoints“+”を追加できます。

>> val p1 = Point(0, 1)
>> val p2 = Point(1, 2)
>> println(p1 + p2)
Point(x=1, y=3)

3. 単項演算のオーバーロード

Unary operations are those that work on just one operand。 たとえば、-a, a++または!aは単項演算です。 一般に、単項演算子をオーバーロードする関数はパラメーターを取りません。

3.1. 単項プラス

いくつかのPointsを使用して、ある種のShapeを作成するのはどうですか。

val s = shape {
    +Point(0, 0)
    +Point(1, 1)
    +Point(2, 2)
    +Point(3, 4)
}

Kotlinでは、これはunaryPlusの演算子関数で完全に可能です。

ShapePointsの単なるコレクションであるため、クラスを記述して、いくつかのPointsをラップしてさらに追加することができます。

class Shape {
    private val points = mutableListOf()

    operator fun Point.unaryPlus() {
        points.add(this)
    }
}

また、shape \{…}構文が得られたのは、LambdaReceiversを使用することでした。

fun shape(init: Shape.() -> Unit): Shape {
    val shape = Shape()
    shape.init()

    return shape
}

3.2. 単項マイナス

“p”という名前のPointがあり、“-p”のようなものを使用してその調整を無効にするとします。 次に、Point:unaryMinusという名前の演算子関数を定義するだけです。

operator fun Point.unaryMinus() = Point(-x, -y)

次に、Pointのインスタンスの前に“-“プレフィックスを追加するたびに、コンパイラはそれをunaryMinus関数呼び出しに変換します。

>> val p = Point(4, 2)
>> println(-p)
Point(x=-4, y=-2)

3.3. インクリメント

incという名前の演算子関数を実装するだけで、各座標を1つインクリメントできます。

operator fun Point.inc() = Point(x + 1, y + 1)

接尾辞“++”演算子は、最初に現在の値を返し、次に値を1つ増やします。

>> var p = Point(4, 2)
>> println(p++)
>> println(p)
Point(x=4, y=2)
Point(x=5, y=3)

逆に、プレフィックス“++”演算子は、最初に値を増やしてから、新しくインクリメントされた値を返します。

>> println(++p)
Point(x=6, y=4)

また、since the “++” operator re-assigns the applied variable, we can’t use val with them.

3.4. デクリメント

インクリメントと非常によく似ていますが、dec演算子関数を実装することで、各座標をデクリメントできます。

operator fun Point.dec() = Point(x - 1, y - 1)

decは、通常の数値型と同様に、デクリメント前およびデクリメント後の演算子でおなじみのセマンティクスもサポートします。

>> var p = Point(4, 2)
>> println(p--)
>> println(p)
>> println(--p)
Point(x=4, y=2)
Point(x=3, y=1)
Point(x=2, y=0)

また、++ のように、vals _._と一緒に使用することはできません。

3.5. Not

座標を!pだけ反転させるのはどうですか? We can do this with not:

operator fun Point.not() = Point(y, x)

簡単に言うと、コンパイラは“!p”“not”の単項演算子関数への関数呼び出しに変換します。

>> val p = Point(4, 2)
>> println(!p)
Point(x=2, y=4)

4. 二項演算のオーバーロード

Binary operators, as their name suggests, are those that work on two operands。 したがって、二項演算子をオーバーロードする関数は、少なくとも1つの引数を受け入れる必要があります。

算術演算子から始めましょう。

4.1. プラス算術演算子

前に見たように、Kotlinで基本的な数学演算子をオーバーロードできます。 “+”を使用して、2つのPointsを足し合わせることができます。

operator fun Point.plus(other: Point): Point = Point(x + other.x, y + other.y)

次に、次のように記述できます。

>> val p1 = Point(1, 2)
>> val p2 = Point(2, 3)
>> println(p1 + p2)
Point(x=3, y=5)

plusは二項演算子関数であるため、関数のパラメーターを宣言する必要があります。

今、私たちのほとんどは、2つのBigIntegersを足し合わせるという優雅さを経験しています。

BigInteger zero = BigInteger.ZERO;
BigInteger one = BigInteger.ONE;
one = one.add(zero);

As it turns out、Kotlinに2つのBigIntegersを追加するより良い方法があります:

>> val one = BigInteger.ONE
println(one + one)

Kotlin standard library itself adds its fair share of extension operators on built-in types like BigInteger.が機能しているため、これは機能しています

4.2. その他の算術演算子

plusと同様に、subtraction, multiplicationdivision, and the remainder are working the same way:

operator fun Point.minus(other: Point): Point = Point(x - other.x, y - other.y)
operator fun Point.times(other: Point): Point = Point(x * other.x, y * other.y)
operator fun Point.div(other: Point): Point = Point(x / other.x, y / other.y)
operator fun Point.rem(other: Point): Point = Point(x % other.x, y % other.y)

次に、Kotlinコンパイラは、すべての呼び出しを“-““*”“/”, or “%”から“minus”“times”“div”, or “rem”にそれぞれ変換します。

>> val p1 = Point(2, 4)
>> val p2 = Point(1, 4)
>> println(p1 - p2)
>> println(p1 * p2)
>> println(p1 / p2)
Point(x=1, y=0)
Point(x=2, y=16)
Point(x=2, y=1)

または、Pointを数値係数でスケーリングするのはどうですか。

operator fun Point.times(factor: Int): Point = Point(x * factor, y * factor)

このようにして、“p1 * 2”のようなものを書くことができます。

>> val p1 = Point(1, 2)
>> println(p1 * 2)
Point(x=2, y=4)

前の例からわかるように、2つのオペランドが同じ型である必要はありません。 The same is true for return types.

4.3. 可換性

オーバーロードされた演算子は常にcommutative.、つまりwe can’t swap the operands and expect things to work as smooth as possibleであるとは限りません。

たとえば、PointInt、たとえば“p1 * 2”に乗算することで、積分係数でスケーリングできますが、その逆はできません。

良いニュースは、KotlinまたはJava組み込み型で演算子関数を定義できることです。 “2 * p1”を機能させるために、Intに演算子を定義できます。

operator fun Int.times(point: Point): Point = Point(point.x * this, point.y * this)

これで、“2 * p1”も問題なく使用できるようになりました。

>> val p1 = Point(1, 2)
>> println(2 * p1)
Point(x=2, y=4)

4.4. 化合物割り当て

これで、“+=”.である“”_ operator, we may be able to use the compound assignment for _“”に2つのBigIntegersを追加できるようになりました。このアイデアを試してみましょう:

var one = BigInteger.ONE
one += one

デフォルトでは、算術演算子の1つ(“plus”など)を実装すると、Kotlinは使い慣れた_“” _演算子をサポートするだけでなく、*対応する_compound割り当て_、つまり“ =”に対しても同じことを行います。 **

これは、これ以上作業することなく、次のこともできることを意味します。

var point = Point(0, 0)
point += Point(2, 2)
point -= Point(1, 1)
point *= Point(2, 2)
point /= Point(1, 1)
point /= Point(2, 2)
point *= 2

ただし、このデフォルトの動作が私たちが探しているものではない場合があります。 “+=”を使用して要素をMutableCollection. に追加するとします。

これらのシナリオでは、plusAssign:という名前の演算子関数を実装することで明示できます。

operator fun  MutableCollection.plusAssign(element: T) {
    add(element)
}

For each arithmetic operator, there is a corresponding compound assignment operator which all have the “Assign” suffix。 つまり、plusAssign, minusAssign, timesAssign, divAssign,remAssign:があります

>> val colors = mutableListOf("red", "blue")
>> colors += "green"
>> println(colors)
[red, blue, green]

すべての複合代入演算子関数は、Unitを返す必要があります。

4.5. 等しいコンベンション

If we override the equals method, then we can use the “==” and “!=” operatorsも:

class Money(val amount: BigDecimal, val currency: Currency) : Comparable {

    // omitted

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is Money) return false

        if (amount != other.amount) return false
        if (currency != other.currency) return false

        return true
    }

    // An equals compatible hashcode implementation
}

Kotlinは、“==”および“!=”演算子への呼び出しをequals関数呼び出しに変換します。明らかに、“!=”を機能させるために、関数呼び出しの結果は反転されます。 Note that in this case, we don’t need the operator keyword.

4.6. 比較演算子

BigIntegerをもう一度bashする時が来ました!

一方のBigIntegerがもう一方よりも大きい場合、条件付きでロジックを実行するとします。 Javaでは、ソリューションはそれほどきれいではありません。

if (BigInteger.ONE.compareTo(BigInteger.ZERO) > 0 ) {
    // some logic
}

Kotlinでまったく同じBigIntegerを使用する場合、次のように魔法のように書くことができます。

if (BigInteger.ONE > BigInteger.ZERO) {
    // the same logic
}

この魔法はKotlin has a special treatment of Java’s Comparable.のために可能です

簡単に言えば、いくつかのKotlin規則により、ComparableインターフェイスでcompareToメソッドを呼び出すことができます。 実際、「<“, “⇐”, “>”,または“>=”」によって行われた比較は、compareTo関数呼び出しに変換されます。

Kotlinタイプで比較演算子を使用するには、そのComparableインターフェイスを実装する必要があります。

class Money(val amount: BigDecimal, val currency: Currency) : Comparable {

    override fun compareTo(other: Money): Int =
      convert(Currency.DOLLARS).compareTo(other.convert(Currency.DOLLARS))

    fun convert(currency: Currency): BigDecimal = // omitted
}

次に、次のように簡単に金額を比較できます。

val oneDollar = Money(BigDecimal.ONE, Currency.DOLLARS)
val tenDollars = Money(BigDecimal.TEN, Currency.DOLLARS)
if (oneDollar < tenDollars) {
    // omitted
}

ComparableインターフェイスのcompareTo関数は、すでにoperator修飾子でマークされているため、自分で追加する必要はありません。

4.7. コンベンションで

要素がPageに属しているかどうかを確認するために、“in”の規則を使用できます。

operator fun  Page.contains(element: T): Boolean = element in elements()

繰り返しますが、the compiler would translate “in” and “!in” conventions to a function call to the contains operator function

>> val page = firstPageOfSomething()
>> "This" in page
>> "That" !in page

“in”の左側のオブジェクトは、引数としてcontainsに渡され、contains関数は右側のオペランドで呼び出されます。

4.8. インデクサーを取得

Indexers allow instances of a type to be indexed just like arrays or collections。 ページ付けされた要素のコレクションをPage<T>としてモデル化し、恥知らずにSpring Dataからアイデアを取り除いたとします。

interface Page {
    fun pageNumber(): Int
    fun pageSize(): Int
    fun elements(): MutableList
}

通常、Page, から要素を取得するには、最初にelements関数を呼び出す必要があります。

>> val page = firstPageOfSomething()
>> page.elements()[0]

Page自体は別のコレクションの単なるラッパーであるため、インデクサー演算子を使用してAPIを拡張できます。

operator fun  Page.get(index: Int): T = elements()[index]

Kotlinコンパイラは、Pagepage[index]get(index)関数呼び出しに置き換えます。

>> val page = firstPageOfSomething()
>> page[0]

getメソッド宣言に必要な数の引数を追加することで、さらに先に進むことができます。

ラップされたコレクションの一部を取得するとします。

operator fun  Page.get(start: Int, endExclusive: Int):
  List = elements().subList(start, endExclusive)

次に、次のようにPageをスライスできます。

>> val page = firstPageOfSomething()
>> page[0, 3]

また、we can use any parameter types for the get operator function, not just Int.

4.9. インデクサーの設定

get-like semanticswe can utilize them to mimic set-like operationsを実装するためのインデクサーの使用に加えて。 私たちがしなければならないのは、少なくとも2つの引数を持つsetという名前の演算子関数を定義することです。

operator fun  Page.set(index: Int, value: T) {
    elements()[index] = value
}

2つの引数だけでset関数を宣言する場合、最初の関数は角かっこ内で使用し、別の関数はassignmentの後に使用する必要があります。

val page: Page = firstPageOfSomething()
page[2] = "Something new"

set関数には、2つ以上の引数を含めることもできます。 その場合、最後のパラメーターは値であり、残りの引数は括弧内に渡す必要があります。

4.10. イテレータコンベンション

他のコレクションのようにPageを繰り返すのはどうですか? 戻り値の型としてIterator<T>を使用してiteratorという名前の演算子関数を宣言する必要があります。

operator fun  Page.iterator() = elements().iterator()

次に、Pageを反復処理できます。

val page = firstPageOfSomething()
for (e in page) {
    // Do something with each element
}

4.11. レンジコンベンション

Kotlinでは、we can create a range using the “..” operator。 たとえば、“1..42”は、1から42までの数値の範囲を作成します。

他の非数値タイプで範囲演算子を使用することが賢明な場合があります。 The Kotlin standard library provides a rangeTo convention on all Comparables

operator fun > T.rangeTo(that: T): ClosedRange = ComparableRange(this, that)

これを使用して、数日間連続して範囲を取得できます。

val now = LocalDate.now()
val days = now..now.plusDays(42)

他の演算子と同様に、Kotlinコンパイラは“..”rangeTo function呼び出しに置き換えます。

5. 演算子を慎重に使用する

Operator overloading is a powerful feature in Kotlinを使用すると、より簡潔で、場合によってはより読みやすいコードを記述できます。 しかし、大きな力には大きな責任が伴います。

Operator overloading can make our code confusing or even hard to readは、頻繁に使用されるか、ときどき誤用される場合。

したがって、特定の型に新しい演算子を追加する前に、まず、演算子が意味的に私たちが達成しようとしていることに適しているかどうかを尋ねます。 または、通常の抽象的な魔法の抽象化でも同じ効果を達成できるかどうかを尋ねます。

6. 結論

この記事では、Kotlinでの演算子のオーバーロードのメカニズムと、それを実現するために一連の規則を使用する方法について詳しく学びました。

これらすべての例とコードスニペットの実装は、GitHub projectにあります。これはMavenプロジェクトであるため、そのままインポートして実行するのは簡単です。