Introduzione a Kotlin: Parte 2 – Gli oggetti

I

Benvenuti nella seconda parte Kotlin – Gli oggetti della serie sul Kotlin. Potete trovare qui la prima parte, mentre invece in questo articolo uno sguardo generale al linguaggio per i programmatori Java/OO.

Kotlin: Gli oggetti

Nell’articolo precedente è stata utilizzata una volta questa espressione, “oggetto“, per riferirsi a qualsiasi cosa con cui abbiamo a che fare.

Ciò differisce fortemente se non totalmente, dai linguaggi procedurali, dove non esistono oggetti (C), oppure esistono ma con un set di caratteristiche ridotto rispetto a quello dei linguaggi OOP (come Javascript che offre oggetti e prototipi).

Che cosa è un oggetto?

Ma innanzitutto, che cosa è un oggetto?

Un oggetto è un’entità che incapsula uno stato e un comportamento.

  • Stato: il contenuto in memoria dell’oggetto. Di un quadrato si può tener conto della grandezza del lato (1 dato); rettangolo, base e altezza (2 dati); persona, potenzialmente tantissimi dati differenti.
  • Comportamento: Un oggetto non solo deve essere dotato di uno stato, ma anche di un set di operazioni fornite dall’oggetto stesso.

Gli oggetti sono presenti anche in linguaggi non orientati agli oggetti (come Javascript appunto), ma ci sono importanti vincoli aggiuntivi a questi, per poter definire un linguaggio orientato agli oggetti.

I vincoli saranno visti nel dettaglio più in seguito nell’articolo. Adesso vediamo come definire un oggetto in Kotlin.

Come funzionano gli oggetti in Kotlin

L’Object-Model in Kotlin, alla base, è molto simile a quello in Java.

Come menzionato sopra, un oggetto è dotato di uno stato e di un comportamento. Se avete solamente uno stato, allora anche se in Kotlin avete comunque un oggetto (vedremo che c’è della logica comune a tutti gli oggetti in Kotlin), fondamentalmente avete un accumulo di dati.

Ad esempio un punto nello spazio 2D, sarà dotato di una coppia di coordinate (x,y) che rappresentano la sua posizione.

È importante osservare che in Kotlin, le vostre variabilicostanti (introdotte nel capitolo precedente), non corrisponderanno intrinsecamente all’oggetto che rappresentano, quanto piuttosto a un suo riferimento.

L’oggetto sarà altrove in memoria, e in Kotlin non ne avrete libero e indiscriminato accesso per alterarli byte per byte.

var variabile1 = Oggetto() // Oggetto #1
var variabile2 = Oggetto() // Oggetto #2
var variabile3: Oggetto
Oggetto() // Oggetto #3

 

Riferimenti vs Oggetti
Immagine 1. Riferimenti vs Oggetti

La differenza sta fondamentalmente nel fatto che, copiando quindi una variabile, non copieremo il suo oggetto, ma il suo riferimento.

var variabile1 = Oggetto() // Oggetto #1
var variabile2 = Oggetto() // Oggetto #2
Oggetto()  // Oggetto #3
var variabile3 = variabile1

Rendendo dunque lo scenario così:

Copia di riferimento
Immagine 2. Copia di riferimento

Supponendo che Oggetto sia modificabile, sarà indifferente modificare l’oggetto referenziato da variabile1 o variabile3, in quanto si riferiscono allo stesso oggetto.

Modificare invece il valore di variabile1variabile3, non modificherà l’oggetto. In questo caso, con valore, intendiamo il valore del riferimento.

