Überladen des Bedieners in Kotlin

Bedienerüberladung in Kotlin

1. Überblick

In diesem Tutorial werden wir über die Konventionen sprechen, die Kotlin zur Unterstützung der Überlastung von Bedienern bereitstellt.

2. Das Schlüsselwortoperator

In Java sind Operatoren an bestimmte Java-Typen gebunden. Beispielsweise könnenString und numerische Typen in Java den Operator + für die Verkettung bzw. Addition verwenden. Kein anderer Java-Typ kann diesen Operator zu seinem eigenen Vorteil wiederverwenden. Kotlin bietet im Gegenteil eine Reihe von Konventionen zur Unterstützung begrenzterOperator Overloading.

Beginnen wir mit einem einfachendata class:

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

Wir werden diese Datenklasse mit einigen Operatoren erweitern.

Um eine Kotlin-Funktion mit einem vordefinierten Namen in einen Operatorwe should mark the function with the operator modifier. umzuwandeln, können wir beispielsweise den Operator“+” überladen:

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

Auf diese Weise können wir zweiPoints mit“+” addieren:

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

3. Überladung für unäre Operationen

Unary operations are those that work on just one operand. Zum Beispiel sind-a, a++ oder!a unäre Operationen. Im Allgemeinen benötigen Funktionen, die unäre Operatoren überlasten, keine Parameter.

3.1. Unary Plus

Wie wäre es, einShape mit ein paarPoints zu konstruieren:

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

In Kotlin ist dies mit der OperatorfunktionunaryPlusdurchaus möglich.

DaShape nur eine Sammlung vonPoints ist, können wir eine Klasse schreiben, die einigePoints umschließt und weitere hinzufügen kann:

class Shape {
    private val points = mutableListOf()

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

Und beachten Sie, dass wir dieshape \{…}-Syntax erhalten haben, indem wirLambda mitReceivers verwendet haben:

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

    return shape
}

3.2. Unary Minus

Angenommen, wir habenPoint mit dem Namen“p” und wir werden seine Koordinationen mit etwas wie“-p” negieren. Dann müssen wir nur noch eine Operatorfunktion mit dem NamenunaryMinus aufPoint: definieren

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

Jedes Mal, wenn wir ein“-“-Präfix vor einer Instanz vonPoint hinzufügen, übersetzt der Compiler es in einenunaryMinus-Funktionsaufruf:

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

3.3. Zuwachs

Wir können jede Koordinate um eins erhöhen, indem wir einfach eine Operatorfunktion mit dem Nameninc implementieren:

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

Der Operator postfix“++”gibt zuerst den aktuellen Wert zurück und erhöht dann den Wert um eins:

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

Im Gegenteil, der Operator des Präfix“++”erhöht zuerst den Wert und gibt dann den neu inkrementierten Wert zurück:

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

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

3.4. Dekrement

Ähnlich wie beim Inkrementieren können wir jede Koordinate dekrementieren, indem wir die Operatorfunktiondecimplementieren:

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

dec unterstützt auch die bekannte Semantik für Operatoren vor und nach dem Dekrementieren wie für reguläre numerische Typen:

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

Ebenso wie++ kann swe nicht mitvals _._ verwenden.

3.5. Not

Wie wäre es, wenn Sie die Koordinaten nur um!p spiegeln? We can do this with not:

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

Einfach ausgedrückt übersetzt der Compiler alle“!p” in einen Funktionsaufruf in die unäre Operatorfunktion von“not”:

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

4. Überladung für binäre Operationen

Binary operators, as their name suggests, are those that work on two operands. Funktionen, die Binäroperatoren überladen, sollten also mindestens ein Argument akzeptieren.

Beginnen wir mit den arithmetischen Operatoren.

4.1. Plus arithmetischer Operator

Wie wir bereits gesehen haben, können wir in Kotlin grundlegende mathematische Operatoren überladen. Wir können“+” verwenden, um zweiPoints zu addieren:

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

Dann können wir schreiben:

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

Daplus eine binäre Operatorfunktion ist, sollten wir einen Parameter für die Funktion deklarieren.

