Dopo aver parlato di cos’è, com’è nato, e dov’è usato Kotlin in quest’articolo (con uno sguardo generale), possiamo guardare dettaglio per dettaglio com’è strutturato il linguaggio.
KOTLIN: Le basi
Hello World
In Kotlin, il classico programma “Hello World”, consisterà di queste 3 semplici righe di codice:
fun main(args: Array<String>) { println("Hello world!") }
Qui possiamo vedere:
- Una funzione main, con un parametro di tipo Array di stringhe
- Una chiamata a funzione println
Non c’è nient’altro da aggiungere (import, classi, ecc.). Questo è un programma completo che compilerà così com’è scritto.
Variabili e costanti
Le variabili e le costanti in Kotlin devono essere esplicitamente dichiarate prima di essere utilizzate. Le costanti si dichiarano con la keyword val
, mentre le variabili con var
.
Ecco un esempio con una variabile e una costante in Kotlin:
val limiteTentativiLogin = 5 var tentativiEseguiti = 0
limiteTentativiLogin
è costante, e dunque non è modificabile. tentativiEseguiti
invece è variabile, dunque potremo aggiornarlo ogni volta che l’utente tenta il login.
Il tipo di variabili e costanti è definito a compile-time, e non è modificabile dopo. Il tipo di una variabile dev’essere dunque definito al momento della dichiarazione della stessa.
Se la variabile si dichiara assegnandole un valore, il tipo si dice inferito dal compilatore, ovvero il compilatore (ed anche noi, spero), conoscerà esattamente il tipo che deve assumere la variabile, in quanto conosce il tipo del valore.
Se la variabile si dichiara senza assegnarle un valore, il tipo non può essere inferito, e andrà dunque esplicitato.
val limiteTentativiLogin: Int var tentativiEseguiti: Int
Alternativamente, è possibile scrivere esplicitamente il tipo per chiarezza aggiuntiva anche quando gli si assegna un valore
var messaggio: String = "Ciao mondo!"
Funzioni
All’inizio dell’articolo abbiamo visto come si dichiara una semplice funzione che accetta un parametro e non ha nessun valore di ritorno.
Una funzione può anche essere aparametrica, cioè che non accetta alcun parametro, oppure può avere parametri e un valore di ritorno. Il modo in cui si denota il tipo di ritorno, è analogo a quello delle variabili.
fun sum(x: Int, y: Int): Int { return x + y }
Questa funzione accetta due numeri interi, e restituisce la loro somma in output.
Anche qui il passaggio di parametri può essere implicito o esplicito. Implicito vuol dire che passeremo i parametri semplicemente nell’ordine in cui sono immaginati
sum(2, 3) // 5
Ma si può altrettanto invocare in differenti modi, rendendo espliciti i parametri con cui la stiamo invocando, e se specifichiamo tutti i parametri, persino invertirli
fun main(args: Array<String>) { sum(2, 3) sum(x = 2, y = 3) sum(2, y = 3) sum(y = 3, x = 2) // Scambio di ordine }
sum è un’operazione commutativa, quindi è indifferente, ma in molti casi di tutti i giorni, l’ordine in cui sono passati i parametri è importante. Anche se differenti parametri possono essere dello stesso tipo, avranno comportamenti diversi. Pensate ad esempio se nella funzione potenza calcoliamo 2 alla 10 anziché 10 alla 2 o viceversa!
In quei casi per chiarezza è consigliabile esplicitare i parametri ai valori.
Cicli
I cicli sono un flusso di controllo per iterare delle istruzioni un numero prefissato o non prefissato di volte.
In passato era comune il classico for sui range, cioè da un numero di inizio a un numero di fine. Il motivo era legato al modo in cui si accedeva a una collezione. In memoria si sapeva di avere all’indirizzo x una sequenza di elementi di una dimensione prefissata (ad esempio 4 bytes). Allora tenendo conto dell’offset (4 bytes per elemento in questo caso), x+0 o x, corrispondeva alla posizione del primo elemento. x+1 a quello del secondo, x+2 a quello del terzo e così via.
Per questo motivo in tantissimi linguaggi di programmazione (tra cui Kotlin) gli Array sono indicizzati da 0 come primo elemento anziché 1. (Per approfondimenti)
Nei cicli tuttavia, si può partire da qualsiasi numero si vuole. Non è detto infatti, che un ciclo debba essere usato per iterare una collezione lineare (come gli array).
In Kotlin il ciclo a range basico si denota così:
for (i in 1..100) { ... } // Range chiuso: Include 100 for (i in 1 until 100) { ... } // Range semi-aperto: Non include 100
Esempio di utilizzo
for (i in 1..5) { println("Ciao for!") }
Stamperà a schermo
Ciao for! Ciao for! Ciao for! Ciao for! Ciao for!
Un ciclo molto comune, spesso preferito al for-range, è il for each. Il for each, tradotto letteralmente “per ogni”, è pensato per iterare tutti gli elementi di una collezione lineare.
In Kotlin il for-each ha una sintassi molto intuitiva. Ad esempio adoperato per una stringa:
// Stampa A CAPO ogni carattere della stringa for (carattere in "Ciao") { println(carattere) }
darà il seguente output sulla console:
C i a o
Immancabile è anche il classico ciclo while, seppur maggiormente consigliato quando non si conosce il numero delle iterazioni a priori che quando lo si conosce.
// Stampa 10 volte il messaggio "Ciao while!" var i = 0 while (i < 10) { println("Ciao while!") i++ }
Infatti, così facendo, dopo la fine del while, si avrà introdotto nello scope una variabile i potenzialmente inutile.
È possibile a dire il vero anche isolare questo blocco, isolando anche il ciclo di vita di i, ma aumenterà ulteriormente la verbosità del codice, che con un for-range risolvevamo in appena 3 righe.
I commenti
Ogni linguaggio di programmazione (beh, quasi) provvede una propria maniera, più o meno comune, per inserire nel codice del testo che sarà ignorato dal compilatore, ma utile ai programmatori/gestori del codice. (Inoltre quasi tutti gli esempi in questo articolo ne contengono)
Ci sono 2 tipi differenti di commenti in Kotlin, entrambi ricalcanti C e Java, tuttavia il secondo tipo ha un’importante differenza.
Commento a linea singola
Denotato con //
, tutto il testo che seguirà nella stessa riga di codice sarà ignorato dal compilatore
// Commento Kotlin a linea singola val x = 4 * 10 val y = 4 // * 10 println(x) // Stamperà 40 println(y) // Stamperà 4
Commento multiRIGA
Inizia con /*
e finisce con */
. Tutto quello che si frappone in queste parentesi sarà ignorato.
A differenza del C e Java, è anche possibile annidare commenti di questo tipo. Infatti, sia in C che in Java, in caso di commenti innestati, accadeva che il primo token */
corrispondeva al compilatore come la fine del commento, e non era contemplata la presenza di commenti multipli e innestati.
/* Sono un commento multiriga in Kotlin. /* Sono un commento multiriga innestato in un altro commento multiriga. */ Sono ancora un commento multiriga (ma non in C o Java). */
Immaginando infatti che non fossero innestabili, il commento sarebbe composto solo dalle prime 5 righe dell’esempio, lasciando le ultime 2 non commentate, e generando (almeno) un errore di compilazione.
I tipi di base
Premessa: Se venite dal C, o dal Java, allora sapete quali sono i tipi primitivi di un po’ tutti i linguaggi direttamente derivati dal C.
In Java avevamo boolean, byte, char, short, int, float, long, double.
In Kotlin invece, analogamente a C#, non abbiamo tipi primitivi. Ciò significa che in Kotlin qualsiasi tipo con cui abbiamo a che fare, consiste in un oggetto. Dunque anche per variabili di tipi di base, potremo chiamare metodi e properties.
I tipi di base in Kotlin consistono in
- Numeri: (Byte, Short, Int, Long, Float, Double)
- Caratteri (Char)
- Booleani (Boolean)
- Arrays (Array)
- Stringhe (String)
Numeri
In Kotlin i numeri sono trattati in maniera simile a Java, fatta eccezione per 3 punti:
- Non ci sono conversioni implicite
- I “letterali” sono diversi in certi casi
- I caratteri non sono numeri
Conversioni implicite
Le conversioni implicite tra numeri non sono permesse nemmeno quando non c’è perdita di valore, ovvero quando si fa un cast implicito da un Float (32-bit) a un Double (64-bit), o da un Int (32-bit) a un Long (64-bit).
Dunque a questo codice corrisponderà un errore di compilazione:
val x: Float = 1.0f val y: Double = x // Type mismatch. Required: 'Double' Found: 'Float'
Tenere a mente che la grandezza dei Numeri in Kotlin è definita da standard, e non cambierà in base al computer/piattaforma dove in cui ci si trova.
Letterali
I letterali sono nell’informatica una notazione per rappresentare un valore fissato nel codice sorgente. Per le stringhe, in Kotlin abbiamo le doppie apici.
Per i numeri, ci sono notazioni leggermente differenti che rappresentano tipi differenti.
// Intero (32-bit) val a = 100 // notazione decimale val myHex = 0xFF // notazione esadecimale val myBin = 0b00001011 // notazione binaria println(a) // 100 println(myHex) // 255 println(myBin) // 11 // Long (64-bit) val b = 1234567890123 // troppo grande per Int, Long di default val c = 100L // sempre 100, ma Long println(c) // 100 // Float (32-bit) val d = 10.0f // f davanti al decimale // Double (64-bit) val e = 10.0 // notazione classica val f = 123.5e10 // notazione esponenziale (= 123.5 * 10 alla 10) print(f) // 1.235e12 print(f.toLong()) // 1235000000000
Inoltre, è possibile usare l’underscore (_) per separare i numeri per chiarezza
val cost = 1_000_000 val creditCardNumber = 1234_5678_9123_4567L
Caratteri
I caratteri in Kotlin sono rappresentati dal tipo Char
, e il loro letterale è denotato dai singoli apici
val myChar = 'a' println(myChar) // a
Non è possibile a differenza di altri linguaggi utilizzare direttamente l’ASCII o Unicode utilizzando numeri per istanziare un carattere o verificarlo, anche se è possibile utilizzare un cast esplicito.
var myChar: Char myChar = 'A' // OK myChar = 65 // Errore myChar = 65.toChar() // OK ('A')
BOOLEANI
I booleani sono rappresentati dal tipo Boolean
, e un valore booleano può essere solamente true
o false
.
Gli operatori per Boolean includono:
||
per l’OR (disgiuntivo)&&
per l’AND (congiuntivo)!
invece per il NOT (negazione)
// AND println(true && true) // true println(true && false) // false println(false && true) // false println(false && false) // false // OR println(true || true) // true println(true || false) // true println(false || false) // false // NOT println(!true) // false println(!false) // true
Sia l’operatore AND che OR mostrati sono anche detti “short-circuit”, il che significa, che se la prima espressione valutata è decisiva, la seconda non viene nemmeno valutata.
Ecco un esempio sullo short-circuit:
// Funzione che ritorna un Boolean per poter essere utilizzata nelle condizioni fun stampa(messaggio: String): Boolean { println(messaggio) return true } fun main(args: Array<String>) { println(false && stampa("primo caso")) println(true && stampa("secondo caso")) println(true || stampa("terzo caso")) println(false || stampa("quarto caso")) println(stampa("quinto caso") || stampa("sesto caso")) println(stampa("settimo caso") && stampa("ottavo caso")) }
L’output sarà:
secondo caso true true quarto caso true quinto caso true settimo caso ottavo caso true
Come si può notare, il primo caso, terzo, e sesto, non sono stati nemmeno eseguiti. Infatti
println(false && stampa("primo caso"))
L’AND valuta prima false, e fallisce subito, perché sicuramente sarà false il risultato finale.println(true || stampa("terzo caso"))
L’OR valuta prima true, e quindi indipendentemente dal risultato del secondo operando, l’espressione sarà vera. Il secondo operando non viene valutato/eseguito.println(stampa("quinto caso") || stampa("sesto caso"))
Analogo al (2.), la funzione stampa(“quinto caso”) ritorna vero, dunque non è necessario nell’OR eseguire il secondo operando.
ARRAYS
Gli array corrispondono a una sequenza (di grandezza fissata) di elementi generalmente dello stesso tipo. A questi elementi ci si accede tramite l’operatore []
oppure il metodo get
in lettura, o tramite lo stesso operatore di prima o il metodo set
in scrittura. Come detto sopra, in tanti altri linguaggi e in Kotlin, gli array sono indicizzati a partire da 0, non 1, per il primo elemento.
val numeriPari: Array<Int> = arrayOf(0, 2, 4, 6, 8, 10) println(numeriPari[0]) // 0 println(numeriPari[3]) // 6
Per iterare gli elementi di un array, senza preoccuparci della loro posizione, possiamo semplicemente fare:
// Stamperà "0 2 4 6 8 10 " for (numero in numeriPari) { print(numero) print(" ") } // Somma tutti i numeri var somma = 0 for (numero in numeriPari) { somma = somma + numero } println(somma) // 30
Le stringhe
Le stringhe sono rappresentate dal tipo o classe String
. Una stringa è una sequenza immutabile di caratteri, con opportuni metodi (operazioni) associati. I caratteri di una stringa possono essere letti così come gli elementi di un classico array,
Letterali
Come menzionato prima, il letterale più classico per le stringhe è il doppio apice
var messaggio: String = "Ciao mondo!"
Tuttavia, questa notazione non va bene qualora volessimo utilizzare più di una riga di codice per scrivere la nostra stringa (immaginiamone una lunga).
In tal caso, è previsto dal linguaggio il triplice doppio-apice:
val stringaMultilinea = """ Ciao, sono una stringa molto lunga e dotata di più righe. """
Come gli array, le stringhe sono altrettanto iterabili in un for-each per carattere. (Esempio nel paragrafo Cicli).
ESERCIZIO
Tra un capitolo e l’altro di questa guida/serie, lascerò degli esercizi di cui mostrerò la soluzione nel successivo, per incitare i lettori a far pratica con Kotlin e mettere altrettanto con quanto spiegato.
Esercizio: Scrivere due funzioni di calcolo del fattoriale, una con approccio ricorsivo, e un’altra con approccio iterativo. Le funzioni devono avere la seguente firma (dichiarazione)
fun fattorialeRicorsivo(n: Long): Long fun fattorialeIterativo(n: Long): Long
Utilizzare solo i concetti spiegati in questo articolo (variabili, cicli e funzioni con i tipi base). La funzione ricorsiva non può contenere cicli.
Testare il codice con le seguenti asserzioni nel metodo main
import kotlin.test.assertEquals fun main(args: Array<String>) { assertEquals(1, fattorialeRicorsivo(0)) assertEquals(1, fattorialeRicorsivo(1)) assertEquals(2, fattorialeRicorsivo(2)) assertEquals(6, fattorialeRicorsivo(3)) assertEquals(3_628_800, fattorialeRicorsivo(10)) assertEquals(1, fattorialeIterativo(0)) assertEquals(1, fattorialeIterativo(1)) assertEquals(2, fattorialeIterativo(2)) assertEquals(6, fattorialeIterativo(3)) assertEquals(3_628_800, fattorialeIterativo(10)) }