Declaración de variables
Las variables son uno de los conceptos más básicos de un lenguaje de programación, pero en Kotlin no se limitan a ser simples contenedores de datos: 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 cuando estás creando bibliotecas reutilizables.
En esta lección aprenderás a:
- Declarar variables con
val
yvar
, y entender las diferencias entre referencias inmutables y mutables. - Encapsular correctamente el acceso a los datos usando getters y setters personalizados.
- Proteger las invariantes internas de tus estructuras, sin perder expresividad hacia quienes usan tu API.
Todo esto con un enfoque práctico y centrado en buenas prácticas de diseño, que te ayudarán a escribir código claro, seguro y mantenible.
📦 Declaración de variables
En Kotlin puedes declarar variables de dos formas:
val
→ de solo lectura: su valor no puede reasignarse una vez inicializado.var
→ lectura y escritura: su valor puede cambiar después de la declaración.
val o var nombreVariable: Tipo = valor
Inferencia de tipos
En variables locales o privadas, puedes omitir el tipo si el compilador puede inferirlo a partir del valor asignado.
Esto mejora la legibilidad sin perder seguridad de tipos.
Sin embargo, en bibliotecas es recomendable declarar explícitamente el tipo de las propiedades y funciones públicas,
ya que esto hace más robusta la API, facilita el mantenimiento y evita cambios accidentales en la firma pública.
🔒 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.
val master = "Goomoonryong"
master = "Yi Shi-Woon" // 🔥 Error: no se puede reasignar un 'val'
val student: String
student = "Haje Kang" // ✅ Correcto: asignación diferida (una sola vez)
student = "Shi-Ho Lee" // 🔥 Error: la referencia no puede cambiar
¿Qué acabamos de hacer?
master
se declara conval
e inicia de inmediato. Reasignarla provoca un error de compilación.student
también es unaval
, pero se inicializa después. Kotlin permite esta asignación diferida, siempre que sea una única vez.
A lo largo del curso nos referiremos a estas variables como referencias inmutables o propiedades de solo lectura: su referencia no puede cambiar una vez asignada.
Esto no implica que el valor al que apuntan sea inmutable.
Por ejemplo, una variable val
puede referirse a una lista mutable: aunque no puedas cambiar la referencia, sí puedes modificar el contenido de la lista.
Más adelante hablaremos de las constantes en tiempo de compilación, declaradas con const val
, cuyo valor debe conocerse en tiempo de compilación.
Por ahora, basta con distinguir entre referencias mutables (var
) y referencias inmutables (val
).
🔓 Lectura y escritura (var
)
Usa var
cuando necesites modificar el valor o cambiar la referencia de una variable después de declararla:
var technique = "Black Heaven & Earth"
technique = "Soul-Crushing Strike" // ✅ Correcto: se cambia la técnica referenciada
var energy = 10
energy = energy + 5 // ✅ Correcto: suma explícita
energy += 5 // ✅ Equivalente más conciso
energy++ // ✅ Incremento en 1
¿Qué acabamos de hacer?
technique
es una variable mutable: su referencia puede actualizarse a distintas técnicas.energy
demuestra varias formas de modificar valores numéricos mutables: reasignación directa, operación compuesta (+=
) e incremento (++
).
Inmutabilidad referencial ≠ inmutabilidad profunda
En Kotlin, declarar una variable con val
no significa que su contenido sea inmutable, sino que la referencia no puede cambiar.
Es decir, si el objeto apuntado es mutable, puedes modificarlo.
val lista = mutableListOf(1, 2, 3)
lista.add(4) // ✅ Válido: se modifica el contenido, no la referencia
lista = mutableListOf(5, 6, 7) // 🔥 Error: no se puede reasignar un 'val'
En cambio, en lenguajes como Rust, la inmutabilidad por defecto sí aplica al contenido, no solo a la referencia:
let mut lista = vec![1, 2, 3];
lista.push(4); // ✅ Solo si fue declarada con `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, prefiere val
en lugar de var
.
La inmutabilidad no solo mejora la legibilidad y el mantenimiento del código, sino que también facilita el razonamiento formal sobre su comportamiento.
Usa var
únicamente cuando realmente necesites que el valor de una variable cambie a lo largo del tiempo.
// ✅ Mejor con val: expresa intención clara, el valor no cambia
val basePower = 42
val bonus = 8
val total = basePower + bonus
// 🔶 Menos claro con var: sugiere que el valor podría cambiar
var total = 42
total += 8
En el primer caso, total
es el resultado de una operación fija y predecible.
En el segundo, el uso de var
puede dar la impresión de que total
seguirá cambiando más adelante, aunque no sea así.
🔄 ¿Cuándo usar var
?
Aunque val
debe ser tu primera opción, hay situaciones justificadas donde el uso de var
es adecuado:
- Cuando una variable cambia como parte del estado interno de una clase mutable.
- Si estás acumulando resultados a lo largo de un bucle o una función.
- Al implementar algoritmos imperativos donde el cambio de estado es más natural o legible.
- En tests o scripts donde la claridad y simplicidad pueden pesar más que la inmutabilidad.
🧱 Propiedades en Kotlin: más que campos
En Kotlin, val
y var
no solo declaran campos como en otros lenguajes:
en realidad definen propiedades con acceso controlado a través de getters y setters.
Esto permite encapsular lógica sin perder expresividad:
val soloLectura: Tipo = inicializador
get() = campoPersonalizado // Getter implícito o redefinido
var lecturaEscritura: Tipo = inicializador
get() = campoPersonalizado
set(value) {
// lógica adicional antes de asignar
field = value
}
¿Qué acabamos de hacer?
val
genera solo un getter, lo que impide modificar la propiedad desde fuera.var
genera tanto getter como setter, permitiendo control total sobre la lectura y escritura.- Puedes personalizar estos métodos si necesitas validación, transformación u otros efectos colaterales.
En el contexto de bibliotecas, esto permite definir APIs que exponen información de forma segura, mientras internamente conservan flexibilidad para evolucionar o proteger invariantes.
🧪 Ejemplo práctico: Exponer listas inmutables con un campo de respaldo
Cuando desarrollas una biblioteca, es común tener estructuras internas que deben mantenerse mutables, pero que no deben exponerse directamente a quienes consumen tu API.
Kotlin permite resolver esto mediante un campo de respaldo:
private val _party: MutableList<String> = mutableListOf("Balthier", "Vaan")
val party: List<String>
get() = _party
¿Qué acabamos de hacer?
_party
es un campo de respaldo: una propiedad privada que contiene los datos reales y permite modificaciones internas.party
es una propiedad pública de solo lectura que expone una vista inmutable de la lista.- Al devolver
List<String>
, evitamos que otrxs puedan modificar el contenido desde fuera de la biblioteca.
Este patrón es común en bibliotecas bien diseñadas: exponer lo mínimo necesario mientras se protegen las invariantes internas.
Conversión implícita de mutabilidad
En Kotlin, cuando una función o propiedad devuelve una MutableList<T>
pero declara su tipo como List<T>
, el compilador permite esta conversión implícitamente.
Esto es útil al desarrollar bibliotecas: puedes mantener una estructura mutable internamente (MutableList<String>
) y exponer solo una vista inmutable (List<String>
) al exterior, sin necesidad de copiar los datos o envolver la lista manualmente.
🧪 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.
var version: String = "1.0.0"
set(value) {
require(Regex("""\d+\.\d+\.\d+""").matches(value)) {
"Version must be in the format X.Y.Z where X, Y, and Z are integers."
}
field = value
}
¿Qué acabamos de hacer?
- La propiedad
version
usa unsetter
personalizado para validar el formato antes de asignar el nuevo valor. - Esta validación protege a la biblioteca de estados inválidos.
- A quienes consumen la API les parece una propiedad normal, pero internamente se aplican reglas.
Este patrón permite crear APIs intuitivas pero seguras, combinando accesibilidad con control sobre las invariantes internas.
require
, check
y error
require(Boolean) { String }: Unit
valida argumentos de entrada. LanzaIllegalArgumentException
.check(Boolean) { String }: Unit
valida el estado interno del objeto. LanzaIllegalStateException
.error(String): Nothing
lanza incondicionalmente unIllegalStateException
.
Estas funciones permiten capturar errores de forma temprana y expresiva, con mensajes claros que facilitan el diagnóstico.
🧩 Ejercicio de cierre: 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.
Solución
var difficultyLevel: String = "normal"
private set(value) {
require(value in listOf("easy", "normal", "hard")) {
"Difficulty level must be one of: easy, normal, hard."
}
field = value
}
¿Qué acabamos de hacer?
- La propiedad
difficultyLevel
es pública para lectura, pero su setter es privado, lo que significa que solo puede modificarse desde el mismo archivo. - La función
require
se asegura de que solo se asignen valores válidos. Si se intenta asignar un valor distinto, se lanza una excepción. - Este patrón es útil cuando queremos proteger las invariantes del estado de la aplicación sin exponer directamente los mecanismos internos de control.
🎯 Conclusiones
A lo largo de esta lección aprendiste a declarar variables en Kotlin, diferenciando claramente entre referencias inmutables (val
) y mutables (var
). Exploramos cuándo conviene usar cada una y cómo Kotlin promueve el uso de propiedades en lugar de simples campos, lo que permite definir lógica de acceso segura, validaciones y encapsulamiento.
También descubriste que val
no significa que el contenido sea inmutable —solo que la referencia no puede cambiar—, y que las propiedades pueden tener getters y setters personalizados, lo cual es fundamental al diseñar bibliotecas robustas y seguras.
🔑 Puntos clave
val
declara referencias inmutables, ideales para expresar valores que no cambian.var
permite referencias mutables, útiles para modelar cambios de estado.- Kotlin trata las variables como propiedades, no como campos directos, lo que habilita control total mediante getters y setters.
- Es posible encapsular estructuras mutables internas usando campos de respaldo (
private val _campo
) y exponer vistas inmutables. - Los
setters
personalizados permiten validar y controlar el acceso de forma declarativa y segura. - Al diseñar bibliotecas, es clave exponer lo mínimo necesario y proteger las invariantes internas.
🧰 ¿Qué nos llevamos?
El diseño cuidadoso de variables y propiedades marca la diferencia entre una biblioteca frágil y una robusta. En Kotlin, las herramientas como val
, var
, getters y setters personalizados, y campos de respaldo te permiten controlar el acceso a la información, proteger el estado interno y hacer que tu API sea segura e intuitiva para otras personas.
Adoptar la inmutabilidad por defecto con val
no solo mejora la legibilidad, sino que también facilita el mantenimiento y reduce errores sutiles. En cambio, usar var
debe ser una decisión deliberada, motivada por una necesidad real de cambio de estado.
En resumen: escribe tus variables pensando en cómo serán usadas, qué deben permitir y qué deben evitar. Esa es la base de un buen diseño de bibliotecas.
📖 ¿Con ganas de más?
🔥 Referencias recomendadas
- 🌐 “Properties” en la documentación oficial de Kotlin:Explica cómo declarar y usar propiedades (
val
yvar
) en Kotlin, incluyendo su sintaxis completa con inicializadores, getters y setters personalizados. Describe cómo controlar la visibilidad de los accesores (private set
), cómo usar campos de respaldo (field
) para almacenar valores internamente y cómo crear propiedades computadas sin necesidad de almacenamiento. También aborda propiedades con inicialización diferida (lateinit
), constantes en tiempo de compilación (const val
) y patrones avanzados mediante propiedades delegadas. Ideal para entender cómo Kotlin permite encapsular, validar y exponer estado de forma segura y expresiva.