Inoltre, se un oggetto non è riferito da nessuna variabile (come Oggetto #3), potrà essere eliminato in qualsiasi momento dal Garbage Collector (GC), componente che si occupa di liberare la memoria in caso di necessità.

Un oggetto (anonimo) in Kotlin

Vediamo dunque come dichiarare un oggetto (anonimo) in Kotlin.

var punto = object {
    var x = 0
    var y = 0
}

punto è un riferimento ad oggetto anonimo. Questo oggetto ha due variabili di tipo intero, x ed y.

Si dice che è un oggetto anonimo in quanto il suo tipo è definito dall’oggetto stesso. Infatti, nella parte 1 del tutorial, ricorderete che si poteva sempre associare un tipo all’oggetto, ad esempio var messaggio: String = "Ciao", dove String è il tipo (e lo è anche se non lo si denota esplicitamente), e "Ciao" è l’oggetto.

Una classe e un oggetto istanziato del suo tipo

Se vogliamo o abbiamo bisogno di generalizzare la definizione di punto, per poter avere più oggetti dello stesso tipo, o una collezione (come una lista) di oggetti punto, dobbiamo definire la sua classe. Una classe, semanticamente, rappresenta una famiglia di oggetti con caratteristiche simili. Ad esempio un numero intero (ad esempio 1), può essere l’oggetto, o l’istanza. Invece Int è la sua classe, ovvero la famiglia di tutti i numeri interi.

Allo stesso modo, l’oggetto di prima mappava un punto, dunque una coppia di numeri (0, 0). E in analogo modo, è possibile definire una famiglia di tutte le coppie di numeri.

class Point(var x: Int, var y: Int)

Questa definizione sancisce che esiste la classe Point (punto), e che ogni punto (istanza/oggetto) si caratterizza da un attributo x e un attributo y.

Per istanziare un oggetto di questo tipo, è sufficiente scrivere

var point = Point(0,0)

E per accedere ad x ed y, rispettivamente

println(point.x)
println(point.y)

point.x = 10

Non si potrà agire su x ed y a livello di classe, vale a dire Point.x o Point.y, in quanto Point è solo una generalizzazione di tutti i possibili oggetti Punto.

Sicché quest’articolo si pone di spiegare gli oggetti e non le classi, si proseguirà con la notazione di oggetto anonimo (i concetti spiegati sugli oggetti sono gli stessi), e si mostrerà perché generalmente non conviene e soprattutto quali sono le implicazioni e soprattutto i limiti nell’usarla al posto di definire una classe. Tenere a mente che quando si parla semplicemente di oggetti e non di oggetti anonimi, quanto viene detto vale per entrambi.

Oggetti e riferimenti

Riprendendo l’Immagine 2, verifichiamo la differenza tra riferimenti e oggetti.

Creiamo tre variabili “punti”, due nuove e una terza che “copia” la prima.

fun main(args: Array<String>) {
    var punto1 = object {
        var x = 0
        var y = 0
    }

    var punto2 = object  {
        var x = 0
        var y = 0
    }

    var punto3 = punto1

    println("Punto 1 (${punto1.x}, ${punto1.y})")
    println("Punto 2 (${punto2.x}, ${punto2.y})")
    println("Punto 3 (${punto3.x}, ${punto3.y})")
}

Come ci si può aspettare, tutti i punti sono impostati a 0,0.

Punto 1 (0, 0)
Punto 2 (0, 0)
Punto 3 (0, 0)

E adesso, prima della fine del main, modifichiamo l’oggetto di punto3, dopo di ché ristampiamo tutti i punti.

punto3.x = 50
punto3.y = 50

println("Punto 1 (${punto1.x}, ${punto1.y})")
println("Punto 2 (${punto2.x}, ${punto2.y})")
println("Punto 3 (${punto3.x}, ${punto3.y})")

E l’output sarà il seguente

Punto 1 (50, 50)
Punto 2 (0, 0)
Punto 3 (50, 50)

Infatti, punto3 si riferisce allo stesso oggetto in memoria a cui si riferisce punto1.

Differenza di tipi

Si è detto nella parte 1 della guida, che Kotlin è un linguaggio fortemente tipizzato, e si conosce esattamente a tempo di compilazione il tipo di una variabile.

È importante osservare dunque che non tutti gli oggetti anonimi sono uguali anche se può sembrare:

var punto1 = object { // punto1 variabile ad oggetto anonimo di un certo tipo
    var x = 0
    var y = 0
}

var punto2 = object  { // punto1 variabile ad oggetto anonimo di un altro tipo
    var x = 0
    var y = 0
}

punto2 = punto1 // Error: Type Mismatch
// punto2 e punto1 sono di tipi differenti.

Anche se di queste variabili infatti specificate solamente l’oggetto e non il tipo, un tipo altrettanto anonimo ce l’hanno comunque assegnato (come le stringhe e i numeri), ed essendo punto1punto2 di tipi diversi, non potete fare quest’assegnazione.

Ma dove credevate di essere, in JavaScript? :]

