Operatori
Un operatore è un simbolo (o una sequenza di simboli) che specifica quale legge applicare a uno o più operandi, per generare un risultato. Vediamo quali sono gli operatori del Kotlin e come si esegue l'overloading su di essi.
Kotlin ha dovuto superare le limitazioni della JVM nella materia degli operatori. Infatti la JVM non permette l'overloading, e quindi Kotlin ha realizzato un modo per by-passare queste limitazioni. Quanto il compilatore si trova di fronte aglio operatori (cioè dei simboli):
- Determina il tipo di a (l'operando di sinistra, se non in un caso specifico) e cerca un metodo (dell'istanza e/o di estensione) che abbia il nome associato all’operatore e con il modificatore operator, e che il il tipo di b (l'operando di destra, se non è un operatore unario e non siamo in un determinato caso specifico) sia compatibile con gli overloading.
- Se essa non si trova e/o è ambigua la chiamata viene lanciato un errore di compilazione.
- Viene dedotto dal risultato della funzione il tipo di ritorno del suddetto operatore.
Si noti che queste operazioni, così come molte altre, sono ottimizzate per i tipi di base del Kotlin.
Operatori unari
Operatore | Tradotto in: |
---|---|
+a | a.unaryPlus() |
-a | a.unaryMinus() |
!a | a.not() |
Ad esempio, ecco come puoi sovraccaricare l'operatore unario meno (-):
data class Point(val x: Int, val y: Int) {
operator fun unaryMinus() = Point(-x, -y)
}
val point = Point(10, 20)
fun main(args: Array<String>) {
println(-point)
}
Operatori di incremento e decremento
Operatore | Tradotto in: |
---|---|
a++ e ++a | a.inc() |
a-- e --a | a.dec() |
Rispetto agli operatori unari c'è da precisare che:
- Nel caso che sia post-fisso:
- Kotlin si memorizza il valore di a in un archivio temporaneo;
- Esegue inc() o dec();
- Assegna il risultato di inc() o dec() ad a;
- Ritorna il valore memorizzato nell’archivio.
- Nel caso che sia pre-fisso:
- Esegue inc() o dec();
- Assegna il risultato di inc() o dec() ad a;
- Ritorna a.
Ecco un esempio:
data class Counter(val dayIndex: Int = 0) {
operator fun inc(): Counter {
return Counter(dayIndex + 1)
}
}
fun main(args: Array<String>) {
var counter = Counter()
println(counter++) // print Counter(dayIndex=0)
println(++counter) // print Counter(dayIndex=2)
}
Operatori binari
Operatori aritmetici
Operatore | Tradotto in: |
---|---|
a + b | a.plus(b) |
a - b | a.minus(b) |
a * b | a.times(b) |
a / b | a.div(b) |
a % b | a.rem(b), a.mod(b) (deprecato) |
a..b | a.rangeTo(b) |
Si noti che l'operatore rem() è supportato da Kotlin 1.1. Kotlin 1.0 utilizza l'operatore mod(), che è deprecato in Kotlin 1.1.
Di seguito è riportato un esempio di classe Contatore che inizia con un valore dato e può essere incrementato utilizzando overload dell' operatore +:
data class Counter(val dayIndex: Int = 0) {
operator fun plus(increment: Int): Counter {
return Counter(dayIndex + increment)
}
}
fun main(args: Array<String>) {
var counter = Counter() + 2
println() // print Counter(dayIndex=2)
}
Operatore in
Operatore | Tradotto in: |
---|---|
a in b | b.contains(a) |
a !in b | !b.contains(a) |
Per in e !in la procedura è sempre la stessa, ma l'ordine degli argomenti è invertito. Ecco comunque un esempio:
data class MyArray<T>(val array: Array<T>) {
operator fun contains(value : T) = array.contains(value)
}
fun main(args: Array<String>) {
val numbers = MyArray(arrayOf(1, 4, 42, -3))
if (4 in numbers) {
println("numbers array contains 4.")
}
}
Operatore is e as
Questi operatore, non sono overloddabili. Il loro funzionamento viene spiegato in una lezione precedente.
Operatore di Indexed (array-like)
Operatore | Tradotto in: |
---|---|
a[i] | a.get(i) |
a[i, j] | a.get(i, j) |
a[i_1, ..., i_n] | a.get(i_1, ..., i_n) |
a[i] = b | a.set(i, b) |
a[i, j] = b | a.set(i, j, b) |
a[i_1, ..., i_n] = b | a.set(i_1, ..., i_n, b) |
Viene scelto il get() o il set() in base se l’operatore = si trova prima o dopo di a[…] ed al numero di parametri passato:
class TwoDimensionalArray<T>(size: Int, init: (x: Int, y: Int) -> T) {
val array: Array<Array<Any?>> = Array(size, { x -> Array<Any?>(size, { y -> init(x, y) }) }) // viene utilizzato un array di Any? per una limitazione della JVM
operator fun get(x: Int, y: Int): T = array[x][y] as T
operator fun set(x: Int, y: Int, value: T) {
array[x][y] = value
}
operator fun get(x: Int) = array[x].map{it as T}.toList()
val size get() = array.size
}
fun main(args: Array<String>) {
val array2d = TwoDimensionalArray(10, { x, y -> 10 })
for (x in 0 until array2d.size) {
for (y in 0 until array2d.size) {
print(array2d[x, y].toString() + " ")
array2d[x, y] = x * y
}
println()
}
println()
for(x in 0 until array2d.size) {
for(item in array2d[x]) {
print(item.toString() + " ")
}
println()
}
}
Realizza questo output:
10 10 10 10 10 10 10 10 10 10
10 10 10 10 10 10 10 10 10 10
10 10 10 10 10 10 10 10 10 10
10 10 10 10 10 10 10 10 10 10
10 10 10 10 10 10 10 10 10 10
10 10 10 10 10 10 10 10 10 10
10 10 10 10 10 10 10 10 10 10
10 10 10 10 10 10 10 10 10 10
10 10 10 10 10 10 10 10 10 10
10 10 10 10 10 10 10 10 10 10
0 0 0 0 0 0 0 0 0 0
0 1 2 3 4 5 6 7 8 9
0 2 4 6 8 10 12 14 16 18
0 3 6 9 12 15 18 21 24 27
0 4 8 12 16 20 24 28 32 36
0 5 10 15 20 25 30 35 40 45
0 6 12 18 24 30 36 42 48 54
0 7 14 21 28 35 42 49 56 63
0 8 16 24 32 40 48 56 64 72
0 9 18 27 36 45 54 63 72 81
Operatore di invoke()
Operatore | Tradotto in: |
---|---|
a() | a.invoke() |
a(i) | a.invoke(i) |
a(i, j) | a.invoke(i, j) |
a(i_1, ..., i_n) | a.invoke(i_1, ..., i_n) |
Viene scelto l’invoke() in base al numero di parametri:
class C {
operator fun invoke(): Unit = println("hello world")
}
fun main(args: Array<String>) {
val c = C()
c()
C()()
}
Operatori con assegnamento
Operatore | Tradotto in: | Non può coesistere con: |
---|---|---|
a += b | a.plusAssign(b) | a.plus(b) |
a -= b | a.minusAssign(b) | a.minus(b) |
a *= b | a.timesAssign(b) | a.times(b) |
a /= b | a.divAssign(b) | a.div(b) |
a %= b | a.remAssign(b) | a.rem(b) |
Codeste funzioni non devono tornare nessun valore, inoltre non deve esistere l'overload dell'operatore + se esiste quello del += (o viceversa), stessa cosa vale per gli altri operatori. A differenza degli altri operatori in questo caso il compilatore:
- Controlla se esiste l'operatore ⭐= (ove ⭐ è uno dei 5 operatori: +, -, *, / e %):
- Se si controlla la validità dell'overload e del tipo di ritorno che deve essere Unit (leggasi void, per chi viene da Java).
- Altrimenti lo trasforma in a = a ⭐ b (ove ⭐ è uno dei 5 operatori: +, -, *, / e %):
- Se non esiste l'operatore viene generato un errore di compilazione.
class C {
operator fun plus(b : C) = C()
operator fun remAssign(b : C) {
println("remAssign")
}
}
fun main(args: Array<String>) {
var v1 = C()
var v2 = C()
var v3 = C()
v1 += v2
v3 %= v1
}
Operatori di identità (verifica il riferimento)
- L'operatore === serve per verificare se due variabili hanno lo stesso riferimento, cioè se condividono l'oggetto puntato;
- L'operatore !==, al contrario, serve per verificare se due variabili non hanno lo stesso riferimento, cioè se non condividono l'oggetto puntato.
fun main(args: Array<String>) {
val number1 = Integer(10) // create new instance
val number2 = Integer(10) // create new instance
val number3 = number1
// check if number1 and number2 are Structural equality
println(number1 == number2) // prints true
// check if number1 and number2 points to the same object
// in other words, checks for Referential equality
println(number1 === number2) // prints false
// check if number1 and number3 points to the same object
println(number1 === number3) // prints true
}
Operatori di uguaglianza e di disuguaglianza
Operatore | Tradotto in: |
---|---|
a == b | a?.equals(b) ?: (b === null) |
a != b | !(a?.equals(b) ?: (b === null)) |
L'operatore == e il relativo != sono speciali: viene tradotta in un'espressione complessa che esegue controlli per il null. Quindi non sempre si viene richiamata la funzione equals():
class Sample(var sample : Int) {
override fun equals(other: Any?): Boolean {
println("Sono dentro equals()")
if(other is Sample)
return this.sample == other.sample
else return false
}
}
fun main(args: Array<String>) {
var v1: Sample? = null
var v2: Sample? = null
var v3: Sample? = Sample(10)
var v4: Sample? = Sample(10)
println("v1 == v2")
v1 == v2
println("v2 == v3")
v2 == v3
println("v3 == v4")
v3 == v4
}
L'output a schermo è il seguente:
v1 == v2
v2 == v3
v3 == v4
Sono dentro equals()
Operatori di confronto
Espressione | Tradotto a |
---|---|
a > b | a.compareTo(b) > 0 |
a < b | a.compareTo(b) < 0 |
a >= b | a.compareTo(b) >= 0 |
a <= b | a.compareTo(b) <= 0 |
Tutti i confronti sono tradotti in chiamate compareTo(), che restituisce degli Int:
class Data {
companion object {
private fun annoBisestile(anno: Int): Boolean {
return (((anno % 4 == 0) && (anno % 100 != 0)) || (anno % 400 == 0))
}
private fun dataValide(giorno: Int, mese: Int, anno: Int): Boolean {
if (anno < 0) return false
if (mese > 12 || mese < 1) return false
if (giorno > 31 || giorno < 1) return false
if ((giorno == 31 && (mese == 2 || mese == 4 || mese == 6 || mese == 9 || mese == 11))) return false
if (giorno == 30 && mese == 2) return false
if ((giorno == 29 && mese == 2) && annoBisestile(anno)) return false
return true
}
}
private var _giorno = 1
var giorno: Int
get() {
return _giorno
}
set(value) {
if (dataValide(value, mese, anno))
_giorno = value
}
private var _mese = 1
var mese: Int
get() {
return _mese
}
set(value) {
if (dataValide(giorno, value, anno))
_mese = value
}
private var _anno = 1900
var anno: Int
get() {
return _anno
}
set(value) {
if (dataValide(giorno, mese, value))
_anno = value
}
constructor()
constructor(giorno: Int, mese: Int, anno: Int) {
if (dataValide(giorno, mese, anno)) {
_giorno = giorno
_mese = mese
_anno = anno
}
}
constructor(giorni: Long) {
this.giorni = giorni
}
var giorni: Long
get() {
var giorni: Long = 0
val annopart = 1900
val mesi = arrayOf(31, if (annoBisestile(_anno)) 29 else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
for (i in annopart until _anno)
giorni += if (annoBisestile(i)) 366 else 365
for (i in 0..(_mese - 2))
giorni += mesi[i]
giorni += _giorno - 1
return giorni
}
set(value) {
val annopart = 1900
var anno = annopart
var mese = 0
var giorni = value
while (giorni >= if (annoBisestile(anno)) 366 else 365) {
giorni -= if (annoBisestile(anno)) 366 else 365
anno++
}
val mesi = arrayOf(31, if (annoBisestile(anno)) 29 else 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
while (giorni >= mesi[mese]) {
giorni -= mesi[mese]
mese++
}
_giorno = giorni.toInt() + 1
_mese = mese + 1
_anno = anno
}
operator fun compareTo(other: Data) : Int {
return this.giorni.compareTo(other.giorni)
}
}
fun main(args: Array<String>) {
val d1 = Data()
val d2 = Data(10, 10, 2000)
val d3 = Data(30000)
val d4 = Data(40, 40, 2000)
val d5 = Data(10, 2, 2010)
println("d1 < d2: " + (d1 < d2))
println("d2 <= d3: " + (d2 <= d3))
println("d3 >= d4: " + (d3 >= d4))
println("d4 > d5: " + (d4 > d5))
}
L'output a schermo è il seguente:
d1 < d2: true
d2 <= d3: false
d3 >= d4: true
d4 > d5: false
Altri operatori letterali
In alcune lezioni abbiamo visto varie keyword che si comportano da operatore: downTo, until, to. Queste in realtà sono delle semplifici funzioni con il modificatore infix. Immagina di utilizzare delle enumerazioni per realizzare un mazzo di carte da Poker:
enum class Suit {
HEARTS,
SPADES,
CLUBS,
DIAMONDS
}
enum class Rank {
ACE, TWO, THREE, FOUR, FIVE,
SIX, SEVEN, EIGHT, NINE,
TEN, JACK, QUEEN, KING
}
data class Card(val rank: Rank, val suit: Suit)
È una struttura molto semplice, fondamentalmente si basa su tre parti:
Suit: Il tipo della carta (HEARTS, SPADES, CLUBS o DIAMONDS).
Rank: Il valore della carta (ACE, TWO, THREE, ..., TEN, JACK, QUEEN, KING).
Card: Una data class composta da Rank e Suit.
Per creare una carta bisogna fare quanto segue:
val card = Card(Rank.QUEEN, Suit.HEARTS)
Ma noi siamo abituati a dire un QUEEN of HEARTS oppure un JACK of SPADES. Ma l'operatore of non è un operatore esistente. Ma Kotlin ci permette di realizzare nuovi operatori, come è già stato fatto per alcune keyword (downTo, until, to...) tramite i metodi con il modificatore infix. Come per gli operatori, questi metodi devono essere metodi di istanza di una classe e/o metodi di estenzione:
enum class Rank {
TWO, THREE, FOUR, FIVE,
SIX, SEVEN, EIGHT, NINE,
TEN, JACK, QUEEN, KING, ACE;
infix fun of(suit: Suit) = Card(this, suit)
}
Grazie a questo modificatore infix siamo in grado di riscrivere lo Snippet 2.12 in maniera molto più naturale:
val card = Rank.ACE of Suit.HEARTS
Ma ancora non è naturale, come noi vogliamo e poi ci manca un metodo per generare le 40 carte. Adesso usiamo gli import per evitare di scrivere Rank. e Suit.:
import Rank.*
import Suit.*
enum class Suit {
HEARTS,
SPADES,
CLUBS,
DIAMONDS
}
enum class Rank {
TWO, THREE, FOUR, FIVE,
SIX, SEVEN, EIGHT, NINE,
TEN, JACK, QUEEN, KING, ACE;
infix fun of(suit: Suit) = Card(this, suit)
}
data class Card(val rank: Rank, val suit: Suit) {
companion object {
private var array: Array<Card>? = null
fun values() : Array<Card> {
if(array == null) {
val list = mutableListOf<Card>()
for (suit in Suit.values()) {
Rank.values().mapTo(list) { rank -> rank of suit }
}
array = list.toTypedArray()
}
return array ?: emptyArray()
}
}
}
fun main(args: Array<String>) {
val list = Card.values().toMutableList()
list.remove(ACE of HEARTS)
list.remove(TEN of DIAMONDS)
list.remove(JACK of CLUBS)
list.remove(SEVEN of SPADES)
}