Gestión de tipos nulos en Kotlin



Kotlin permite un código seguro contra ciertos errores (del tipo NullPointerException) que pueden aparecer cuando se manejan valores nulos. Esto lo hace a través de los llamados nullable types que podemos traducir como tipos anulables, esto es, con capacidad de ser nulos. Kotlin consigue esto obligándonos a tener en cuenta los posibles null o valores nulos.

En principio podemos distinguir entre tipos anulables y tipos no nulos (Nullable types and Non-Null Types). Pero, salvo que se exprese explícitamente, los nulos en Kotlin no existen. Es decir, a ningún objeto por defecto (y recuerda que en Kotlin todos los tipos son objetos) se le puede asignar null. Simplemente no podemos asignar null a un valor no anulable:
var numero: Int = null  // ERROR: Null can not be a value of a non-null type Int

Operador de acceso seguro ?

Para que una variable acepte la posibilidad de aceptar nulos, hay que marcar el tipo con el operador ?. Entonces lo anterior lo resolvemos de la siguiente manera:
var numero: Int? = null // Ahora numero es un tipo anulable
Otro ejemplo con el mismo tipo de error:
fun strLen(s: String) = s.length    
strLen(null)  // ERROR: Null can not be a value of a non-null type String
En el ejemplo anterior el parámetro se declara como de tipo String, y en Kotlin esto significa que siempre debe contener una instancia de String. El compilador lo aplica, por lo que no puede pasar un argumento que contenga un valor nulo (ni cualquier otro tipo que no sea de tipo String). Esto da la garantía de que la función strLen nunca emitirá una NullPointerException en tiempo de ejecución.

Para permitir el uso de la función strLen con posibles valores nulos, se debe marcar explícitamente colocando un signo de interrogación después del nombre de tipo, pero esto no es suficiente:
fun strLen(s: String?) = s.length // ERROR
Y esto es así porque el compilador recuerda que el parámetro de la función es un tipo anulable y no lo puede tratar después como no anulable. En cambio el siguiente código es perfectamente válido porque con el if chequeamos en tiempo de compilación si s es nulo y devuelve una salida válida en cualquier caso:
fun strLen(s: String?): Int = if (s != null) s.length else 0    
val x: String? = null
println(strLen(x))  // 0
println(strLen("abc")) // 3
El código anterior se puede presentar también así:
fun strLen(s: String?): Int {
    if (s != null) return s.length
    return 0
}   
val x: String? = null
println(strLen(x))  // 0
println(strLen("abc")) // 3

Expresión de acceso seguro

Pero Kotlin ofrece un operador especial (expresión de acceso seguro que se escribe con una ? delante del . cuando se llama a un método) para acceder de forma segura a un valor anulable sin recurrir al if, y nuestra función strLen quedaría así:
fun strLen(s: String?) = s?.length    
val x: String? = null
println(strLen(x))  // null
println(strLen("abc")) // 3
Otro ejemplo:
fun printMayus(s: String?) {
    val palabra: String? = s?.toUpperCase()
    println(palabra)
}
printMayus("abc") // ABC
printMayus(null) // null

Operador Elvis ?:

Además Kotlin tiene otro operador, el operador Elvis u operador de unión nula (?:), que sirve para proporcionar un valor predeterminado en lugar de un nulo. El operador toma dos valores, y su resultado es el primer valor si no es nulo y el segundo valor si es nulo. Un ejemplo:
fun foo(s: String?) {
    val t: String = s ?: "" // si s es nulo, el resultado es un String vacío
    println(t)
}    
foo("hola") // hola
foo(null) // devuelve una cadena vacía
Aplicando el operador Elvis a nuestra función strLen, el código quedaría así para que en caso de nulo devuelva 0:
fun strLen(s: String?): Int = s?.length ?: 0    
println(strLen("abc")) // 3
println(strLen(null)) // 0

Expresiones no nulas !!

