Introduzione a Kotlin

II parte: La sintassidi

Introduzione

Il primo articolo di questa serie ha introdotto l’origine e gli obiettivi di Kotlin, parlando brevemente dei campi di applicazione e di come iniziare ad utilizzare il linguaggio. Andiamo ora a vedere più in dettaglio i costrutti principali di Kotlin attraverso una serie di esempi che ci aiuteranno ad imparare la sintassi.

 

Hello World

Come detto in precedenza, Kotlin supporta first class functions; iniziamo quindi definendo la più semplice delle funzioni, la classica Hello World. Facciamo pure un passo avanti definendo una funzione hello che accetti un parametro name e restituisca "Hello <name>". Questa è la prima versione:

fun hello(name: String) : String {
    return "Hello $name"
}

Usiamo queste tre righe di codice per introdurre le basi della sintassi:

  • la parola chiave fun definisce una funzione o il metodo di una classe;
  • i parametri sono elencati tra parentesi tonde con la sintassi <nome del parametro>: <tipo del parametro>
  • il tipo di ritorno della funzione segue la definizione dei parametri;
  • il punto e virgola (;) alla fine delle istruzioni è opzionale;
  • Kotlin offre la string interpolation ("Hello $name"), ossia sostituisce il valore del parametro name nella stringa restituita dalla funzione (il carattere $ invoca la string interpolation).

Possiamo utilizzare la Kotlin REPL per eseguire velocemente questo primo esempio. Copiamo la definizione della funzione sulla nostra REPL, e poi digitiamo

hello(“World”)

Quando eseguiamo la REPL (con IntelliJ IDEA: CTRL + Enter, o Command + Enter su Mac), il risultato è quello che ci aspettiamo:

Hello World

 

Type inference

A differenza di Java che richiede di esplicitare in ogni caso il tipo di una variabile, o il tipo di ritorno di una funzione, Kotlin cerca per quanto possibile di determinarlo in base all’analisi del codice, permettendo allo sviluppatore di scrivere codice molto più conciso.

Tornando alla nostra funzione hello, è evidente che il tipo di ritorno possa essere solo una stringa. Evitiamo di dichiarare ciò che già è evidente al compilatore, e semplifichiamo la definizione:

fun hello(name: String) = “Hello $name”

Possiamo omettere il tipo di ritorno quando Kotlin è in grado di determinarlo autonomamente e senza ambiguità; allo stesso tempo, visto che il corpo del metodo è costituito da una sola istruzione, possiamo tranquillamente anche omettere le parentesi graffe e la parola chiave return.

Analogamente, quando dichiariamo e inizializziamo il valore di una variabile, possiamo omettere il tipo se questo è chiaro dal contesto:

val greeting = “Hello”
val amount = 1

Kotlin automaticamente assocerà alla variabile greeting il tipo String e alla variabile amount il tipo Int.

 

La funzione main

Abbandoniamo ora la REPL e integriamo la funzione hello in un’applicazione Kotlin. Niente di più facile: creiamo un file Hello.kt con il seguente contenuto:

fun hello(name: String) = "Hello $name"
fun main(args: Array<String>) {
    println(hello("world"))
}

La funzione main rimpiazza il metodo statico omonimo utilizzato in Java per eseguire un’applicazione. Si noti anche la funzione println che, come si può facilmente intuire, stampa una stringa sulla console.

 

Classi in Kotlin

Negli esempi precedenti abbiamo definito delle funzioni ma, come notato nel primo articolo di questa serie, Kotlin supporta anche il paradigma di programmazione orientato agli oggetti. Creiamo una classe per definire il concetto di persona, modificando Hello.kt come segue:

class Person(private val first: String, private val last: String) {
    fun fullName() = "$first $last"
}
fun hello(person: Person) = "Hello ${person.fullName()}"
fun main(args: Array<String>) {
    println(hello(Person(last = “Duck", first = “Donald")))
}

Per prima cosa vediamo che è possibile omettere la visibilità della classe, che è public di default. Il costruttore è definito direttamente dopo il nome della classe (tra le parentesi); i parametri possono essere definiti come val (immutabili, equivalenti a final in Java) oppure var (mutabili).

All’interno del corpo della classe, possiamo definire ulteriori variabili o metodi, utilizzando la stessa sintassi che abbiamo visto per le funzioni.

Abbiamo anche modificato il metodo main il modo che crei un oggetto di tipo Person e invochi la funzione hello su di esso:

  • la parola chiave new non è necessaria a instanziare un oggetto;
  • i parametri (del costruttore, di un metodo o di una funzione) possono essere specificati attraverso il loro nome [1]; è comunque ancora possibile specificare i parametri nell’ordine in cui vengono definiti dal costruttore e omettendo il nome.

 

