Lebihan Operator di Kotlin

1. Gambaran keseluruhan

Dalam tutorial ini, kita akan membincangkan konvensyen yang diberikan oleh Kotlin untuk menyokong beban operator yang berlebihan.

2. Kata kunci pengendali

Di Jawa, operator terikat dengan jenis Java tertentu. Sebagai contoh, String dan jenis numerik di Java masing-masing dapat menggunakan operator + untuk penggabungan dan penambahan. Tidak ada jenis Java lain yang dapat menggunakan semula operator ini untuk kepentingannya sendiri. Kotlin, sebaliknya, menyediakan sekumpulan konvensyen untuk menyokong Beban Operator yang terhad .

Mari mulakan dengan kelas data mudah :

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

Kami akan meningkatkan kelas data ini dengan beberapa operator.

Untuk mengubah fungsi Kotlin dengan nama yang telah ditentukan menjadi operator, kita harus menandakan fungsi tersebut dengan pengubah operator . Sebagai contoh, kita boleh membebankan operator "+" :

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

Dengan cara ini kita dapat menambahkan dua Poin dengan “+” :

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

3. Beban berlebihan untuk Operasi Tidak Berguna

Operasi tidak tetap adalah operasi yang hanya dijalankan pada satu operasi . Contohnya, -a, a ++ atau ! A adalah operasi yang tidak bersesuaian. Secara amnya, fungsi yang akan membebankan operator yang tidak berfungsi tidak memerlukan parameter.

3.1. Unary Plus

Bagaimana dengan membina Bentuk semacam dengan beberapa Mata :

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

Di Kotlin, itu sangat mungkin dilakukan dengan fungsi pengendali unaryPlus .

Oleh kerana Shape hanyalah kumpulan Poin , maka kita dapat menulis kelas, membungkus beberapa Titik dengan kemampuan untuk menambahkan lebih banyak:

class Shape { private val points = mutableListOf() operator fun Point.unaryPlus() { points.add(this) } }

Dan perhatikan bahawa apa yang memberi kita bentuk {…} sintaks adalah menggunakan Lambda dengan Penerima :

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

3.2. Tidak Berkurang

Anggaplah kita memiliki Titik bernama "p" dan kita akan menolak koordinatnya menggunakan sesuatu seperti "-p" . Kemudian, yang harus kita lakukan adalah menentukan fungsi pengendali bernama unaryMinus on Point:

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

Kemudian, setiap kali kita menambah awalan "-" sebelum contoh Point , penyusun menerjemahkannya ke panggilan fungsi unaryMinus :

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

3.3. Kenaikan

Kami dapat meningkatkan setiap koordinat dengan satu hanya dengan melaksanakan fungsi operator bernama inc :

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

Pengendali postfix "++" , pertama mengembalikan nilai semasa dan kemudian meningkatkan nilai dengan satu:

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

Sebaliknya, operator awalan "++" , pertama meningkatkan nilai dan kemudian mengembalikan nilai yang baru ditingkatkan:

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

Juga, kerana operator "++" menetapkan semula pemboleh ubah yang diterapkan, kami tidak dapat menggunakan val dengannya.

3.4. Penurunan

Sama seperti kenaikan, kita dapat mengurangkan setiap koordinat dengan melaksanakan fungsi operator dek :

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

dec juga menyokong semantik biasa bagi pengendali pra dan pasca penurunan seperti untuk jenis angka biasa:

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

Juga, seperti ++ kita tidak dapat menggunakan - dengan val s .

3.5. Tidak

Bagaimana dengan membalik koordinat hanya dengan ! P ? Kami boleh melakukan ini dengan tidak :

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

Ringkasnya, penyusun menerjemahkan sebarang "! P" ke panggilan fungsi ke fungsi pengendali yang tidak "tidak" :

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

4. Beban berlebihan untuk Operasi Binari

Pengendali binari, seperti namanya, adalah pengendali dua operasi . Oleh itu, fungsi pengendali binari yang berlebihan harus menerima sekurang-kurangnya satu argumen.

Mari mulakan dengan pengendali aritmetik.

4.1. Operator Aritmetik Plus

Seperti yang kita lihat sebelumnya, kita dapat membebani operator matematik asas di Kotlin. Kita dapat menggunakan "+" untuk menambahkan dua Poin bersama:

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

Kemudian kita boleh menulis:

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

Oleh kerana plus adalah fungsi pengendali binari, kita harus menyatakan parameter untuk fungsi tersebut.

Sekarang, sebahagian besar dari kita telah mengalami kekurangan untuk menambahkan dua BigInteger :

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

Ternyata, ada cara yang lebih baik untuk menambahkan dua BigIntegers di Kotlin:

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

Ini berfungsi kerana perpustakaan standard Kotlin itu sendiri menambah bahagian pengendali pelanjutannya pada jenis terbina dalam seperti BigInteger .

4.2. Pengendali Aritmetik lain

Mirip dengan tambah , pengurangan , pendaraban , pembahagian, dan selebihnya berfungsi dengan cara yang sama:

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)

Kemudian, penyusun Kotlin menerjemahkan sebarang panggilan ke “-“ , “*” , “/”, atau “%” ke “minus” , “times” , “div”, atau “rem” , masing-masing:

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

Atau, bagaimana dengan skala Titik dengan faktor angka:

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

Dengan cara ini kita dapat menulis sesuatu seperti "p1 * 2" :

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