Per compensare questo problema, è possibile dedicare la dichiarazione dell’oggetto a una funzione-factory, anch’essa di tipo implicito, ma uguale per tutte le chiamate.

// Funzione di tipo implicito definito dall'oggetto anonimo
private fun makePoint(px: Int, py: Int) = object { 
    var x = px
    var y = py
}

fun main(args: Array<String>) {
    var punto1 = makePoint(0, 0)
    var punto2 = makePoint(0, 0)

    punto2 = punto1 // Consentito. La funzione makePoint ha un unico tipo di ritorno
}

Logica dell’oggetto

Prima si è detto che un oggetto è caratterizzato da uno stato e un comportamento. Il nostro tipo locale di punto ha uno stato (x,y) ma non un comportamento (operazioni). Un semplice comportamento potrebbe essere un metodo print per stampare l’oggetto in maniera semplificata.

// Codice analogo all'esempio di prima dei 3 punti, più pulito
private fun makePoint(px: Int, py: Int) = object {
    var x = px
    var y = py

    fun print() = println("($x, $y)")
}

fun main(args: Array<String>) {
    var punto1 = makePoint(0,0)
    var punto2 = makePoint(0, 0)

    punto1.print()
    punto2.print()
    punto3.print()
    
    var punto3 = punto1
    punto3.x = 50
    punto3.y = 50

    punto1.print()
    punto2.print()
    punto3.print()
}

Adesso, x ed y rappresentano lo stato di un punto. print invece un comportamento (o operazione)

Nota: distinzione tra riferimento e valore nella memoria

È importante tenere a mente, che la differenza tra copia di riferimento e copia di valore non discende automaticamente dal tipo di memoria con cui si ha a che fare (stack o heap). In certi linguaggi infatti, è possibile riferirsi a una variabile nella memoria stack e modificarla attraverso un’altra variabile (non possibile in Kotlin), mentre è altrettanto possibile copiare direttamente della memoria dall’heap allo stack o dall’heap all’heap (non possibile in Kotlin). Il motivo è che in Kotlin questo concetto è astratto. Ci sono i riferimenti (le variabili) e gli oggetti associati in memoria. Un riferimento può riferirsi solo a un oggetto. Non ci sono riferimenti a riferimento.

Oggetti immutabili

Detto questo, è importante distinguere l’immutabilità di un oggetto dall’immutabilità di una variabile.

Osservate l’immagine 1 e 2 di prima.

Se una variabile è immutabile, vuol dire che non è possibile cambiare l’oggetto a cui si riferisce, ovvero la “freccia” nell’immagine.

Questo però, non vuol dire che l’oggetto a cui la variabile si riferisce non possa essere mutato. Se così invece dovesse essere (cioè oggetto che non può essere mutato), allora l’oggetto si dice immutabile.

Generalmente si preferiscono oggetti immutabili, in quanto sono meno proni agli errori. I tipi di base numerici e le stringhe infatti, sono immutabili. Se una vostra variabile si riferisce alla stringa “Ciao”, non potrete alterarne il valore tramite un’altra variabile (come abbiamo fatto con punto1 e punto3). L’unico modo per cambiare la stringa, sarà crearne una nuova, e assegnarla alla variabile di prima.

Ecco una differenza sempre riprendendo l’esempio dei punti, tra variabili e costanti e oggetti immutabili o mutabili. Ci sono 4 possibili casi:

private fun makePoint(px: Int, py: Int) = object {
    // x e y sono var
    var x = px
    var y = py

    fun print() = println("($x, $y)")
}

private fun makeImmutablePoint(px: Int, py: Int) = object {
    // Notare che x e y sono val
    val x = px 
    val y = py

    fun print() = println("($x, $y)")
}

