1概要

このチュートリアルでは、Kotlinがオペレータの過負荷をサポートするために提供している規約について説明します。


2

operator

キーワード

Javaでは、演算子は特定のJava型に関連付けられています。たとえば、Javaの

String

と数値型は、それぞれ連結と追加に演算子を使用できます。他のJavaタイプは、それ自身の利益のためにこの演算子を再利用できません。 Kotlinは、反対に、限られた

Operator Overloading

をサポートするための一連の規約を提供します。

簡単な

データクラス

から始めましょう:

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

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

定義済みの名前を持つKotlin関数を演算子に変換するには、

operator

修飾子を使ってその関数をマークする必要があります。

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単項演算のオーバーロード

  • 単項演算は、1つのオペランドだけで動作するものです。たとえば、

    -a、a

    、または

    !a

    は単項演算です。一般に、単項演算子をオーバーロードしようとしている関数はパラメータを取りません。

3.1. 単項プラス

いくつかの

Points

を使ってある種の

Shape

を構築するのはどうでしょうか。

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

Kotlinでは、それは

unaryPlus

演算子関数で完全に可能です。


Shape

は単なる

Points

のコレクションなので、クラスを書くことができます。さらにいくつかの____Pointをラップして、さらに追加することができます。

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

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

  • そして

    shape \ {…​}

    構文を与えたのは

    Receivers

    と共に

    Lambda

    を使うことだったことに注意してください:**

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

    return shape
}

3.2. 単項マイナス


Point

という名前の

“ p”

があり、

“ – 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)

また、

“”

演算子は適用された変数を再割り当てするため、

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)

また、










































de































使用使用

シャ


では、

3.5. ではない


!p

だけで座標を反転するのはどうですか。

これを

not

で行うことができます。

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では基本的な数学演算子をオーバーロードできます。


Point +を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);

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

  • Kotlin標準ライブラリ自体が

    BigInteger

    のような組み込み型に拡張演算子を公平に追加しているため、これはうまくいきます。

4.2. その他の算術演算子


plus




subtract



multiplication



division、

、および剰余__と同じように機能します。

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コンパイラは

“ – “



“ ** ”



“/”、または“%”

への呼び出しを

“マイナス”



“ 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. 可換性

オーバーロードされた演算子が常に__commutativeになるわけではありません。つまり、

オペランドを交換して、できる限りスムーズに動作することを期待できません

たとえば、

Point



Int

に乗算することで、整数倍で拡大することができます。たとえば、““ 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

デフォルトで、私たちが

“ plus”

と言う算術演算子の1つを実装するとき、Kotlinは使い慣れた

““

演算子をサポートするだけでなく

、対応する

compound assignment

、つまり“ =”に対しても同じことを行います。

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

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)
}

  • 各算術演算子には、対応する複合代入演算子があり、すべてに

    “Assign”

    接尾辞** が付いています。つまり、

    plusAssign、minusAssign、timesAssign、divAssign、

    、および__remAssignがあります。

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

すべての複合代入演算子関数は__Unit__を返さなければなりません。

====  4.5. 等しい規約

**  __equals__メソッドをオーバーライドする場合は、__“ ==” __および__“!=” __演算子を使用することもできます。

[source,java,gutter:,true]

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では、解決策はそれほどきれいではありません。

[source,java,gutter:,true]

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

Kotlinでまったく同じ__BigInteger__を使うとき、これを魔法のように書くことができます。

[source,java,gutter:,true]

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

**  KotlinはJavaの__Comparableを特別扱いしているので、この魔法は可能です。

簡単に言うと、いくつかのKotlinの規約によって、__Comparable__インターフェースの__compareTo__メソッドを呼び出すことができます。実際、“ __ <”、“ <=”、“>”、__または__“> =” __による比較はすべて、__compareTo__関数呼び出しに変換されます。

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

[source,java,gutter:,true]

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
}

それから私達は貨幣価値を比較することができるのと同じくらい簡単にすることができます:

[source,java,gutter:,true]

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” __規則を使用できます。

[source,java,gutter:,true]

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

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

[source,java,gutter:,true]

>> val page = firstPageOfSomething()
>> “This” in page
>> “That” !in page

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

====  4.8. インデクサーを取得

** インデクサーは、配列やコレクションと同じように、型のインスタンスにインデックスを付けることを可能にします。 https://www.baeldung.com/spring-data-jpa-query[Spring Data]からアイデアを切り取る__Page <T> __としてページ付けされた要素のコレクションをモデル化しようとしているとします。

[source,java,gutter:,true]

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

通常、____Pageから要素を取得するには、____ weはまず__elements__関数を呼び出す必要があります。

[source,java,gutter:,true]

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


Page

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

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

Kotlinコンパイラは、__Page__の__page[index]__を__get(index)__関数呼び出しに置き換えます。

[source,java,gutter:,true]

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


get

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

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

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

それでは、次のように

Page

をスライスできます。

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

また、**  __Int __だけでなく、__get__演算子関数にも任意のパラメータ型を使用できます。

====  4.9. インデクサーの設定

__get-like semantics__を実装するためにインデクサーを使用することに加えて、**  set-like操作を模倣するためにそれらを利用することもできます** 。あと2つの引数を使用して__set__という名前の演算子関数を定義するだけです。

[source,java,gutter:,true]

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

2つだけ引数を指定して__set__関数を宣言するときは、最初のものを角かっこ内で使用し、別の引数を__assignment__の後に使用します。

[source,java,gutter:,true]

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

__set__関数は、2つ以上の引数を持つこともできます。もしそうなら、最後のパラメータは値であり、残りの引数は括弧の中に渡されるべきです。

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

他のコレクションのように__Page__を反復するのはどうですか。戻り型として__Iterator <T> __を使用して__iterator__という名前の演算子関数を宣言するだけです。

[source,java,gutter:,true]

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

それでは、__Page__を繰り返し処理します。

[source,actionscript3,gutter:,true]

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

====  4.11. レンジ大会

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

他の非数値型では範囲演算子を使用するのが賢明です。 **  Kotlin標準ライブラリはすべての__Comparables ** に__rangeTo__規則を提供します。

[source,java,gutter:,true]

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

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

[source,java,gutter:,true]

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

他の演算子と同様に、Kotlinコンパイラは__ ".." __を____rangeTo ____function呼び出しに置き換えます。

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

** 演算子オーバーロードはKotlin ** の強力な機能です。しかし、大きな力があれば大きな責任が生まれます。

** あまりにも頻繁に使用されたり、時折誤用されたりすると、** オペレータのオーバーロードによってコードが混乱したり、読みにくくなることさえあります。

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

===  ** **  6.  ** ** 結論

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

これらすべての例とコードスニペットの実装はhttps://github.com/eugenp/tutorials/tree/master/core-kotlin[GitHubプロジェクト]で見つけることができます - それはMavenプロジェクトなのでインポートするのは簡単なはずですそのまま実行します。