As we can spot from the preceding example, there is no obligation for two operands to be of the same type. The same is true for return types.

4.3. Commutativity

Overloaded operators are not always commutative. That is, we can't swap the operands and expect things to work as smooth as possible.

For example, we can scale a Point by an integral factor by multiplying it to an Int, say “p1 * 2”, but not the other way around.

The good news is, we can define operator functions on Kotlin or Java built-in types. In order to make the “2 * p1” work, we can define an operator on Int:

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

Now we can happily use “2 * p1” as well:

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

4.4. Compound Assignments

Now that we can add two BigIntegers with the “+” operator, we may be able to use the compound assignment for “+” which is “+=”. Let's try this idea:

var one = BigInteger.ONE one += one

By default, when we implement one of the arithmetic operators, say “plus”, Kotlin not only supports the familiar “+” operator, it also does the same thing for the corresponding compound assignment, which is “+=”.

This means, without any more work, we can also do:

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

But sometimes this default behavior is not what we're looking for. Suppose we're going to use “+=” to add an element to a MutableCollection.

For these scenarios, we can be explicit about it by implementing an operator function named 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. That is, there are plusAssign, minusAssign, timesAssign, divAssign, and remAssign:

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

All compound assignment operator functions must return Unit.

4.5. Equals Convention

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

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 translates any call to “==” and “!=” operators to an equals function call, obviously in order to make the “!=” work, the result of function call gets inverted. Note that in this case, we don't need the operator keyword.

4.6. Comparison Operators

It's time to bash on BigInteger again!

Suppose we're gonna run some logic conditionally if one BigInteger is greater than the other. In Java, the solution is not all that clean:

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

When using the very same BigInteger in Kotlin, we can magically write this:

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

This magic is possible because Kotlin has a special treatment of Java's Comparable.

Simply put, we can call the compareTo method in the Comparable interface by a few Kotlin conventions. In fact, any comparisons made by “<“, “”, or “>=” would be translated to a compareTo function call.

In order to use comparison operators on a Kotlin type, we need to implement its Comparable interface:

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 }

Then we can compare monetary values as simple as:

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

Since the compareTo function in the Comparable interface is already marked with the operator modifier, we don't need to add it ourselves.

4.7. In Convention

In order to check if an element belongs to a Page, we can use the “in” convention:

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

Again, 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

The object on the left-hand side of “in” will be passed as an argument to contains and the contains function would be called on the right-side operand.

4.8. Get Indexer

Indexers allow instances of a type to be indexed just like arrays or collections. Suppose we're gonna model a paginated collection of elements as Page, shamelessly ripping off an idea from Spring Data:

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

Normally, in order to retrieve an element from a Page, we should first call the elements function:

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

Since the Page itself is just a fancy wrapper for another collection, we can use the indexer operators to enhance its API:

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

The Kotlin compiler replaces any page[index] on a Page to a get(index) function call:

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

We can go even further by adding as many arguments as we want to the get method declaration.

Suppose we're gonna retrieve part of the wrapped collection:

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

Then we can slice a Page like:

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

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

4.9. Set Indexer

In addition to using indexers for implementing get-like semantics, we can utilize them to mimic set-like operations, too. All we have to do is to define an operator function named set with at least two arguments:

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

When we declare a set function with just two arguments, the first one should be used inside the bracket and another one after the assignment:

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

The set function can have more than just two arguments, too. If so, the last parameter is the value and the rest of the arguments should be passed inside the brackets.

4.10. Invoke

In Kotlin and many other programming languages, it's possible to invoke a function with functionName(args) syntax. It's also possible to mimic the function call syntax with the invoke operator functions. For example, in order to use page(0) instead of page[0] to access the first element, we can declare an extension:

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

Then, we can use the following approach to retrieve a particular page element:

assertEquals(page(1), "Kotlin")

Here, Kotlin translates the parentheses to a call to the invoke method with an appropriate number of arguments. Moreover, we can declare the invoke operator with any number of arguments.

4.11. Iterator Convention

How about iterating a Page like other collections? We just have to declare an operator function named iterator with Iterator as the return type:

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

Then we can iterate through a Page:

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

4.12. Range Convention

In Kotlin, we can create a range using the “..” operator. For example, “1..42” creates a range with numbers between 1 and 42.

Sometimes it's sensible to use the range operator on other non-numeric types. The Kotlin standard library provides a rangeTo convention on all Comparables:

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

We can use this to get a few consecutive days as a range:

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

As with other operators, the Kotlin compiler replaces any “..” with a rangeTo function call.

5. Use Operators Judiciously

Operator overloading is a powerful feature in Kotlin which enables us to write more concise and sometimes more readable codes. However, with great power comes great responsibility.

Operator overloading can make our code confusing or even hard to read when its too frequently used or occasionally misused.

Oleh itu, sebelum menambahkan operator baru ke jenis tertentu, pertama, tanyakan apakah pengendali semantik sesuai untuk apa yang kita cuba capai. Atau tanyakan adakah kita dapat mencapai kesan yang sama dengan abstraksi normal dan kurang ajaib.

6. Kesimpulannya

Dalam artikel ini, kami mempelajari lebih lanjut mengenai mekanik beban operator yang berlebihan di Kotlin dan bagaimana ia menggunakan sekumpulan konvensyen untuk mencapainya.

Pelaksanaan semua contoh dan potongan kod ini boleh didapati di projek GitHub.