fun main(args: Array<String>) {
    val origin = makePoint(0,0)
    var p1 = makePoint(1, 1) // Variabile ad oggetto mutabile
    p1.y = 10 // Consentito. Modifica dell'oggetto (y è var)
    p1 = origin // Consentito. Modifica della variabile

    val p2 = makePoint(2, 2) // Costante ad oggetto immutabile
    p2.x = 30 // Consentito. Non si sta modificando p2 ma l'oggetto a cui punta (x è var)
    //p2 = origin // Errore. p2 è val e non si può ri-assegnare.

    val p3 = makeImmutablePoint(3,3) // Costante ad oggetto immutabile
    // p3.x = 2 // Errore. L'oggetto p3 è immutabile (x è val)

    var p4 = makeImmutablePoint(4, 4) // Variabile ad oggetto immutabile
    // p4.x = 10 // Errore. L'oggetto di p4 è immutabile (x è val)
    p4 = p3 // Consentito. p3 è variabile (var).
}

Se un oggetto è immutabile, l’unico modo per avere un oggetto diverso è crearne un altroassegnare un altro oggetto esistente. Invece se un oggetto è mutabile, è possibile modificare lo stesso oggetto.

Generalmente è consigliato lavorare in un contesto mutabile quando vanno fatte diverse alterazioni a un oggetto (ad esempio in fase di costruzione per parti), così da non crearne sempre di nuovi, e poi esportare l’oggetto finito come immutabile, in quanto conferisce molta più solidità alla struttura del programma. Avere infatti oggetti mutabili implica la possibilità che vengano modificati ovunque siano passati.

Limiti degli oggetti anonimi

Diciamo di voler scrivere una funzione che calcoli la distanza tra due punti. Questa funzione deve accettare dunque due variabili di un tipo che rappresenti un punto, e sapere a tempo di compilazione, che la variabile ha sia una proprietà x, sia una proprietà y.

Con l’uso delle classi, prima avremmo scritto qualcosa come

class Point(var x: Int, var y: Int)

fun dist(p1: Point, p2: Point): Double {
    return sqrt(Math.pow((p1.x - p2.x).toDouble(), 2.0) + Math.pow((p1.y - p2.y).toDouble(), 2.0))
}

Semplificabile in questo, se ammettiamo x ed y come Double (numeri reali) anziché Int.

class Point(var x: Double, var y: Double)

fun dist(p1: Point, p2: Point): Double {
    return sqrt(Math.pow(p1.x - p2.x, 2.0) + Math.pow(p1.y - p2.y, 2.0))
}

(In seguito come vedremo, ci sono modi per semplificare molto di più la notazione in Kotlin)

Per utilizzare una convenzione ad oggetti, potremmo inoltre implementare la funzione dist direttamente nella classe.

class Point(var x: Int, var y: Int) {
    fun print() = println("($x, $y)")
    fun dist(right: Point): Double {
        return sqrt(Math.pow((this.x - right.x).toDouble(), 2.0) + Math.pow((this.y - right.y).toDouble(), 2.0))
    }
}

fun main(args: Array<String>) {
    println(Point(0, 0).dist(Point(30, 3))) // 30.14962686336267
}

Con gli oggetti anonimi, il tipo non è dotato di un nome, impedendo dunque di accettare in una funzione una variabile del loro tipo. Tuttavia, è possibile specificare un’interfaccia, a cui un oggetto anonimo si conformi. Nel capitolo successivo, vedremo questo concetto nell’ereditarietà e polimorfismo.

Un’interfaccia non è dotata di logica ma specifica solo delle operazioni. Ecco come potremmo riprodurre l’esempio di sopra.

// Interfaccia che sancisce che un punto deve avere almeno due proprietà, x ed y, 
// un metodo print e uno per calcolare la distanza tra punti
interface MutablePoint {
    var x: Int
    var y: Int

    fun print()
    fun dist(right: MutablePoint): Double
}

