Volver a Kotlin Básico

Introducción

Una de las características más elogiadas de Kotlin es su sistema de seguridad de nulos que elimina el error de mil millones de dólares de las excepciones de puntero nulo en tiempo de compilación.

Tipos Nullable vs No-Nullable

// No-nullable (no puede ser null) var nombre: String = "Alicia" // nombre = null // ¡Error de compilación! // Nullable (puede ser null) var nombreNullable: String? = "Bob" nombreNullable = null // OK // Inferencia de tipo val saludo = "Hola" // String (no-nullable) val opcional: String? = null // String? (nullable)

Operador de Llamada Segura (?.)

val nombre: String? = null // Llamada segura - retorna null si el receptor es null val longitud = nombre?.length // null val mayus = nombre?.uppercase() // null // Encadenar llamadas seguras val pais: String? = usuario?.direccion?.pais

Operador Elvis (?:)

Proporcionar valores por defecto para casos null:

val nombre: String? = null // Forma tradicional val nombreMostrado = if (nombre != null) nombre else "Desconocido" // Operador Elvis val nombreMostrado = nombre ?: "Desconocido" // Con llamada segura val longitud = nombre?.length ?: 0 // Retorno anticipado fun procesarNombre(nombre: String?): String { val nombreSeguro = nombre ?: return "Se requiere nombre" return nombreSeguro.uppercase() }

Aserción No-Nula (!!)

Convertir nullable a no-nullable (¡usar con cuidado!):

val nombre: String? = "Alicia" // Esto lanzará NullPointerException si nombre es null val longitud = nombre!!.length // Solo usar cuando estés absolutamente seguro de que no es null val config = cargarConfig() // Retorna Config? val host = config!!.host // Mejor usar llamada segura o elvis

Conversiones Seguras (as?)

val cualquiera: Any = "Hola" // Conversión insegura (lanza ClassCastException si falla) // val str: String = cualquiera as String // Conversión segura (retorna null si falla) val str: String? = cualquiera as? String // "Hola" val num: Int? = cualquiera as? Int // null

Función Let

Ejecutar código solo si no es null:

val nombre: String? = "Alicia" // Forma antigua if (nombre != null) { println(nombre.length) println(nombre.uppercase()) } // Con let nombre?.let { println(it.length) println(it.uppercase()) } // Ejemplo del mundo real usuario?.email?.let { email -> enviarEmailA(email) }

Función Also

Realizar efectos secundarios:

val numeros = mutableListOf(1, 2, 3) .also { println("Original: $it") } .also { it.add(4) } .also { println("Modificado: $it") }

Función Run

Ejecutar bloque y retornar resultado:

val resultado = run { val nombre: String? = obtenerNombre() nombre?.uppercase() ?: "DESCONOCIDO" }

Lateinit

Declarar propiedad no-nula que se inicializará después:

class MiActividad { // Se inicializará antes del primer uso lateinit var presentador: Presentador fun onCreate() { presentador = Presentador() } fun verificarInit() { if (::presentador.isInitialized) { presentador.iniciar() } } }

Inicialización Perezosa

Inicializar propiedad en el primer acceso:

class GestorDatos { // Se inicializa solo cuando se accede por primera vez val baseDatos: BaseDatos by lazy { println("Creando base de datos") BaseDatos.conectar() } val config: Config by lazy { cargarConfig() } }

Tipos de Plataforma

Al llamar a Java desde Kotlin:

// Método Java: String getName() // Kotlin lo ve como String! val nombre = objetoJava.name // String! (tipo de plataforma) // Mejor: declarar explícitamente val nombre: String = objetoJava.name // No-nulo val nombreNullable: String? = objetoJava.name // Nullable

Buenas Prácticas

// ✅ Bueno: Usar llamadas seguras fun obtenerLongitud(texto: String?): Int { return texto?.length ?: 0 } // ❌ Malo: Abusar de !! fun obtenerLongitud(texto: String?): Int { return texto!!.length // ¡Puede fallar! } // ✅ Bueno: Usar let para verificaciones de null usuario?.email?.let { enviarEmail(it) } // ❌ Malo: Verificación tradicional de null if (usuario != null && usuario.email != null) { enviarEmail(usuario.email) } // ✅ Bueno: Retorno anticipado con Elvis fun procesar(datos: String?): Resultado { val datosSeguro = datos ?: return Resultado.Error("Sin datos") return Resultado.Exito(datosSeguro) }

Ejemplos Prácticos

Validación de usuario:

data class Usuario(val nombre: String?, val email: String?, val edad: Int?) fun validarUsuario(usuario: Usuario?): String { val nombre = usuario?.nombre ?: return "Usuario es null" val email = usuario.email ?: return "Email requerido" val edad = usuario.edad ?: return "Edad requerida" if (edad < 18) return "Debe ser mayor de 18" if (!email.contains("@")) return "Email inválido" return "Usuario válido: $nombre" }

Carga segura de configuración:

class GestorConfig { private var config: Config? = null fun cargar() { config = try { cargarDeArchivo() } catch (e: Exception) { null } } fun obtenerHost(): String = config?.host ?: "localhost" fun obtenerPuerto(): Int = config?.puerto ?: 8080 fun estaCargado(): Boolean = config != null }

Procesamiento de cadena opcional:

data class Direccion(val calle: String?, val ciudad: String?, val pais: String?) data class Usuario(val nombre: String?, val direccion: Direccion?) fun obtenerUbicacionUsuario(usuario: Usuario?): String { return usuario?.direccion?.ciudad?.let { ciudad -> usuario.direccion.pais?.let { pais -> "$ciudad, $pais" } } ?: "Ubicación desconocida" } // O más elegantemente fun obtenerUbicacionUsuario(usuario: Usuario?): String { val ciudad = usuario?.direccion?.ciudad val pais = usuario?.direccion?.pais return if (ciudad != null && pais != null) { "$ciudad, $pais" } else { "Ubicación desconocida" } }

Patrones Comunes

Operaciones seguras con colecciones:

val nombres: List<String?> = listOf("Alicia", null, "Bob", null, "Carlos") // Filtrar valores no-nulos val nombresNoNulos = nombres.filterNotNull() // [Alicia, Bob, Carlos] // Mapear con llamadas seguras val longitudes = nombres.mapNotNull { it?.length } // [6, 3, 6]

Múltiples verificaciones de null:

// En lugar de ifs anidados if (a != null) { if (b != null) { if (c != null) { hacerAlgo(a, b, c) } } } // Usar let con múltiples parámetros a?.let { seguroA -> b?.let { seguroB -> c?.let { seguroC -> hacerAlgo(seguroA, seguroB, seguroC) } } } // O verificar todos a la vez if (a != null && b != null && c != null) { hacerAlgo(a, b, c) }

Conclusión

El sistema de seguridad de nulos de Kotlin previene la mayoría de excepciones de puntero nulo en tiempo de compilación. Al comprender los tipos nullable, las llamadas seguras y el operador Elvis, puedes escribir código más seguro y expresivo.

En el próximo capítulo, exploraremos clases y data classes en Kotlin.