Overbelasting door operator in Kotlin

1. Overzicht

In deze tutorial gaan we het hebben over de conventies die Kotlin biedt om overbelasting van operators te ondersteunen.

2. Het operator Trefwoord

In Java zijn operators gebonden aan specifieke Java-typen. Bijvoorbeeld, Draad en numerieke typen in Java kunnen de operator + gebruiken voor respectievelijk aaneenschakeling en optelling. Geen enkel ander Java-type kan deze operator voor zijn eigen voordeel hergebruiken. Kotlin daarentegen biedt een reeks conventies om beperkt te ondersteunen Overbelasting door operator.

Laten we beginnen met een simpele dataklasse:

dataklasse Point (val x: Int, val y: Int)

We gaan deze dataklasse uitbreiden met een paar operators.

Om een ​​Kotlin-functie met een vooraf gedefinieerde naam in een operator te veranderen, we moeten de functie markeren met de operator modificator. We kunnen bijvoorbeeld de “+” operator:

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

Op deze manier kunnen we er twee toevoegen Punten met “+”:

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

3. Overbelasting voor unaire operaties

Unaire operaties zijn operaties die op slechts één operand werken. Bijvoorbeeld, -a, a ++ of !een zijn unaire operaties. Over het algemeen hebben functies die unaire operators overbelasten geen parameters nodig.

3.1. Unary Plus

Hoe zit het met het bouwen van een Vorm van een soort met een paar Punten:

val s = vorm {+ punt (0, 0) + punt (1, 1) + punt (2, 2) + punt (3, 4)}

In Kotlin is dat perfect mogelijk met de unaryPlus operator functie.

Sinds een Vorm is slechts een verzameling van Punten, dan kunnen we een les schrijven en er een paar inpakken Punts met de mogelijkheid om meer toe te voegen:

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

En merk op dat wat ons de vorm {…} syntaxis was om een Lambda met Ontvangers:

leuke vorm (init: Shape. () -> Unit): Shape {val shape = Shape () shape.init () retourvorm}

3.2. Unair minpunt

Stel dat we een Punt genaamd "P" en we gaan de coördinatie ervan teniet doen door zoiets als "-P". Vervolgens hoeven we alleen nog een operatorfunctie met de naam te definiëren unaryMinus Aan Punt:

operator plezier Point.unaryMinus () = Punt (-x, -y)

Vervolgens voegen we elke keer een “-“ prefix voor een instantie van Punt, vertaalt de compiler het naar een unaryMinus functie-oproep:

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

3.3. Toename

We kunnen elke coördinaat met één verhogen door een operatorfunctie met de naam te implementeren inc:

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

De postfix “++” operator, retourneert eerst de huidige waarde en verhoogt vervolgens de waarde met één:

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

Integendeel, het voorvoegsel “++” operator, verhoogt eerst de waarde en retourneert vervolgens de nieuw verhoogde waarde:

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

Ook, sinds de “++” operator wijst de toegepaste variabele opnieuw toe, we kunnen deze niet gebruiken val met hen.

3.4. Afname

Vrij gelijkaardig aan increment, kunnen we elke coördinaat verlagen door de implementatie van de dec operator functie:

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

dec ondersteunt ook de bekende semantiek voor pre- en post-verlagingsoperatoren zoals voor reguliere numerieke typen:

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

Ook als ++ we kunnen niet gebruiken met vals.

3.5. Niet

Hoe zit het met het omdraaien van de coördinaten gewoon door ! p? We kunnen dit met niet:

operator plezier Point.not () = Punt (y, x)

Simpel gezegd, de compiler vertaalt elke "! P" naar een functieaanroep naar de "niet" unaire operatorfunctie:

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

4. Overbelasting voor binaire bewerkingen

Binaire operatoren zijn, zoals hun naam al doet vermoeden, operatoren die op twee operanden werken. Functies die binaire operatoren overbelasten, moeten dus ten minste één argument accepteren.

Laten we beginnen met de rekenkundige operatoren.

4.1. Plus rekenkundige operator

Zoals we eerder zagen, kunnen we elementaire wiskundige operatoren in Kotlin overbelasten. We kunnen gebruiken “+” om er twee toe te voegen Punten samen:

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

Dan kunnen we schrijven:

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

Sinds plus is een binaire operatorfunctie, we moeten een parameter voor de functie declareren.

Nu hebben de meesten van ons de inelegantie ervaren van het bij elkaar optellen van twee BigIntegers:

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

Het blijkt dat er een betere manier is om er twee toe te voegen BigIntegers in Kotlin:

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

Dit werkt omdat de De standaardbibliotheek van Kotlin voegt zelf een groot aantal extensie-operators toe aan ingebouwde typen zoals BigInteger.

4.2. Andere rekenkundige operatoren

Gelijkwaardig aan plus, aftrekken, vermenigvuldiging, divisie, en het overblijfsel werken op dezelfde manier:

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 plezier Point.div (other: Point): Point = Point (x / other.x, y / other.y) operator fun Point.rem (other: Point): Point = Point (x% other. x, y% andere.y)