// Conformazione all'interfaccia
private fun makePoint(px: Int, py: Int) = object: MutablePoint {
    override var x = px
    override var y = py

    override fun print() = println("($x, $y)")
    override fun dist(right: MutablePoint): Double {
        return sqrt(Math.pow((this.x - right.x).toDouble(), 2.0) + Math.pow((this.y - right.y).toDouble(), 2.0))
    }
}

// Forma alternativa (come prima con Point)
fun dist(p1: MutablePoint, p2: MutablePoint): Double {
    return sqrt(Math.pow((p1.x - p2.x).toDouble(), 2.0) + Math.pow((p1.y - p2.y).toDouble(), 2.0))
}

Nella forma alternativa non accetteremo un oggetto Point anonimo semplicemente, ma un qualsiasi oggetto che è conforme all’interfaccia, incluso il nostro anonimo.

L’uso delle interfacce consente dunque flessibilità, ma quello degli oggetti anonimi meno.

L’altra grossa limitazione infatti, sarà che se scrivete una libreria o una componente nel vostro stesso progetto, gli oggetti anonimi perderanno i dettagli della loro dichiarazione. Ad esempio se makePoint non avesse il modifier di visiblità private, assumendo che non steste utilizzando l’interfaccia, non sarebbe possibile utilizzare le proprietà x ed y, né i metodi printdist, in quanto l’oggetto sarebbe esportato con il tipo Any, cioè “qualsiasi oggetto” (e qualsiasi oggetto non è dotato di proprietà x ed y intere né dist/print).

La visibilità private in questo caso, significa visibile nel file di codice di dichiarazione, non accessibile altrove. Quindi il tipo è visibile solo nello stesso file o nella stessa classe se interno a un’altra classe.

Dunque per avere tipi pubblici o visibili a porzioni di codice oltre il singolo file/classe, utilizzate le classi!

Le classi in Kotlin

Nel prossimo articolo si parlerà molto di più sulle classi, su quello che permettono di fare in un linguaggio Orientato agli Oggetti, e la capacità di astrazione, information hiding e flessibilità che danno, più aspetti aggiuntivi, come delle agevolazioni, che contraddistinguono le classi in Kotlin rispetto ad altri linguaggi OOP.

Ad esempio ammettendo:

val p = Point(0, 0)
val p2 = Point(30, 3)

println(p dist p2) // 30.14962686336267

Esercizio

Come esercizio del capitolo, dichiarare in Kotlin la classe Rectangle, dotata di un’origine (punto), una larghezza e un’altezza. Utilizzare come tipo numerico, Int.

Scrivere una classe RectangleDrawer, con un metodo draw che accetta un rettangolo, che stampi su uno stream qualsiasi (come la console) il rettangolo (solo contorno) con un carattere passato per parametro.

(Consiglio: utilizzare  print per stampare carattere per carattere. Utilizzare  println, oppure il carattere '\n' per andare a capo)

Esempio di utilizzo:

val rect = Rectangle(origin = Point(1,1), width = 5, height = 3)
val drawer = RectangleDrawer()

drawer.draw(rect, '#')

Esempio di output:

               (prima riga vuota perché si parte da (1,1))
 #####
 #   #
 #####

Esercizio precedente (soluzione)

Il capitolo precedente era stato dato un esercizio sul calcolo del fattoriale, iterativamente e ricorsivamente. La soluzione è questa:

fun fattorialeRicorsivo(n: Long): Long {
    n <= 1 && return 1
    return n * fattorialeRicorsivo(n-1)
}

fun fattorialeIterativo(n: Long): Long {
    var factorial = 1L
    for (i in 1..n) {
        factorial *= i
    }
    return factorial
}

Non essendo stato spiegato ancora l’if nella parte 1, l’esercizio si può svolgere con l’operatore && che consente grazie allo short-circuit di implementare il caso base della ricorsione. Infatti se n > 2, allora il resto dell’espressione (return 1) non viene valutato!

A proposito di me

Ruggiero Altini

Studente di Informatica, sviluppatore iOS e Android, appassionato di algoritmi, studia per Big Data e Data Science. Ha usato e usa estensivamente C++, Java, Kotlin e Swift, ma conosce anche Python e Javascript.

Gli articoli più letti

Articoli recenti

Commenti recenti