Null safety

Una delle funzionalità più spesso menzionate di Kotlin è la sua capacità di ridurre drasticamente gli errori causati da riferimenti a oggetti nulli attraverso i nullable types [2]. Continuiamo a lavorare sul nostro esempio aggiungendo il parametro middle al costruttore della classe Person:

class Person(private val first: String,
            private val middle: String,
            private val last: String)
{ ...

Questa modifica ci costringe a cambiare l’istanziazione dell’oggetto nella funzione main; non tutti hanno un secondo nome e, nel nostro caso, non possiamo far altro che impostare middle a null.

Person(last = “Duck", first = “Donald", middle = null)

Il compilatore ci segnala prontamente un errore: Kotlin non permette di definire un parametro di tipo String con un valore nullo; middle infatti può essere impostato a null solo se di tipo nullable. È possibile definire un parametro o una variabile come nullable semplicemente aggiungendo il carattere ? dopo il tipo, come nell’esempio seguente:

class Person(private val first: String,
            private val middle: String?,
            private val last: String)
{ ...

Dopo aver modificato il costruttore, il nostro codice compila, ma possiamo renderlo ancora più elegante introducendo un valore di default per il parametro middle:

class Person(private val first: String,
            private val middle: String? = null,
            private val last: String)
{ ...

che ci consentirà di omettere del tutto la dichiarazione di middle nel caso vogliamo che sia nullo.

Person(last = “Duck", first = “Donald”)

Vantaggi e conseguenze dei nullable type

Dichiarare un parametro come middle con un nullable type ha diversi vantaggi e conseguenze. Innanzitutto rende il programmatore cosciente del fatto che un parametro possa essere nullo; a questo punto si può decidere ad esempio di forzare esplicitamente una NullPointerException se il valore è effettivamente nullo, aggiungendo i caratteri !! dopo il nome del parametro

// la seguente istruzione lancia una NullPointerException se middle è null
println(middle!!)

Si può invece scegliere di controllare se il valore è nullo usando una if clause oppure il cosiddetto Elvis operator ?:

// myValue è uguale a middle se non nullo, altrimenti a stringa vuota
val myValue = middle ?: “”

Infine, si può decidere di invocare qualsiasi metodo del parametro, utilizzando una safe call (aggiungendo il carattere ? dopo il nome del parametro); la safe call restituirà null anche se il parametro stesso è nullo, invece di una NullPointerException come avverrebbe in Java.

// safe call sull’oggetto middle che ritorna la sua lunghezza o null se l’oggetto è nullo
middle?.length

Riscriviamo il nostro esempio in modo da utilizzare una safe call per stampare solo l’iniziale del secondo nome

class Person(private val first: String,
            private val middle: String? = null,
            private val last: String)
{
    fun completeName() = "$first ${middle?.substring(0, 1) ?: ""} $last"
}
fun hello(person: Person) = "Hello ${person.completeName()}"
fun main(args: Array<String>) {
    println(hello(Person(last = "Duck", first = "Donald")))
}

Quando eseguiamo il codice, vediamo la safe call in azione su un parametro nullo: l’invocazione del metodo substring sulla referenza nulla non lancerà una NulPointerException ma semplicemente restituirà null.

 

Data classes e uguaglianza tra oggetti

Andiamo ora a parlare di uguaglianza tra oggetti provando ad eseguire la seguente funzione:

fun main(args: Array<String>) {
    val person1 = Person(last = "Duck", first = "Donald")
    val person2 = Person(last = "Duck", first = "Donald")
    println("Person1 e Person2 sono uguali (==) --> ${person1 == person2}")
    println("Person1 e Person2 sono uguali (===) --> ${person1 === person2}")
}

L’applicazione crea due variabili (person1 e person2) immutabili, e le confronta utilizzando l’operatore == (che è equivalente ad invocare il metodo Java equals) e l’operatore === (equivalente all’uguaglianza tra istanze, o == in Java). Il risultato è il seguente:

Person1 e Person2 sono uguali (==) --> false
Person1 e Person2 sono uguali (===) --> false

Niente di inaspettato finora: abbiamo definito due oggetti distinti e verificato che, in effetti, lo sono! In molti contesti e applicazioni vogliamo però avere un concetto di uguaglianza logica; in questo caso, possiamo per esempio assumere che due persone che abbiano stesso nome (first, middle e last) siano in effetti la stessa persona. In Java dovremmo ridefinire il metodo equals (e il complementare hashCode) con la logica opportuna; in Kotlin è sufficiente definire Person come una data class (si noti la parola chiave data di fronte alla definizione della classe):

data class Person(private val first: String,
            private val middle: String? = null,
            private val last: String)
{
    fun completeName() = "$first ${middle?.substring(0, 1) ?: ""} $last"
}

Eseguiamo di nuovo l’applicazione e il risultato è ciò che ci aspettiamo:

Person1 e Person2 sono uguali (==) --> true
Person1 e Person2 sono uguali (===) --> false

Data class

Una data class [3] è, come il nome suggerisce, una classe che è pensata come “contenitore” dei dati di un entità; ciò non significa che non possa avere metodi — è una normale classe a tutti gli effetti — ma tutti i suoi parametri devono essere immutabili. Offre inoltre i seguenti vantaggi

  • metodi equals e hashCode definiti automaticamente in funzione dei valori dei suoi parametri;
  • un metodo copy()generato automaticamente per clonare l’oggetto;
  • il metodo toString() che mostra il nome della classe e il valore di tutti i suoi parametri.

 

Liste, mappe e iterazioni

Concludiamo questa carrellata sui costrutti di base di Kotlin con un esempio che illustra come creare e operare su liste e mappe. Kotlin mette a disposizione delle funzioni standard per creare liste (listOf<Type>(elements)), mappe (mapOf<Type, Type>(pairs)) e set (setOf<Type>(elements)) [4].

Liste

Creiamo una lista di persone e generiamo una stringa di saluto per ciascuna di esse:

fun main(args: Array<String>) {
    val names = listOf(
    Person(first = "Donald", last = "Duck"),
    Person(first = "Mickey", last = "Mouse"),
    Person(first = "Scrooge", last = "McDuck")
    )
    println(names.map(::hello))
}

La definizione della lista attraverso la funzione listOf è immediata: la funzione accetta come parametri gli elementi della lista stessa; è possibile omettere il tipo generico visto che il compilatore lo determinerà dagli elementi inseriti.

Le liste in Kotlin offrono tutti i metodi a cui siamo già abituati — analoghi a quelli introdotti nella Collection API in Java 8 — come filter, map, flatMap, forEach… Inoltre l’istruzione ::hello non è altro che la sintassi usata da Kotlin per referenziare una funzione: anche qui la similitudine con Java è evidente.

Il risultato dell’esecuzione dell’applicazione è il seguente:

[Hello Donald Duck, Hello Mickey Mouse, Hello Scrooge McDuck]

Mappe

La definizione di mappe utilizza una sintassi un po’ insolita, dove all’interno del metodo mapOf ogni coppia chiave-valore è definita con la sintassi <chiave> to <valore> come nell'esempio:

fun main(args: Array<String>) {
    val names = mapOf(
    "Donald" to Person(first = "Donald", last = "Duck"),
    "Mickey" to Person(first = "Mickey", last = "Mouse"),
    "Scrooge" to Person(first = "Scrooge", last = "McDuck")
    )
    println(names.map {
        (key, value) -> "$key è ${value.completeName()}"
    })
}

Contrariamente a quanto si potrebbe pensare, to non è una parola chiave del linguaggio ma una infix function [7], ossia una funzione di un solo argomento che non necessita di punto e parentesi per essere invocata. Ciò significa che

"Donald" to Person(first = "Donald", last = "Duck”)

è equivalente a

“Donald”.to(Person(first = "Donald", last = "Duck”))

Il tipo di ritorno del metodo to [5] è un oggetto di tipo Pair: quindi una mappa può essere pensata semplicemente come una lista di oggetti di tipo Pair.

 

Conclusioni

In questo secondo articolo della serie abbiamo toccato velocemente le basi della sintassi di Kotlin. La lista di esempi e concetti presentati rappresenta solo parte delle conoscenze necessarie per programmare in modo produttivo in Kotlin, ma è sufficiente per iniziare a sperimentare con il linguaggio. Per maggiori dettagli, la documentazione di Kotlin online è molto esaustiva e in particolare la sezione dedicata agli idiomi del linguaggio [6] offre un ottimo punto di partenza.

Moltissime delle conoscenze accumulate in anni di programmazione in Java sono completamente trasferibili a Kotlin, a partire dall’uso degli strumenti per la compilazione come Maven o Gradle, dall’utilizzo di librerie esterne e tool per il testing.

Nel prossimo articolo metteremo in pratica quanto appreso nelle sezioni precedenti per sviluppare la prima applicazione in Kotlin.

 

Condividi

Pubblicato nel numero
238 aprile 2018
Filippo Diotalevi si occupa di consulenza IT da circa 20 anni. Dopo la laurea in Ingegneria Informatica, ha lavorato in Italia e Germania, e si è stabilito da alcuni anni a Londra dove continua ad essere attivo nella progettazione e sviluppo di applicazioni per la Java Virtual Machine. È autore…
Articoli nella stessa serie
Ti potrebbe interessare anche