Por otra parte, otra herramienta disponible en Kotlin para tratar con valores de tipo anulable y afirmar que un tipo es, con total seguridad, no nulo se representa por un signo de exclamación doble, lo que convierte cualquier valor a un tipo que no sea nulo (y lanza una excepción para valores nulos). Con este operador se evita la necesidad de chequear null si se está completamente seguro de que una variable nunca será nula (¿podemos estar completamente seguros de eso?).
fun ignorarNulos(s: String?) {
    val sNoNull: String = s!!  // tipo no anulable
    println(sNoNull.length)
}

ignorarNulos("hola") // 4
ignorarNulos(null) // Exception in thread "main" kotlin.KotlinNullPointerException
Con el código anterior básicamente se le está diciendo al compilador: "Sé con completa seguridad que el valor no es nulo, y aceptaré una excepción si resulta que estoy equivocado".

Función let

Otra herramienta que facilita el manejo de expresiones anulables es la función let. La función let junto con un operador de acceso seguro permite evaluar una expresión, verificar el resultado en busca de valores nulos y almacenar el resultado en una variable, todo en una sola expresión concisa.

Uno de los usos más comunes de la función let es el manejo de un argumento anulable que debe pasarse a una función que espera un parámetro no nulo. En el siguiente ejemplo, la función enviarEmail toma un parámetro de tipo String (por tanto requiere un parámetro no nulo) y envía un correo electrónico a esa dirección.
fun enviarEmail(email: String) { /*...*/ }
No podríamos pasar un valor de tipo anulable a esta función:
val email: String? = "direccion@email.com"
enviarEmail(email)  // ERROR: Type mismatch: inferred type is String? but String was expected
Debemos comprobar explícitamente si ese valor no es nulo:
if (email != null) enviarEmail(email)
Pero se puede hacer de otra manera con la función let y lo que hemos visto hasta ahora para hacer una llamada segura. La función let condiciona su ejecución al valor del correo electrónico (solo se ejecuta si el valor es no nulo), por lo que utiliza el correo electrónico como un argumento no nulo de la función lambda:
email?.let { email -> enviarEmail(email) }
Esta expresión se puede acortar omitiendo el parámetro, quedando el código de la siguiente manera:
fun enviarEmail(email: String) {
    println("Enviando email a $email")
}

var email: String? = "correo@ejemplo.com"
email?.let { enviarEmail(it) } // Enviando email a correo@ejemplo.com

email = null
email?.let { enviarEmail(it) } // No se ejecuta la función

Propiedades inicializadas con demora: lateinit

Para terminar por ahora con este tema, solo comentar la posibilidad de declarar una propiedad o variable (que no es de un tipo que acepte nulo) sin inicializarla inmediatamente, marcando la propiedad con el modificador lateinit para evitar las verificaciones de valores nulos.

Y es que Kotlin no permite que una variable no nula se deje sin inicializar durante su declaración. Sin embargo, lateinit permite declarar una variable primero y luego inicializarla durante el ciclo de ejecución del código. Un ejemplo:
// variable que inicializaremos en un punto posterior del código
lateinit var persona1: Persona
 
fun main(args: Array<String>) {
    // inicializando la variable
    persona1 = Persona("Manolo", 28)
    print(persona1.nombre + " tiene " + persona1.edad.toString() + " años.")
    // Manolo tiene 28 años.
}
 
data class Persona(var nombre:String, var edad:Int)
En el código anterior puedes comprobar que si eliminas el modificador lateinit aparece el error Property must be initialized. Con lo que hemos visto hasta ahora podríamos solucionarlo así:
// variable tipo nulo inicializada como null
var persona1: Persona? = null
 
fun main(args: Array<String>) {    
    persona1 = Persona("Manolo", 28)
    // expresión de acceso seguro
    print(persona1?.nombre + " tiene " + persona1?.edad.toString() + " años.")
    // Manolo tiene 28 años.
}
 
data class Persona(var nombre:String, var edad:Int)
Pero si queremos que persona1 sea no anulable y no la queremos o podemos inicializar al declararla, debemos utilizar lateinit para declararla sin inicializarla.

Comentarios

Entradas populares

Recursos gratis para aprender Kotlin

I/O: entrada y salida de datos en consola

Lectura y escritura de archivos