Vervolgens vertaalt de Kotlin-compiler elke aanroep naar “-“, “*”, "/" Of "%" naar "minus", "keer", "Div" of "rem" , respectievelijk:

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

Of, hoe zit het met het schalen van een Punt door een numerieke factor:

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

Op deze manier kunnen we zoiets schrijven als "P1 * 2":

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

Zoals we kunnen zien aan de hand van het voorgaande voorbeeld, is er geen verplichting voor twee operanden om van hetzelfde type te zijn. Hetzelfde geldt voor retourtypen.

4.3. Commutativiteit

Overbelaste operators zijn niet altijd commutatief. Dat is, we kunnen de operanden niet verwisselen en verwachten dat alles zo soepel mogelijk verloopt.

We kunnen bijvoorbeeld een Punt met een integrale factor door deze te vermenigvuldigen met een Int, zeggen "P1 * 2", maar niet andersom.

Het goede nieuws is dat we operatorfuncties kunnen definiëren op ingebouwde Kotlin- of Java-typen. Om het "2 * p1" werk, kunnen we een operator op definiëren Int:

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

Nu kunnen we met plezier gebruiken "2 * p1" ook:

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

4.4. Samengestelde opdrachten

Nu we er twee kunnen toevoegen BigIntegers met de “+” operator, kunnen we mogelijk de samengestelde toewijzing gebruiken voor “+” dat is “+=”. Laten we dit idee eens proberen:

var one = BigInteger.ONE one + = one

Standaard, wanneer we een van de rekenkundige operatoren implementeren, bijvoorbeeld "plus", Kotlin ondersteunt niet alleen het bekende “+” operator, het doet ook hetzelfde voor de corresponderende samengestelde toewijzing, dat is "+ =".

Dit betekent dat we zonder meer werk ook kunnen doen:

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

Maar soms is dit standaardgedrag niet wat we zoeken. Stel dat we gaan gebruiken “+=” om een ​​element toe te voegen aan een MutableCollection.

Voor deze scenario's kunnen we er expliciet over zijn door een operatorfunctie met de naam te implementeren plus toewijzen:

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

Voor elke rekenkundige operator is er een overeenkomstige samengestelde toewijzingsoperator die allemaal de "Toewijzen" achtervoegsel. Dat wil zeggen, er zijn plusAssign, minusAssign, timesAssign, divAssign, en remAssign:

>> val kleuren = mutableListOf ("rood", "blauw") >> kleuren + = "groen" >> println (kleuren) [rood, blauw, groen]

Alle operatorfuncties voor samengestelde toewijzing moeten worden geretourneerd Eenheid.

4.5. Is gelijk aan conventie

Als we het is gelijk aan methode, dan kunnen we de “==” en “!=” operatorsook:

class Money (val amount: BigDecimal, val currency: Currency): Comparable {// weggelaten 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 is gelijk aan compatibele hashcode-implementatie} 

Kotlin vertaalt elke oproep naar “==” en “!=” operators naar een is gelijk aan functieaanroep, uiteraard om de “!=” werk, wordt het resultaat van de functieaanroep omgekeerd. Merk op dat we in dit geval de operator trefwoord.

4.6. Vergelijkingsoperatoren

Het is tijd om op te bashen BigInteger opnieuw!

Stel dat we een of andere logica voorwaardelijk gaan gebruiken, als er een is BigInteger is groter dan de andere. In Java is de oplossing niet zo schoon:

if (BigInteger.ONE.compareTo (BigInteger.ZERO)> 0) {// enige logica}

Bij gebruik van hetzelfde BigInteger in Kotlin kunnen we dit op magische wijze schrijven:

if (BigInteger.ONE> BigInteger.ZERO) {// dezelfde logica}

Deze magie is mogelijk omdat Kotlin heeft een speciale behandeling van Java's Vergelijkbaar.

Simpel gezegd, we kunnen de vergelijk met methode in de Vergelijkbaar interface door een paar Kotlin-conventies. In feite zijn alle vergelijkingen gemaakt door "<“, “”, of “>=” zou worden vertaald naar een vergelijk met functieaanroep.

Om vergelijkingsoperatoren op een Kotlin-type te gebruiken, moeten we het Vergelijkbaar koppel:

class Money (val bedrag: BigDecimal, val valuta: Valuta): Vergelijkbaar {override fun CompareTo (other: Money): Int = convert (Currency.DOLLARS) .compareTo (other.convert (Currency.DOLLARS)) fun convert (valuta: Valuta): BigDecimal = // weggelaten}

Dan kunnen we monetaire waarden vergelijken, zo simpel als:

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

Sinds de vergelijk met functie in de Vergelijkbaar interface is al gemarkeerd met de operator modifier, hoeven we deze zelf niet toe te voegen.

4.7. In Conventie

Om te controleren of een element tot een Bladzijde, kunnen we de "in" conventie:

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

Opnieuw, de compiler zou vertalen "in" en "!in" conventies voor een functieaanroep van de bevat operator functie:

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

Het object aan de linkerkant van "in" zal als argument worden doorgegeven aan bevat en de bevat functie wordt aangeroepen op de rechter operand.

4.8. Download Indexer

Met indexers kunnen instanties van een type worden geïndexeerd, net als arrays of verzamelingen. Stel dat we een gepagineerde verzameling elementen gaan modelleren als Bladzijde, schaamteloos een idee uit Spring Data scheuren:

interface Pagina {fun pageNumber (): Int fun pageSize (): Int leuke elementen (): MutableList}

Normaal gesproken, om een ​​element op te halen uit een Bladzijde, we moeten eerst de elementen functie:

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

Sinds de Bladzijde zelf is gewoon een mooie verpakking voor een andere verzameling, we kunnen de indexeeroperatoren gebruiken om de API te verbeteren:

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

De Kotlin-compiler vervangt alle pagina [index] op een Bladzijde naar een krijgen (index) functie-oproep:

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

We kunnen nog verder gaan door zoveel argumenten toe te voegen als we willen aan de krijgen methode verklaring.

Stel dat we een deel van de ingepakte verzameling gaan ophalen:

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

Dan kunnen we een Bladzijde Leuk vinden:

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

Ook, we kunnen elk parametertype gebruiken voor de krijgen operatorfunctie, niet alleen Int.

4.9. Stel Indexer in

Naast het gebruik van indexers voor het implementeren van get-like semantiek, we kunnen ze gebruiken om set-achtige operaties na te bootsenook. Het enige wat we hoeven te doen is een operatorfunctie met de naam te definiëren set met minstens twee argumenten:

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

Wanneer we een set functie met slechts twee argumenten, de eerste moet tussen de haakjes worden gebruikt en de andere na de opdracht:

val page: Page = firstPageOfSomething () page [2] = "Iets nieuws"

De set functie kan ook meer dan twee argumenten hebben. Als dit het geval is, is de laatste parameter de waarde en moeten de rest van de argumenten tussen haakjes worden doorgegeven.

4.10. Beroep doen op

In Kotlin en vele andere programmeertalen is het mogelijk om een ​​functie aan te roepen met functionName (args) syntaxis. Het is ook mogelijk om de syntaxis van de functie-aanroep na te bootsen met de beroep doen op operator functies. Om bijvoorbeeld te gebruiken pagina (0) in plaats van pagina [0] om toegang te krijgen tot het eerste element, kunnen we een extensie declareren:

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

Vervolgens kunnen we de volgende benadering gebruiken om een ​​bepaald pagina-element op te halen:

assertEquals (pagina (1), "Kotlin")

Hier vertaalt Kotlin de haakjes naar een aanroep naar de beroep doen op methode met een passend aantal argumenten. Bovendien kunnen we het beroep doen op operator met een willekeurig aantal argumenten.

4.11. Iterator-conventie

Hoe zit het met het herhalen van een Bladzijde zoals andere collecties? We hoeven alleen een operatorfunctie met de naam te declareren iterator met Iterator als het retourtype:

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

Dan kunnen we door een Bladzijde:

val page = firstPageOfSomething () for (e in page) {// Doe iets met elk element}

4.12. Bereik Conventie

In Kotlin, we kunnen een bereik maken met behulp van de “..” operator. Bijvoorbeeld, “1..42” creëert een reeks met nummers tussen 1 en 42.

Soms is het verstandig om de bereikoperator voor andere niet-numerieke typen te gebruiken. De standaardbibliotheek van Kotlin biedt een bereikNaar conventie over iedereen Vergelijkbaar:

operator plezier  T.rangeTo (dat: T): ClosedRange = ComparableRange (dit, dat)

We kunnen dit gebruiken om een ​​aantal opeenvolgende dagen als bereik te krijgen:

val now = LocalDate.now () val dagen = nu..now.plusDays (42)

Net als bij andere operators, vervangt de Kotlin-compiler elke “..” met een bereikNaar functieaanroep.

5. Gebruik operators oordeelkundig

Overbelasting van de operator is een krachtige functie in Kotlin waardoor we beknoptere en soms beter leesbare codes kunnen schrijven. Met grote kracht komt echter een grote verantwoordelijkheid.

Overbelasting van de operator kan onze code verwarrend of zelfs moeilijk leesbaar maken wanneer het te vaak of af en toe wordt misbruikt.

Voordat u een nieuwe operator aan een bepaald type toevoegt, moet u dus eerst vragen of de operator semantisch goed past bij wat we proberen te bereiken. Of vraag of we hetzelfde effect kunnen bereiken met normale en minder magische abstracties.

6. Conclusie

In dit artikel hebben we meer geleerd over de werking van overbelasting door operators in Kotlin en hoe het een reeks conventies gebruikt om dit te bereiken.

De implementatie van al deze voorbeelden en codefragmenten is te vinden in het GitHub-project.