Jetzt haben die meisten von uns die Uneleganz erlebt, zweiBigIntegers zu addieren:

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

As it turns out gibt es eine bessere Möglichkeit, zweiBigIntegers in Kotlin hinzuzufügen:

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

Dies funktioniert, weilKotlin standard library itself adds its fair share of extension operators on built-in types like BigInteger.

4.2. Andere arithmetische Operatoren

Ähnlich wieplus,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)

Dann übersetzt der Kotlin-Compiler jeden Aufruf in“-“,“*”,“/”, or “%” in“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)

Oder wie wäre es mit einer Skalierung vonPoint um einen numerischen Faktor:

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

Auf diese Weise können wir so etwas wie“p1 * 2” schreiben:

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

Wie wir aus dem vorhergehenden Beispiel ersehen können, müssen zwei Operanden nicht vom gleichen Typ sein. The same is true for return types.

4.3. Kommutativität

Überladene Operatoren sind nicht immercommutative., dhwe can’t swap the operands and expect things to work as smooth as possible.

Zum Beispiel können wir einPoint mit einem Integralfaktor skalieren, indem wir es mit einemInt multiplizieren, beispielsweise“p1 * 2”, aber nicht umgekehrt.

Die gute Nachricht ist, dass wir Operatorfunktionen für integrierte Kotlin- oder Java-Typen definieren können. Damit“2 * p1”funktioniert, können wir einen Operator fürIntdefinieren:

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

Jetzt können wir auch gerne“2 * p1” verwenden:

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

4.4. Verbundzuweisungen

Jetzt können wir zweiBigIntegers mit“”_ operator, we may be able to use the compound assignment for _“” addieren, was“+=”. ist. Versuchen wir diese Idee:

var one = BigInteger.ONE
one += one

Wenn wir einen der arithmetischen Operatoren implementieren, z. B.“plus”, unterstützt Kotlin standardmäßig nicht nur den bekannten _ "" _ -Operator *, sondern auch das Gleiche für die entsprechende _compound-Zuweisung_, nämlich "=". * *

Dies bedeutet, dass wir ohne weitere Arbeit auch Folgendes tun können:

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

Manchmal ist dieses Standardverhalten jedoch nicht das, wonach wir suchen. Angenommen, wir verwenden“+=”, um einemMutableCollection.  ein Element hinzuzufügen

Für diese Szenarien können wir dies explizit angeben, indem wir eine Operatorfunktion mit dem NamenplusAssign: implementieren

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. Das heißt, es gibtplusAssign, minusAssign, timesAssign, divAssign, undremAssign:

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

Alle zusammengesetzten Zuweisungsoperatorfunktionen müssenUnit zurückgeben.

4.5. Equals Convention

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

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 übersetzt jeden Aufruf von“==” und“!=” Operatoren in einenequals Funktionsaufruf. Damit das“!=”funktioniert, wird das Ergebnis des Funktionsaufrufs offensichtlich invertiert. Note that in this case, we don’t need the operator keyword.

4.6. Vergleichsoperatoren

Es ist Zeit, erneut aufBigInteger zu schlagen!

Angenommen, wir führen eine Logik unter bestimmten Bedingungen aus, wenn einBigInteger größer als das andere ist. In Java ist die Lösung nicht ganz so sauber:

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

Wenn wir in Kotlin die gleichenBigInteger verwenden, können wir dies auf magische Weise schreiben:

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

Diese Magie ist möglich, weilKotlin has a special treatment of Java’s Comparable.

Einfach ausgedrückt können wir diecompareTo-Methode in derComparable-Schnittstelle durch einige Kotlin-Konventionen aufrufen. Tatsächlich würden alle Vergleiche von „<“, “⇐”, “>”, oder“>=” in einen Funktionsaufruf voncompareTo übersetzt.

Um Vergleichsoperatoren für einen Kotlin-Typ zu verwenden, müssen wir dessenComparable-Schnittstelle implementieren:

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
}

Dann können wir Geldwerte so einfach vergleichen:

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

