Sobrecarga de operador em Kotlin

Sobrecarga de operador em Kotlin

1. Visão geral

Neste tutorial, vamos falar sobre as convenções que o Kotlin oferece para suportar a sobrecarga de operador.

2. A palavra-chaveoperator

Em Java, os operadores estão vinculados a tipos específicos de Java. Por exemplo,Stringe tipos numéricos em Java podem usar o operador + para concatenação e adição, respectivamente. Nenhum outro tipo de Java pode reutilizar esse operador para seu próprio benefício. Kotlin, ao contrário, fornece um conjunto de convenções para oferecer suporte aOperator Overloading limitados.

Vamos começar com um simplesdata class:

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

Vamos aprimorar essa classe de dados com alguns operadores.

Para transformar uma função Kotlin com um nome predefinido em um operador,we should mark the function with the operator modifier. Por exemplo, podemos sobrecarregar o operador“+”:

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

Desta forma, podemos adicionar doisPoints com“+”:

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

3. Sobrecarga para operações unárias

Unary operations are those that work on just one operand. Por exemplo,-a, a++ ou!a são operações unárias. Geralmente, as funções que sobrecarregam operadores unários não assumem parâmetros.

3.1. Unary Plus

Que tal construir umShape de algum tipo com algunsPoints:

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

Em Kotlin, isso é perfeitamente possível com a função de operadorunaryPlus.

Como aShape é apenas uma coleção dePoints, podemos escrever uma classe, envolvendo algunsPoints com a capacidade de adicionar mais:

class Shape {
    private val points = mutableListOf()

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

E observe que o que nos deu a sintaxeshape \{…} foi usar umLambda comReceivers:

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

    return shape
}

3.2. Unary Minus

Suponha que temos umPoint chamado“p”e vamos negar suas coordenadas usando algo como“-p”. Então, tudo o que temos a fazer é definir uma função de operador chamadaunaryMinus emPoint:

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

Então, toda vez que adicionamos um prefixo“-“ antes de uma instância dePoint, o compilador o traduz para uma chamada de funçãounaryMinus:

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

3.3. Incremento

Podemos incrementar cada coordenada em um apenas implementando uma função de operador chamadainc:

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

O operador postfix“++” primeiro retorna o valor atual e, em seguida, aumenta o valor em um:

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

Pelo contrário, o prefixo“++” operador, primeiro aumenta o valor e depois retorna o valor recém-incrementado:

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

Além disso,since the “++” operator re-assigns the applied variable, we can’t use val with them.

3.4. Decremento

Muito semelhante ao incremento, podemos decrementar cada coordenada implementando a função de operadordec:

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

dec também suporta a semântica familiar para operadores pré e pós-decremento como para tipos numéricos regulares:

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

Além disso, como++ we não pode usar comvals _._

3.5. Not

Que tal inverter as coordenadas apenas por!p? We can do this with not:

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

Simplificando, o compilador traduz qualquer“!p” em uma chamada de função para a função de operador unário“not”:

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

4. Sobrecarga para operações binárias

Binary operators, as their name suggests, are those that work on two operands. Portanto, funções que sobrecarregam operadores binários devem aceitar pelo menos um argumento.

Vamos começar com os operadores aritméticos.

4.1. Operador aritmético positivo

Como vimos anteriormente, podemos sobrecarregar os operadores matemáticos básicos no Kotlin. Podemos usar“+” para adicionar doisPoints juntos:

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

Então podemos escrever:

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

Comoplus é uma função de operador binário, devemos declarar um parâmetro para a função.

Agora, a maioria de nós experimentou a deselegância de somar doisBigIntegers:

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

As it turns out, há uma maneira melhor de adicionar doisBigIntegers em Kotlin:

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

Isso está funcionando porque oKotlin standard library itself adds its fair share of extension operators on built-in types like BigInteger.

4.2. Outros operadores aritméticos

Semelhante aplus,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)

Então, o compilador Kotlin traduz qualquer chamada para“-“,“*”,“/”, or “%” para“minus”,“times”,“div”, or “rem”, respectivamente:

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

Ou que tal dimensionar umPoint por um fator numérico:

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

Desta forma, podemos escrever algo como“p1 * 2”:

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

Como podemos observar no exemplo anterior, não há obrigação de dois operandos serem do mesmo tipo. The same is true for return types.

4.3. Comutatividade

Os operadores sobrecarregados nem sempre sãocommutative. Ou seja,we can’t swap the operands and expect things to work as smooth as possible.

Por exemplo, podemos dimensionarPoint por um fator integral multiplicando-o porInt, digamos“p1 * 2”, mas não o contrário.

A boa notícia é que podemos definir funções do operador nos tipos internos Kotlin ou Java. Para fazer o“2 * p1” funcionar, podemos definir um operador emInt:

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

Agora também podemos usar“2 * p1”:

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

4.4. Atribuições compostas

Agora que podemos adicionar doisBigIntegers com o“”_ operator, we may be able to use the compound assignment for _“” que é“+=”.. Vamos tentar esta ideia:

var one = BigInteger.ONE
one += one

