Declaración de variables

Encuentra el código de la lección:

Abstract

En la mayoría de los lenguajes las variables son simples contenedores de datos. En Kotlin, en cambio, son propiedades que pueden incluir lógica de acceso, validación y encapsulamiento.

Este enfoque convierte incluso a las declaraciones más simples en herramientas poderosas de diseño, especialmente al crear bibliotecas reutilizables.

En esta lección aprenderás a:

  • Declarar variables con val y var , entendiendo las diferencias entre referencias inmutables y mutables.
  • Encapsular el acceso a los datos mediante getters y setters personalizados.
  • Proteger las invariantes internas de tus estructuras sin perder expresividad hacia quienes usan tu API.

Lo harás con un enfoque práctico, apoyado en buenas prácticas de diseño, para escribir código claro, seguro y mantenible.

Declaración de variables

En Kotlin existen dos formas principales de declarar variables, según necesites inmutabilidad o mutabilidad:

  • val referencia inmutable: su valor no puede reasignarse una vez inicializado.
  • var referencia mutable: su valor puede cambiar después de la declaración.
Sintaxis general y ejemplos
(val|var) nombreVariable: Tipo = valor

// Examples:
val pi: Double = 3.14159
var counter: Int = 0

Inferencia de tipos

En variables locales o privadas puedes omitir el tipo si el compilador lo infiere a partir del valor asignado. Esto mejora la legibilidad sin perder seguridad de tipos.

En propiedades y funciones públicas de una biblioteca, en cambio, es recomendable declarar explícitamente el tipo : hace más clara la API, reduce la fragilidad frente a cambios y facilita el mantenimiento a largo plazo.

Sólo lectura: val

Usa val cuando la referencia de la variable no cambiará después de su asignación. Una vez inicializada, no podrás volver a asignarle otro valor.

Ejemplos de uso de val
val hero = "Mr. Incredible"
hero = "Elastigirl"   // Error: no se puede reasignar un 'val'

val sidekick: String
sidekick = "Dash"     // Correcto: asignación diferida (una sola vez)
sidekick = "Violet"   // Error: la referencia no puede cambiar

¿Qué acabamos de hacer?

  • hero se declara con val e inicia de inmediato. Reasignarla provoca un error de compilación.
  • sidekick también es una val , pero Kotlin permite una asignación diferida siempre que sea única.

Estas variables son referencias inmutables o propiedades de solo lectura: su referencia no puede cambiar una vez asignada. Sin embargo, el valor al que apuntan puede ser mutable (por ejemplo, una lista modificable).

Más adelante veremos las constantes en tiempo de compilación declaradas con const val , que deben conocerse en compilación. Por ahora, basta con distinguir entre var y val .

Lectura y escritura: var

Usa var cuando necesites cambiar el valor o reasignar la referencia de una variable después de declararla.

Variables de lectura y escritura
var codename = "Mr. Incredible"
codename = "Elastigirl"             // ✅ Reasignación: la referencia cambia

var energy = 10
energy = energy + 5                 // ✅ Suma explícita
energy += 5                         // ✅ Operador compuesto
energy++                            // ✅ Incremento en 1

// También puedes reasignar a otro objeto del mismo tipo
var gadgets = listOf("Tracker", "Comms")
gadgets = listOf("Grapple", "Inviso-Goggles") // ✅ Nueva lista

¿Qué acabamos de hacer?

  • codename es mutable: puedes reasignar la referencia a otro valor del mismo tipo.
  • energy muestra distintas formas de actualizar números: reasignación directa, operadores compuestos ( += ) e incremento ( ++ ).
  • En gadgets , la referencia cambia a una nueva lista.

Inmutabilidad referencial ≠ inmutabilidad del objeto

En Kotlin, declarar una variable con val no garantiza que el contenido sea inmutable: significa que la referencia no puede cambiar. Si el objeto apuntado es mutable, puedes modificar su estado.

Kotlin: val fija la referencia, no necesariamente el estado interno
val lista = mutableListOf(1, 2, 3)
lista.add(4)                      // válido: cambia el contenido, no la referencia
lista[0] = 99                     // también válido

val otra = listOf(1, 2, 3)        // Lista de solo lectura (interfaz), no garantiza inmutabilidad profunda
otra.add(4)                       // no compila: la API no expone mutación

lista = mutableListOf(5, 6, 7)    // Error: no se puede reasignar un 'val'

En lenguajes como Rust, la inmutabilidad por defecto sí aplica al valor; para mutar necesitas declarar explícitamente con mut :

Rust: inmutabilidad del valor por defecto
let mut lista = vec![1, 2, 3];
lista.push(4); // permitido gracias a 'mut'

let lista_fija = vec![1, 2, 3];
lista_fija.push(4); // error: no se puede modificar un valor inmutable

Prefiere val siempre que sea posible

Siempre que puedas, usa val en lugar de var . La inmutabilidad mejora la legibilidad, facilita el mantenimiento del código y hace más sencillo el razonamiento formal sobre su comportamiento. Recurre a var solo cuando el valor deba cambiar con el tiempo.

Expresividad e intención: mejor con val que con var
// Mejor con val: expresa intención clara, el valor no cambia (más seguro y legible)
val basePower = 42
val bonus = 8
val total = basePower + bonus