Da diecompareTo-Funktion in derComparable-Schnittstelle bereits mit dem Modifikatoroperator markiert ist, müssen wir sie nicht selbst hinzufügen.

4.7. In der Konvention

Um zu überprüfen, ob ein Element zu einemPage gehört, können wir die“in”-Konvention verwenden:

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

Wiederumthe 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

Das Objekt auf der linken Seite von“in” wird als Argument ancontains übergeben, und die Funktioncontains wird auf dem rechten Operanden aufgerufen.

4.8. Holen Sie sich Indexer

Indexers allow instances of a type to be indexed just like arrays or collections. Angenommen, wir modellieren eine paginierte Sammlung von Elementen alsPage<T> und rippen schamlos eine Idee vonSpring Data ab:

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

Normalerweise sollte zum Abrufen eines Elements aus einemPage, we zuerst die Funktionelementsaufgerufen werden:

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

DaPage selbst nur ein ausgefallener Wrapper für eine andere Sammlung ist, können wir die Indexeroperatoren verwenden, um die API zu verbessern:

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

Der Kotlin-Compiler ersetzt allepage[index] auf einemPage durch einenget(index) Funktionsaufruf:

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

Wir können noch weiter gehen, indem wir der Methodendeklaration vongeto viele Argumente hinzufügen, wie wir möchten.

Angenommen, wir werden einen Teil der verpackten Sammlung abrufen:

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

Dann können wir einPage wie folgt schneiden:

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

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

4.9. Stellen Sie den Indexer ein

Zusätzlich zur Verwendung von Indexern zur Implementierung vonget-like semantics,we can utilize them to mimic set-like operations. Wir müssen lediglich eine Operatorfunktion mit dem Namenset mit mindestens zwei Argumenten definieren:

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

Wenn wir eineset-Funktion mit nur zwei Argumenten deklarieren, sollte das erste in der Klammer und ein weiteres nach demassignment verwendet werden:

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

Die Funktionsetkann auch mehr als nur zwei Argumente haben. In diesem Fall ist der letzte Parameter der Wert und der Rest der Argumente sollte in eckigen Klammern angegeben werden.

4.10. Iteratorenkonvention

Wie wäre es mit einem Iterieren vonPagewie bei anderen Sammlungen? Wir müssen nur eine Operatorfunktion mit dem Nameniterator mitIterator<T> als Rückgabetyp deklarieren:

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

Dann können wir einPage durchlaufen:

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

4.11. Range Convention

In Kotlin sindwe can create a range using the “..” operator. Beispielsweise erstellt“1..42” einen Bereich mit Zahlen zwischen 1 und 42.

Manchmal ist es sinnvoll, den Bereichsoperator für andere nicht numerische Typen zu verwenden. The Kotlin standard library provides a rangeTo convention on all Comparables:

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

Wir können dies verwenden, um ein paar aufeinanderfolgende Tage als Bereich zu erhalten:

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

Wie bei anderen Operatoren ersetzt der Kotlin-Compiler“..” durch einen FunktionsaufrufrangeTo .

5. Verwenden Sie Operatoren mit Bedacht

Operator overloading is a powerful feature in Kotlin, wodurch wir präzisere und manchmal besser lesbare Codes schreiben können. Mit großer Kraft geht jedoch auch große Verantwortung einher.

Operator overloading can make our code confusing or even hard to read, wenn es zu häufig verwendet oder gelegentlich missbraucht wird.

Bevor Sie einem bestimmten Typ einen neuen Operator hinzufügen, fragen Sie zunächst, ob der Operator semantisch gut zu dem passt, was wir erreichen möchten. Oder fragen Sie, ob wir mit normalen und weniger magischen Abstraktionen den gleichen Effekt erzielen können.

6. Fazit

In diesem Artikel haben wir mehr über die Mechanismen des Überladens von Operatoren in Kotlin erfahren und wie es eine Reihe von Konventionen verwendet, um dies zu erreichen.

Die Implementierung all dieser Beispiele und Codefragmente finden Sie inGitHub project - es ist ein Maven-Projekt, daher sollte es einfach zu importieren und auszuführen sein, wie es ist.