Null Safety e Riferimenti Nullabili e non Nullabili
Kotlin a differenza di Java prevede che tutti i tipi sono degli oggetti (ha eliminato i tipi primitivi e le classi wrapper), e quindi passati per riferimento. Ma proprio per questa mancanza deve migliorare il sistema per evitare le NullPointerException (NPE), in quanto basterebbe che uno si dimentichi di inizializzare una variabile e scatta la NPE.
Infatti per essere all'altezza di altri linguaggi di programmazione dove esistono i tipi passati per valore non nullabili (vedi i tipi derivanti dalle struct del C# o Swift) e per by-passare le limitazioni della JVM (essa non permette la definizione di tipi di valore) Kotlin fa una distinzione tra i riferimenti che possono contenere null (riferimenti nullabili) e quelli che non possono (riferimenti non nullabili). Ad esempio, una variabile di tipo, se non esplicitamente detto tramite l'operatore ? descritto di seguito , String non può contenere null:
Secondo la documentazione ufficiale di Kotlin ci sono pochi casi in cui si può generare una NullPointerException:
- Una chiamata esplicita a throw NullPointerException();
- Uso dell'operatore !!, descritto di seguito, che genera una KotlinNullPointerException;
- Del codice Java esterno;
- C'è qualche incongruenza tra i tipi di dati nell'inizializzazione delle variabili, quasi sempre segnalate dal compilatore.
Riferimento non nullabile:
Dichiarando una variabile e poi provando ad assegnarli il valore null:
var a: String = "abc"
a = null // compilation error
Come si nota non è possibile assegnare in nessun modo il riferimento nullo ad una variabile non nullabile. Il compilatore non te fa fare.
Riferimento nullabile e operatore ?:
Per consentire il valore null, possiamo dichiarare una variabile come stringa nullable, con la scritta String?:
var b: String? = "abc"
b = null // ok
Ora, se chiamate un metodo o accedi a una proprietà a (dello Snippet 10.16), è garantito che non provochi un NPE, in e possiamo tranquillamente fare:
val l = a.length
Ma se si desidera accedere alla stessa proprietà b (dello Snippet 10.17), ciò non sarebbe sicuro e il compilatore segnala un errore:
val l = b.length // error: variable 'b' can be null
Ma abbiamo ancora bisogno di accedere a quella proprietà, giusto? Ci sono alcuni modi per farlo.
Controllo anti-null
In primo luogo, è possibile controllare esplicitamente se bè null e gestire separatamente le due opzioni:
val l = if (b != null) b.length else -1
Il compilatore traccia le informazioni sul controllo eseguito e consente la chiamata lengthall'interno del if. Sono supportate anche condizioni più complesse:
if (b != null && b.length > 0) {
print("String of length ${b.length}")
} else {
print("Empty string")
}
Si noti che questo funziona solo dove b è immutabile, nel seno che non deve essere stata fatta nessuna modifica al riferimento di b tra il controllo (b != null) e l'accesso ad un membro.
Operatore di accesso sicuro (?.)
La seconda opzione è l'operatore di accesso sicuro, che prevede l'inserimento di un ? prima del .:
b?.length
Questo restituisce b.length se b non è null e altrimenti null. Il tipo di ritorno, naturalmente è un Int nullabile, cioè Int?.
L'operatore di accesso sicuro si può anche applicare più e più volte nella stessa riga:
var name = bob?.department?.head?.name
Tale catena restituisce null se almeno una delle proprietà è null, altrimenti restituisce il valore.
let{}, also{} e run{}
Per eseguire una determinata operazione solo per valori non null, è possibile utilizzare l'operatore di chiamata di sicurezza insieme a let{}:
val listWithNulls: List<String?> = listOf("A", null)
for (item in listWithNulls) {
item?.let { println(it) } // prints A and ignores null
}
Se vogliamo applicare un'operazione aggiuntiva oltre a let{}, ad esempio accedere ad ogni valore non null, possiamo usare il metodo also{} in catena al metodo let{}:
val firstName = "Tom"
val secondName = "Michael"
val names: List<String?> = listOf(firstName, null, secondName)
var res = listOf<String?>()
for (item in names) {
item?.let { res = res.plus(it); it }
?.also{it -> println("non nullable value: $it")}
}
Kotlin ha anche un metodo run{} per eseguire un'operazione in un riferimento nullabile. È molto simile a let, ma la lambda non accetta parametri, ma si può usare this per rimediare:
val firstName = "Tom"
val secondName = "Michael"
val names: List<String?> = listOf(firstName, null, secondName)
var res = listOf<String?>()
for (item in names) {
item?.run{res = res.plus(this)}
}
In questo caso la parola chiave this si riferisce alla variabile item, questa speciale caratteristica di Kotlin verrà vista in futuro.
Operatore Elvis o null coalescing (?:)
L'operatore ?: è detto operatore Elvis o di null coalescing, ed è molto utile in una serie di situazioni permettendo di scrivere codice più elegante e ristretto in Kotlin.
Quante volte vi è capitato di dover scrivere un blocco if per controllare se una variabile è null, ed agire di conseguenza scegliendo un valore di default da assegnare alla stessa variabile? Ecco un esempio:
var name : String
if(str != null) {
name = str
}
else {
name = "senza nome"
}
Probabilmente starete pensando: "beh c'è sempre l'operatore ternario!", non in Kotlin! Ma potremmo sempre usare when:
var name = when(str) {
null -> "senza nome"
else -> str
}
Ma l'operatore elvis o di null coalescing consente di essere ancora più concisi. Esso verifica se l'operando a sinistra è diverso da null, ed in tal caso restituisce il valore dell'operando sinistro stesso. In caso contrario invece restituisce l'operando destro:
var nome = str ?: "senza nome"
è possibile anche realizzare una catena di operatori elvis:
var name = firstStr ?: secondStr ?: "questo caso è proprio disperato!"
In Kotlin le keyword throw e return possono essere utilizzate anche sul lato destro dell'operatore elvis. Questo può essere molto utile, per esempio, per controllare argomenti di funzione:
fun foo(node: Node): String? {
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
// ...
}
L'Operatore (!!)
L'ultima delle opzioni prevista da Kotlin è per quelli che amano l'NPE. Possiamo scrivere b!!e questo restituirà un valore non nullo b (ad esempio, Stringnel nostro esempio) o lanciare una NPE se bè nullo:
val l = b!!.length
Quindi, se si desidera lanciare una NPE, da gestire altrove, è possibile farlo ma necessità di una "dichiarazione esplicita" della volontà del programmatore.
Cast in sicurezza
I cast regolari, visti in una precedente lezione, possono risultare in un ClassCastException se l'oggetto non è del tipo di destinazione. Un'altra opzione è quella di utilizzare i cast di sicurezza che restituiscono null se il tentativo non è riuscito:
val aInt: Int? = a as? Int
Collezioni Nullabili
Ci sono due modi per trasformare una collezione nullabile in una non nullabile:
fun needList(list: List<Int>?) {
val notNullList: List<Int> = list!! // metodo 1
val notNullList: List<Int> = list ?: emptyList() // metodo 2
}
In Kotlin non esiste un operatore per l'accesso sicuro agli elementi dell'array, in stile di: ?[]. In questo caso dovremmo accedere tramite la funzione ?.get() e/o ?.set().
Collezioni di Riferimenti Nullabili
Se si dispone di una raccolta di elementi di tipo nullable e si desidera filtrare elementi non nullo, è possibile farlo utilizzando filterNotNull().
val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()
https://kotlinlang.org/docs/reference/
https://stackoverflow.com/documentation/kotlin/topics
https://medium.com/makingtuenti/infix-functions-in-kotlin-2db3d3142dd2