Introduzione a Kotlin: Parte 1 – Le basi

I

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+0x, 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 booleanbytecharshortintfloatlongdouble.

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:

  1. Non ci sono conversioni implicite
  2. I “letterali” sono diversi in certi casi
  3. 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 truefalse.

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 casoterzo, e sesto, non sono stati nemmeno eseguiti. Infatti

  1. println(false && stampa("primo caso"))
    L’AND valuta prima false, e fallisce subito, perché sicuramente sarà false il risultato finale.
  2. 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.
  3. 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))
}

 

A proposito di me

Ruggiero Altini

Laureato in Informatica, appassionato di algoritmi, ha esperienza in Computer Vision, Machine Learning e nello sviluppo di app per iOS e Android. Ha usato e usa estensivamente Java, Python, C++, Swift e Kotlin. Si interessa attualmente ai rami dell'Intelligenza Artificiale.

Gli articoli più letti

Articoli recenti

Commenti recenti