
La delegación de propiedades es un mecanismo que permite que la lógica de lectura y escritura de una propiedad se externalice a otro objeto. En lugar de escribir un getter y un setter personalizado en cada clase, se declara la propiedad con la palabra clave by y se apunta a un delegado que define ese comportamiento una sola vez.
La biblioteca estandar ofrece varios delegados listos para los casos mas frecuentes: inicialización perezosa, observación de cambios, validación antes de asignar, almacenamiento respaldado por un mapa y propiedades no nulas con asignación diferida. Cuando ninguno encaja, es posible implementar un delegado propio con muy poco código.
Sintaxis general
La forma general es val nombre: Tipo by delegado. Cada vez que se lea la propiedad, el delegado recibe la llamada getValue, y si es mutable, cada escritura invoca setValue.
import kotlin.reflect.KProperty
class TiempoActual {
operator fun getValue(thisRef: Any?, property: KProperty<*>): Long {
return System.currentTimeMillis()
}
}
class Reloj {
val ahora: Long by TiempoActual()
}
fun main() {
val reloj = Reloj()
println(reloj.ahora)
Thread.sleep(10)
println(reloj.ahora)
}
Cada acceso a reloj.ahora delega en TiempoActual.getValue y devuelve un valor nuevo. Con este patrón, la propiedad deja de ser un campo estático y se convierte en un acceso dinámico sin tener que escribir un getter manual.
Inicialización perezosa con by lazy
El delegado lazy evalua el bloque la primera vez que se lee la propiedad y cachea el resultado. A partir de ese momento cualquier lectura devuelve el valor ya calculado. Es el patrón idoneo para inicializaciones costosas que pueden no llegar a necesitarse.
class Configuracion {
val ajustes: Map<String, String> by lazy {
println("Cargando ajustes desde disco")
mapOf("tema" to "oscuro", "idioma" to "es-ES")
}
}
fun main() {
val c = Configuracion()
println("Antes del primer acceso")
println(c.ajustes["tema"])
println(c.ajustes["idioma"])
}
La traza imprime "Cargando ajustes desde disco" una sola vez. Las lecturas posteriores no repiten la carga. Por defecto, lazy es seguro para llamadas desde múltiples hilos gracias a un bloqueo con doble comprobación. Si la propiedad solo se va a leer desde un hilo, se puede pasar LazyThreadSafetyMode.NONE para evitar el coste del bloqueo.
val datos: List<Int> by lazy(LazyThreadSafetyMode.NONE) {
(1..1_000_000).toList()
}
by lazysolo funciona sobre propiedadesval. Si necesitas reinicializar, usa otro delegado o una referencia explícita aLazy<T>.
Observación de cambios con Delegates.observable
El delegado observable permite ejecutar un callback cada vez que el valor cambia. Recibe el nombre de la propiedad, el valor anterior y el nuevo. Es muy útil para logs, actualización de interfaces o auditoría de datos.
import kotlin.properties.Delegates
class Sesion {
var usuario: String by Delegates.observable("anonimo") { prop, anterior, nuevo ->
println("[${prop.name}] ${anterior} -> ${nuevo}")
}
}
fun main() {
val sesion = Sesion()
sesion.usuario = "ana"
sesion.usuario = "luis"
}
La salida muestra cada transición. Como el callback se ejecuta después de la asignación, el estado ya refleja el nuevo valor cuando se invoca. Para reaccionar antes de asignar y poder rechazar el cambio, existe Delegates.vetoable.
Validación con Delegates.vetoable
vetoable se comporta como observable, pero el lambda devuelve un Boolean. Si retorna false, la asignación se descarta y la propiedad conserva su valor previo.
import kotlin.properties.Delegates
class CampoEdad {
var edad: Int by Delegates.vetoable(0) { _, anterior, nuevo ->
nuevo in 0..120
}
}
fun main() {
val c = CampoEdad()
c.edad = 35
c.edad = -1
c.edad = 250
println(c.edad)
}
La salida es 35. Los intentos de asignar valores fuera de rango se rechazan sin necesidad de lanzar excepciones. Este patrón es útil para campos con invariantes sencillos cuando no se quiere contaminar el setter con lógica.
Propiedades no nulas con Delegates.notNull
Una limitación clásica de las propiedades var no nulas es que deben inicializarse en el constructor. Si el valor llega mas tarde (por ejemplo desde una respuesta de red), se suele recurrir a lateinit o a un tipo nullable. El delegado notNull ofrece una alternativa para tipos primitivos que no admiten lateinit.
import kotlin.properties.Delegates
class Respuesta {
var codigo: Int by Delegates.notNull()
}
fun main() {
val r = Respuesta()
r.codigo = 200
println(r.codigo)
}
Leer codigo antes de asignarlo lanza IllegalStateException. Para tipos referencia, lateinit var es la opción habitual; notNull cubre el hueco de los tipos primitivos como Int, Long o Boolean.
Delegación a un Map o MutableMap
Cuando se leen datos desde JSON, formularios o atributos dinámicos, es frecuente tener un mapa con claves de tipo String. Kotlin permite que las propiedades de una clase lean su valor directamente de ese mapa usando el nombre de la propiedad como clave.
class ConfigUsuario(map: Map<String, Any?>) {
val nombre: String by map
val edad: Int by map
val vip: Boolean by map
}
fun main() {
val datos = mapOf("nombre" to "Ana", "edad" to 30, "vip" to true)
val cfg = ConfigUsuario(datos)
println("${cfg.nombre}, ${cfg.edad}, vip=${cfg.vip}")
}
La versión mutable (MutableMap) también permite escribir, de modo que una asignación en la propiedad altera el mapa subyacente. Es un puente elegante entre estructuras dinámicas y un modelo tipado.
Cómo funciona la delegación
La clase declarante no almacena el estado directamente. Es el delegado quien asume la responsabilidad de calcularlo, cachearlo o persistirlo según el comportamiento deseado: cada lectura invoca getValue y cada escritura setValue con la propia KProperty como metadato.
Crear un delegado propio
Implementar un delegado personalizado solo requiere exponer los operadores getValue y, si la propiedad es mutable, setValue. Las interfaces ReadOnlyProperty y ReadWriteProperty ofrecen una forma declarativa.
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class AuditoriaSimple<T>(inicial: T) : ReadWriteProperty<Any?, T> {
private var valor: T = inicial
private val historial = mutableListOf<T>()
override fun getValue(thisRef: Any?, property: KProperty<*>): T = valor
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
historial += valor
valor = value
}
fun historial(): List<T> = historial.toList()
}
class Documento {
private val tituloDelegado = AuditoriaSimple("Sin titulo")
var titulo: String by tituloDelegado
fun historialTitulo(): List<String> = tituloDelegado.historial()
}
fun main() {
val doc = Documento()
doc.titulo = "Borrador"
doc.titulo = "Revision 1"
doc.titulo = "Version final"
println(doc.titulo)
println(doc.historialTitulo())
}
El delegado AuditoriaSimple mantiene un historial de valores previos y expone una lectura normal para el cliente. Es un patrón útil para campos auditados en dominios financieros o de trazabilidad sin ensuciar la clase principal con listas y lógica accesoria.
Cuando usar delegación y cuando no
La delegación brilla cuando existe un comportamiento cruzado que se repite en varias propiedades o clases. Algunos criterios orientan la elección.
- 1. Para inicialización perezosa y cara:
by lazyevita trabajo innecesario y cachea el resultado. - 2. Para reaccionar a cambios:
observableyvetoableencapsulan el patrón observer sin boilerplate. - 3. Para integrar con mapas dinámicos:
by mapconecta datos externos con un tipo fuerte. - 4. Para compartir lógica entre clases: extraer un delegado es mas limpio que repetir getters y setters.
- 5. Cuando el comportamiento es específico de una sola propiedad simple: un getter o setter tradicional puede ser mas claro.
Un delegado resuelve bien problemas transversales. Si solo lo vas a usar en un sitio, valora si la complejidad adicional compensa.
La delegación de propiedades, junto con las scope functions y las extensiones, es una de las piezas que convierten a Kotlin en un lenguaje muy expresivo. Permite escribir clases que leen como una declaración del que en lugar del como, delegando en componentes reutilizables las partes mecanicas de la persistencia, la validación o la observación.
Combinación de delegados
Los delegados se pueden componer cuando el problema lo requiere. Un patrón habitual consiste en envolver un delegado base (por ejemplo observable) con uno propio que anada persistencia o difusión de cambios.
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class Persistente<T>(inicial: T, private val guardar: (T) -> Unit) : ReadWriteProperty<Any?, T> {
private var valor: T = inicial
override fun getValue(thisRef: Any?, property: KProperty<*>): T = valor
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
valor = value
guardar(value)
}
}
class Preferencias(almacenamiento: MutableMap<String, Any?>) {
var idioma: String by Persistente("es-ES") { almacenamiento["idioma"] = it }
var modoOscuro: Boolean by Persistente(false) { almacenamiento["modoOscuro"] = it }
}
fun main() {
val disco = mutableMapOf<String, Any?>()
val prefs = Preferencias(disco)
prefs.idioma = "en-US"
prefs.modoOscuro = true
println(disco)
}
El delegado Persistente encapsula la sincronización con un almacen externo. Las clases cliente declaran propiedades normales y la clase del delegado asume el coste de la persistencia, testeable de forma aislada.
Delegación de implementación de interfaces
Además de propiedades, Kotlin admite delegar la implementación completa de una interfaz a otro objeto con el operador by. Este mecanismo es distinto pero complementario, porque comparte la palabra clave y el espiritu de delegar trabajo a un colaborador.
interface Repositorio<T> {
fun guardar(item: T)
fun obtener(id: String): T?
}
class RepositorioMemoria<T> : Repositorio<T> {
private val datos = mutableMapOf<String, T>()
override fun guardar(item: T) {
val id = item.toString().hashCode().toString()
datos[id] = item
}
override fun obtener(id: String): T? = datos[id]
}
class RepositorioConLog<T>(
delegado: Repositorio<T>
) : Repositorio<T> by delegado {
override fun guardar(item: T) {
println("Guardando $item")
(this as Repositorio<T>).guardar(item)
}
}
Con Repositorio<T> by delegado, la nueva clase recibe gratis la implementación de los métodos de la interfaz. Solo se redefinen los que interesa interceptar. Este patrón es el principio preferir composición sobre herencia llevado al lenguaje.
No confundas delegación de propiedades con delegación de interfaces. La primera afecta a un campo concreto. La segunda sustituye toda la implementación por la de otro objeto.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Kotlin es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de Kotlin
Explora más contenido relacionado con Kotlin y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Usar la delegación de propiedades con by lazy, Delegates.observable, Delegates.vetoable, delegación a Map y creación de delegados personalizados con ReadWriteProperty.