// Menos claro con var: sugiere que el valor podría seguir cambiando
var total = 42
total += 8

En el primer caso, total es un resultado fijo y predecible. En el segundo, el uso de var puede dar la impresión de que total seguirá cambiando, aunque no sea así.

¿Cuándo usar var ?

Aunque val debería ser tu primera opción, hay casos donde var resulta más apropiado:

  • Cuando una variable representa el estado interno mutable de una clase.
  • Cuando necesitas acumular resultados de forma incremental en un bucle o función.
  • Cuando implementas algoritmos imperativos donde el cambio de estado es más natural y legible.
  • Cuando escribes tests o scripts en los que la simplicidad y claridad pesan más que la inmutabilidad.

Propiedades en Kotlin: más que simples campos

En Kotlin, val y var van más allá de declarar simples campos: definen propiedades con acceso controlado mediante getters y setters.

Esto permite encapsular lógica sin perder expresividad:

Propiedades en Kotlin
// Propiedad de solo lectura (val)
val soloLectura: Tipo
    get() = campoPersonalizado // lógica al obtener el valor

// Propiedad de lectura y escritura (var)
var lecturaEscritura: Tipo
    get() = campoPersonalizado
    set(value) {
        // lógica antes de asignar
        field = value
    }

¿Qué acabamos de hacer?

  • val genera solo un getter, lo que impide modificar la propiedad desde fuera.
  • var genera getter y setter, permitiendo tanto lectura como escritura controlada.
  • Puedes personalizar los métodos para añadir validación, transformación u otros efectos colaterales.

En el contexto de bibliotecas, esto permite exponer APIs seguras que muestran información de forma controlada, mientras el diseño interno conserva la flexibilidad necesaria para evolucionar o proteger invariantes.

Ejemplo práctico: Listas inmutables con campos de respaldo

Al diseñar una biblioteca, es común tener estructuras internas mutables que no deben exponerse directamente. En Kotlin, esto se resuelve fácilmente mediante un campo de respaldo con convención de prefijo _ :

Lista inmutable con campo de respaldo
variables/src/main/kotlin/com/github/username/Battle.kt
private val _party: MutableList<String> = mutableListOf("Balthier", "Vaan")

val party: List<String>
    get() = _party

fun addMember(name: String) {
    _party.add(name) // mutación interna permitida
}

¿Qué acabamos de hacer?

  • _party es el campo de respaldo: privado, almacena los datos reales y permite modificaciones internas.
  • party es la propiedad pública de solo lectura que expone una vista inmutable.
  • Al devolver List<String> , evitamos que otras personas modifiquen el contenido desde fuera de la biblioteca.
  • Este patrón garantiza que la API pública sea segura y predecible.

Es una práctica común en bibliotecas bien diseñadas: exponer lo mínimo necesario mientras se protegen las invariantes internas.

Ejemplo práctico: Encapsular lógica de asignación en el setter

Cuando diseñamos una biblioteca, puede ser necesario que ciertos valores no se asignen directamente, sino que pasen por validaciones o transformaciones. Esto se logra mediante un setter personalizado.

private val semverRegex = Regex("""^\d+\.\d+\.\d+$""")

var version: String = "1.0.0"
    set(value) {
        val v = value.trim()
        require(semverRegex.matches(v)) {
            "Version must be in the format X.Y.Z (integers)."
        }
        field = v
    }

¿Qué acabamos de hacer?

  • version aplica normalización (trim) y validación en su setter.
  • El patrón semverRegex se precompila y requiere X.Y.Z con enteros. ^ y $ aseguran que toda la cadena coincida (inicio y fin respectivamente).
  • require(...) lanza IllegalArgumentException si el valor no es válido.

Este patrón permite crear APIs intuitivas pero seguras, combinando accesibilidad con control sobre las invariantes internas.

Funciones de validación estándar: require() , check() y error()

  • require(Boolean, () -> Any): Unit — valida argumentos de entrada. Lanza IllegalArgumentException . El mensaje solo se calcula si falla.
  • check(Boolean, () -> Any): Unit — valida el estado interno. Lanza IllegalStateException (evaluación diferida del mensaje).
  • error(Any): Nothing — lanza incondicionalmente IllegalStateException .

Setter privado

Una propiedad puede tener un getter público y un setter privado. Esto permite que el valor se lea desde fuera, pero solo se modifique dentro del mismo contexto (clase, objeto, interfaz o nivel superior).

var score: Int = 0
    private set // setter privado

Ejercicio: Controlar el nivel de dificultad desde el motor

Supón que estás diseñando una biblioteca para un motor de juego. Quieres exponer el nivel de dificultad actual para que otras personas puedan consultarlo, pero solo el motor debe poder modificarlo.

Declara una propiedad difficultyLevel que:

  • Sea de tipo String .
  • Permita valores como "easy" , "normal" o "hard" .
  • Pueda leerse públicamente desde cualquier parte.
  • Solo pueda modificarse desde dentro del módulo, y con validación.

Hints

Puedes usar el operador in para verificar que un valor se encuentre dentro de un conjunto de la forma x in setOf(...) .

Solución

var difficultyLevel: String = "normal"
    private set(value) {
        require(value in setOf("easy", "normal", "hard")) {
            "Difficulty level must be one of: easy, normal, hard."
        }
        field = value
    }