1. 概要

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

2. 演算子キーワード

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

簡単なデータクラスから始めましょう。

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

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

事前定義された名前のKotlin関数を演算子に変換するには、演算子修飾子を使用して関数をマークする必要があります。たとえば、「+」演算子をオーバーロードできます:

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

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

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

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

単項演算は、1つのオペランドでのみ機能する演算です。 たとえば、 -a、a ++ 、または!aは単項演算です。 一般に、単項演算子をオーバーロードする関数はパラメーターを取りません。

3.1. 単項プラス

いくつかのポイントを使用して、ある種の形状を作成するのはどうですか。

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

Kotlinでは、unaryPlus演算子関数を使用してこれを完全に実行できます。

ShapePointsの単なるコレクションであるため、いくつかの Point をラップして、さらに追加する機能を備えたクラスを作成できます。

class Shape {
    private val points = mutableListOf<Point>()

    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」のようなものを使用してその調整を無効にするとします。 次に、 unaryMinus on Point:という名前の演算子関数を定義するだけです。

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)

また、「++」演算子は適用された変数を再割り当てするため、valを使用することはできません。

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)

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

3.5. いいえ

!p だけで座標を反転してみませんか? これは次の方法では実行できません:

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

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

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

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

二項演算子は、その名前が示すように、2つのオペランドで機能する演算子です。 したがって、二項演算子をオーバーロードする関数は、少なくとも1つの引数を受け入れる必要があります。

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

4.1. プラス算術演算子

前に見たように、Kotlinでは基本的な数学演算子をオーバーロードできます。 “ +” を使用して、2つのポイントを追加できます。

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つのBigIntegerを足し合わせるという優雅さを経験しています。

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

結局のところ、Kotlinに2つのBigIntegersを追加するより良い方法があります。

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

Kotlin標準ライブラリ自体が、BigIntegerなどの組み込み型に拡張演算子のかなりの部分を追加しているため、これは機能しています。

4.2. その他の算術演算子

plus と同様に、減算、乗算、除算、および余りは同じように機能します:

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コンパイラは、すべての呼び出しを “-” “*” “/”、または “%”から“マイナス”[に変換します。 X108X]、「times」「div」、または「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つのオペランドが同じタイプである必要はありません。 同じことがリターンタイプにも当てはまります。

4.3. 可換性

オーバーロードされた演算子は常にではありません可換。 あれは、 オペランドを交換することはできず、物事が可能な限りスムーズに機能することを期待できません

たとえば、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. 複合割り当て

これで2つ追加できます BigIntegers とともに 「+」 演算子、複合代入を使用できる場合があります 「+」 これは 「+=」。 このアイデアを試してみましょう

var one = BigInteger.ONE
one += one

デフォルトでは、算術演算子の1つを実装すると、次のようになります。 “プラス” 、Kotlinはおなじみのサポートだけではありません 「+」 オペレーター、 対応する複合割り当て、つまり「+=」に対しても同じことを行います。

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

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 <T> MutableCollection<T>.plusAssign(element: T) {
    add(element)
}

算術演算子ごとに、対応する複合代入演算子があり、すべて「割り当て」接尾辞が付いています。 つまり、 plusAssign、minusAssign、timesAssign、divAssign、、および remAssign:があります。

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

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

4.5. イコールコンベンション

equalsメソッドをオーバーライドすると、「==」および「!=」演算子も使用できます。

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

    // 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関数呼び出しに変換します。 =” が機能し、関数呼び出しの結果が反転します。 この場合、operatorキーワードは必要ないことに注意してください。

4.6. 比較演算子

BigInteger をもう一度バッシュする時が来ました!

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

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

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

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

この魔法が可能になるのは Kotlinには、JavaのComparableの特別な扱いがあります。

簡単に言えば、いくつかのKotlin規則により、CompareableインターフェイスでcompareToメソッドを呼び出すことができます。 実際、「 <“、“ <=”、“>”、 また 「>=」 に翻訳されます compareTo 関数呼び出し。

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

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

    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
}

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

4.7. コンベンションで

要素がページに属しているかどうかを確認するために、「in」規則を使用できます。

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

繰り返しますが、コンパイラは、「in」および「!in」規則を、contains演算子関数への関数呼び出しに変換します。

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

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

4.8. インデクサーを取得

インデクサーを使用すると、配列やコレクションと同じように、型のインスタンスにインデックスを付けることができます。 ページ化された要素のコレクションを次のようにモデル化するとします。 ページ 、恥知らずにアイデアをはぎ取る春のデータ

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

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

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

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

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

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

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

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

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

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

次に、ページを次のようにスライスできます。

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

また、 Int。だけでなく、get演算子関数にも任意のパラメータータイプを使用できます。

4.9. インデクサを設定する

get-likeセマンティクスを実装するためにインデクサーを使用することに加えて、set-like操作を模倣するためにそれらを利用することもできます。 set という名前の演算子関数を、少なくとも2つの引数で定義するだけです。

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

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

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

set 関数は、2つ以上の引数を持つこともできます。 その場合、最後のパラメーターは値であり、残りの引数は角かっこで囲む必要があります。

4.10. 呼び出す

Kotlinや他の多くのプログラミング言語では、次のコマンドを使用して関数を呼び出すことができます。 functionName(args) 構文 。 また、invoke演算子関数を使用して関数呼び出し構文を模倣することもできます。 たとえば、を使用するには page(0) それ以外のページ[0] 最初の要素にアクセスするために、拡張機能を宣言できます。

operator fun <T> Page<T>.invoke(index: Int): T = elements()[index]

次に、次のアプローチを使用して特定のページ要素を取得できます。

assertEquals(page(1), "Kotlin")

ここで、Kotlinは、括弧を適切な数の引数を使用したinvokeメソッドの呼び出しに変換します。 さらに、私たちは宣言することができます呼び出す任意の数の引数を持つ演算子。

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

他のコレクションのようにページを繰り返すのはどうですか? 名前の付いた演算子関数を宣言する必要がありますイテレータイテレータ戻りタイプとして:

operator fun <T> Page<T>.iterator() = elements().iterator()

次に、ページを反復処理できます。

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

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

Kotlinでは、「..」演算子を使用して範囲を作成できます。 たとえば、“ 1..42” は、1から42までの数値の範囲を作成します。

他の非数値タイプで範囲演算子を使用することが賢明な場合があります。  Kotlin標準ライブラリは、すべてのComparablesでrangeTo規則を提供します。

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

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

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

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

5.5。 演算子を慎重に使用する

演算子のオーバーロードは、Kotlin の強力な機能であり、より簡潔で、場合によってはより読みやすいコードを記述できます。 しかし、大きな力には大きな責任が伴います。

演算子のオーバーロードは、コードが頻繁に使用されたり、ときどき誤用されたりすると、コードを混乱させたり、読みにくくしたりする可能性があります

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

6.6。 結論

この記事では、Kotlinでの演算子のオーバーロードの仕組みと、それを実現するために一連の規則をどのように使用するかについて詳しく学びました。

これらすべての例とコードスニペットの実装は、GitHubプロジェクトにあります。