Por padrão, quando implementamos um dos operadores aritméticos, digamos“plus”, Kotlin não suporta apenas o familiar operador _ “” _, * também faz a mesma coisa para a _atribuição composta_ correspondente, que é “=”. *

Isso significa que, sem mais trabalho, também podemos fazer:

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

Mas às vezes esse comportamento padrão não é o que estamos procurando. Suponha que vamos usar“+=” para adicionar um elemento a umMutableCollection. 

Para esses cenários, podemos ser explícitos sobre isso implementando uma função de operador chamadaplusAssign:

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. Ou seja, existemplusAssign, minusAssign, timesAssign, divAssign, eremAssign:

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

Todas as funções de operador de atribuição composta devem retornarUnit.

4.5. Convenção de Igualdade

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

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 traduz qualquer chamada para os operadores“==”e“!=” para uma chamada de funçãoequals, obviamente, para fazer“!=” funcionar, o resultado da chamada de função é invertido. Note that in this case, we don’t need the operator keyword.

4.6. Operadores de comparação

É hora de baterBigInteger novamente!

Suponha que iremos executar alguma lógica condicionalmente se umBigInteger for maior que o outro. Em Java, a solução não é tão limpa:

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

Ao usar o mesmoBigInteger em Kotlin, podemos escrever isto magicamente:

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

Essa mágica é possível porqueKotlin has a special treatment of Java’s Comparable.

Simplificando, podemos chamar o métodocompareTo na interfaceComparable por algumas convenções de Kotlin. Na verdade, quaisquer comparações feitas por “<“, “⇐”, “>”, ou“>=” seriam traduzidas em uma chamada de funçãocompareTo.

Para usar operadores de comparação em um tipo Kotlin, precisamos implementar sua interfaceComparable:

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
}

Em seguida, podemos comparar valores monetários tão simples quanto:

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

Visto que a funçãocompareTo na interfaceComparable já está marcada com o modificadoroperator, não precisamos adicioná-la nós mesmos.

4.7. Em Convenção

Para verificar se um elemento pertence aPage, podemos usar a convenção“in”:

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

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

O objeto no lado esquerdo de“in” será passado como um argumento paracontainse a funçãocontains será chamada no operando do lado direito.

4.8. Obter indexador

Indexers allow instances of a type to be indexed just like arrays or collections. Suponha que vamos modelar uma coleção paginada de elementos comoPage<T>, arrancando descaradamente uma ideia deSpring Data:

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

Normalmente, a fim de recuperar um elemento de umPage, we deve primeiro chamar a funçãoelements:

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

Como oPage em si é apenas um invólucro sofisticado para outra coleção, podemos usar os operadores do indexador para aprimorar sua API:

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

O compilador Kotlin substitui qualquerpage[index] em umPage por uma chamada de funçãoget(index):

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

Podemos ir ainda mais longe adicionando quantos argumentos quisermos à declaração do métodoget.

Suponha que vamos recuperar parte da coleção embalada:

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

Então podemos fatiar umPage como:

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

Além disso,we can use any parameter types for the get operator function, not just Int.

4.9. Definir indexador

Além de usar indexadores para implementarget-like semantics,we can utilize them to mimic set-like operations também. Tudo o que precisamos fazer é definir uma função de operador chamadaset com pelo menos dois argumentos:

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

Quando declaramos uma funçãoset com apenas dois argumentos, o primeiro deve ser usado dentro do colchete e outro após oassignment:

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

A funçãoset também pode ter mais do que apenas dois argumentos. Nesse caso, o último parâmetro é o valor e o restante dos argumentos deve ser passado entre colchetes.

4.10. Convenção Iteradora

Que tal iterar aPage como outras coleções? Precisamos apenas declarar uma função de operador chamadaiterator comIterator<T> como o tipo de retorno:

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

Então, podemos iterar por meio de umPage:

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

4.11. Convenção Range

Em Kotlin,we can create a range using the “..” operator. Por exemplo,“1..42” cria um intervalo com números entre 1 e 42.

Às vezes, é sensato usar o operador de intervalo em outros tipos não numéricos. The Kotlin standard library provides a rangeTo convention on all Comparables:

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

Podemos usar isso para obter alguns dias consecutivos como um intervalo:

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

Como com outros operadores, o compilador Kotlin substitui qualquer“..” por uma chamada de funçãorangeTo .

5. Use os operadores criteriosamente

Operator overloading is a powerful feature in Kotlin que nos permite escrever códigos mais concisos e às vezes mais legíveis. No entanto, com grande poder vem uma grande responsabilidade.

Operator overloading can make our code confusing or even hard to read quando é usado com muita frequência ou ocasionalmente usado incorretamente.

Assim, antes de adicionar um novo operador a um tipo específico, primeiro pergunte se o operador é semanticamente adequado para o que estamos tentando alcançar. Ou pergunte se podemos alcançar o mesmo efeito com abstrações normais e menos mágicas.

6. Conclusão

Neste artigo, aprendemos mais sobre a mecânica da sobrecarga de operadores no Kotlin e como ele usa um conjunto de convenções para alcançá-lo.

A implementação de todos esses exemplos e trechos de código pode ser encontrada emGitHub project - é um projeto Maven, portanto, deve ser fácil de importar e executar como está.