Declaración de variables
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
yvalval, entendiendo las diferencias entre referencias inmutables y mutables.varvar - 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:
-
→ referencia inmutable: su valor no puede reasignarse una vez inicializado.valval -
→ referencia mutable: su valor puede cambiar después de la declaración.varvar
(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 val
val val
Usa 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.
val val
val 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?
-
se declara conheroheroe inicia de inmediato. Reasignarla provoca un error de compilación.valval -
también es unasidekicksidekick, pero Kotlin permite una asignación diferida siempre que sea única.valval
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 , que deben conocerse en
compilación. Por ahora, basta con distinguir entre
const val const val y var var .
val val
Lectura y escritura: var var
var var
Usa cuando necesites cambiar el valor o reasignar la referencia de una variable después de declararla.
var var
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?
-
es mutable: puedes reasignar la referencia a otro valor del mismo tipo.codenamecodename -
muestra distintas formas de actualizar números: reasignación directa, operadores compuestos (energyenergy) e incremento (+=+=).++++ - En
, la referencia cambia a una nueva lista.gadgetsgadgets
Inmutabilidad referencial ≠ inmutabilidad del objeto
En Kotlin, declarar una variable con no garantiza que el contenido sea inmutable:
significa que la referencia no puede cambiar. Si el
objeto apuntado es mutable, puedes modificar su estado.
val val
val 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 mut
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 val siempre que sea posible
val val
Siempre que puedas, usa en lugar de val val . 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 var solo cuando el valor deba cambiar con el tiempo.
var var
val val que con
var 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, es un resultado fijo y
predecible. En el segundo, el uso de total total puede
dar la impresión de que
var var seguirá cambiando, aunque no sea así.
total total
¿Cuándo usar var var ?
var var
Aunque debería ser tu primera opción, hay casos donde val val resulta más apropiado:
var var
- 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, y val val van más
allá de declarar simples campos: definen propiedades con acceso
controlado mediante getters y setters.
var var
Esto permite encapsular lógica sin perder expresividad:
// 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?
-
genera solo un getter, lo que impide modificar la propiedad desde fuera.valval -
genera getter y setter, permitiendo tanto lectura como escritura controlada.varvar - 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 :
_ _
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?
-
es el campo de respaldo: privado, almacena los datos reales y permite modificaciones internas._party_party -
es la propiedad pública de solo lectura que expone una vista inmutable.partyparty - Al devolver
, evitamos que otras personas modifiquen el contenido desde fuera de la biblioteca.List<String>List<String> - 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?
-
aplica normalización (trim) y validación en su setter.versionversion - El patrón
se precompila y requieresemverRegexsemverRegexcon enteros.X.Y.ZX.Y.Zy^^aseguran que toda la cadena coincida (inicio y fin respectivamente).$$ -
lanzarequire(...)require(...)si el valor no es válido.IllegalArgumentExceptionIllegalArgumentException
Este patrón permite crear APIs intuitivas pero seguras, combinando accesibilidad con control sobre las invariantes internas.
Funciones de validación estándar: require() require() , check() check() y error() error()
require() require() check() check() error() error() -
— valida argumentos de entrada. Lanzarequire(Boolean, () -> Any): Unitrequire(Boolean, () -> Any): Unit. El mensaje solo se calcula si falla.IllegalArgumentExceptionIllegalArgumentException -
— valida el estado interno. Lanzacheck(Boolean, () -> Any): Unitcheck(Boolean, () -> Any): Unit(evaluación diferida del mensaje).IllegalStateExceptionIllegalStateException -
— lanza incondicionalmenteerror(Any): Nothingerror(Any): Nothing.IllegalStateExceptionIllegalStateException
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 que:difficultyLevel difficultyLevel
- Sea de tipo
.StringString - Permita valores como
,"easy""easy"o"normal""normal"."hard""hard" - Pueda leerse públicamente desde cualquier parte.
- Solo pueda modificarse desde dentro del módulo, y con validación.
Hints
in in para verificar que un valor se encuentre dentro de un conjunto de la forma x in setOf(...) 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
}