Skip to main content

Tipos producto como registros y data class

⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.


En la lección anterior exploramos cómo las clases comunes en Kotlin pueden utilizarse para representar tipos producto: estructuras que agrupan múltiples valores en una sola entidad, como Position(x, y) o Person(name, age). Aprendimos a nombrar explícitamente cada campo y a encapsular lógica relevante dentro de la clase, superando las limitaciones de tipos genéricos como Pair o Triple.

En esta lección llevaremos esa idea un paso más allá, introduciendo el concepto de registro (record), una abstracción común en muchos lenguajes orientados a datos y programación funcional. Veremos cómo Kotlin proporciona un soporte conciso y expresivo para este patrón mediante las data class, y qué ventajas ofrece frente a las clases tradicionales.

Nuestro objetivo será comprender cómo los registros —y en particular las data class de Kotlin— permiten modelar tipos producto de forma más declarativa, segura y reutilizable, lo cual resulta especialmente útil en el contexto del diseño de bibliotecas.

📘 ¿Qué es un registro (record)?

En programación, un registro (o record) es una estructura de datos que agrupa múltiples valores en una sola entidad, donde cada valor está asociado a un nombre explícito. Es una forma de representar datos estructurados, es decir, información compuesta que transmite una intención semántica clara.

Desde la teoría de tipos, un registro corresponde a un caso particular de tipo producto: un tipo que posee simultáneamente un valor para cada uno de sus campos.

Por ejemplo, para representar una coordenada bidimensional, podríamos definir un registro con los campos x e y:

Registro como coordenada bidimensional
val point = Coordinate(x = 2, y = 5)

A diferencia de listas o tuplas, un registro asigna un nombre a cada componente, lo que mejora la legibilidad, refuerza la intención del diseño y facilita el mantenimiento. Esta claridad es especialmente valiosa al diseñar tipos reutilizables dentro de bibliotecas.

🆚 ¿En qué se diferencia de una clase u objeto?

Aunque un registro puede parecerse superficialmente a una clase, su propósito es más limitado y está orientado exclusivamente al modelo de datos:

CaracterísticaClases tradicionalesRegistros (record, data class, etc.)
PropósitoModelar entidades con comportamientoModelar estructuras de datos simples
Comparación (==)Por referencia (por defecto)Por contenido (campos)
MutabilidadComúnmente mutablesUsualmente inmutables por convención
Lógica internaPueden encapsular lógica complejaLimitados a almacenar y exponer datos
En resumen

Una clase modela una entidad con identidad, estado mutable y comportamiento asociado.

Un registro modela una colección inmutable de datos con nombre, comparable por valor, sin lógica compleja, ideal para representar tipos producto planos y transparentes.

🌍 Registros en distintos lenguajes

Muchos lenguajes ofrecen mecanismos específicos para definir registros, es decir, estructuras inmutables y comparables por valor:

  • En Haskell y ML, los records son estructuras inmutables con campos nombrados y soporte para desestructuración.
  • En Python, herramientas como namedtuple, dataclass y attrs permiten definir contenedores de datos con comparaciones por valor y generación automática de métodos.
  • En Java, a partir de la versión 16, los record permiten declarar tipos centrados en datos de forma concisa y segura.
  • En Scala, las case class ofrecen inmutabilidad, desestructuración y comparación por valor de manera idiomática.
  • En Kotlin, las data class son el mecanismo principal para representar registros, con soporte directo para copy, toString, equals, hashCode, y desestructuración.

Aunque varían en sintaxis y detalles, todos estos mecanismos comparten un objetivo común: representar datos con estructura explícita, semántica clara y sin lógica incidental.

📦 ¿Qué es una data class?

En Kotlin, una data class es la forma idiomática de definir registros: estructuras que agrupan varios valores con nombre y se comparan por contenido.

Al marcar una clase como data, el compilador genera automáticamente:

  • equals() y hashCode() para comparación por contenido, no por referencia (ideal para usar como claves en mapas, sets o para deduplicación).
  • toString() para imprimir una representación legible (útil en logs, debugging y documentación).
  • copy() para clonar objetos inmutables con modificaciones puntuales.
  • Funciones componentN() para desestructurar fácilmente los valores en expresiones y funciones.

Estas características hacen que las data class sean ideales para representar tipos producto.

Definición de un mago como data class
data class Wizard(
val name: String,
val magic: String,
val powerLevel: Int
)

Este tipo representa el producto cartesiano:

String×String×Int\text{String} \times \text{String} \times \text{Int}

Cada instancia combina un nombre, un tipo de magia y un nivel de poder. Su propósito es expresar datos, no lógica compleja ni identidad mutable.

🎭 Sin data class

Este ejemplo muestra cómo sería implementar manualmente una clase que represente un registro. Aunque es completamente válido, resulta verboso, propenso a errores y repetitivo, especialmente si lo comparamos con una data class, que genera automáticamente todo este comportamiento:

Definición de un mago como clase normal
class Wizard(
val name: String,
val magic: String,
val powerLevel: Int
) {
fun copy(
name: String = this.name,
magic: String = this.magic,
powerLevel: Int = this.powerLevel
) = Wizard(name, magic, powerLevel)

operator fun component1(): String = name
operator fun component2(): String = magic
operator fun component3(): Int = powerLevel

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is Wizard) return false

return name == other.name &&
magic == other.magic &&
powerLevel == other.powerLevel
}

override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + magic.hashCode()
result = 31 * result + powerLevel
return result
}

override fun toString() =
"Wizard(name='$name', magic='$magic', powerLevel=$powerLevel)"
}
¿Por qué importa?

Este ejemplo ilustra lo que hace el compilador de Kotlin al marcar una clase como data.

  • Ahorro de código: una data class genera automáticamente copy, componentN, equals, hashCode y toString.
  • Comportamiento predecible: evita errores sutiles que pueden surgir al implementar manualmente métodos como equals.
  • Intención clara: al usar data, declaras explícitamente que el propósito de la clase es representar datos con semántica por valor.

Este patrón es fundamental al diseñar bibliotecas que manejan estructuras inmutables, comparables y fáciles de serializar o probar.

🥊 Comparación por contenido vs referencia

Al representar datos, es clave entender cómo se comparan las instancias. En Kotlin:

  • Las clases comunes comparan por referencia (===), es decir, evalúan si apuntan al mismo objeto en memoria.
  • Las data class comparan por contenido (==), es decir, evalúan si los campos tienen los mismos valores.

Veamos un ejemplo inspirado en Rocky Balboa:

Clase común: comparación por referencia
class Boxer(val name: String, val weight: Int)

val rocky1 = Boxer("Rocky", 91)
val rocky2 = Boxer("Rocky", 91)

println(rocky1 == rocky2) // false
Data class: comparación por contenido
data class Boxer(val name: String, val weight: Int)

val rocky1 = Boxer("Rocky", 91)
val rocky2 = Boxer("Rocky", 91)

println(rocky1 == rocky2) // true
¿Qué acabamos de hacer?

Este ejemplo muestra una diferencia clave entre clases comunes y data class:

  • En la primera versión, aunque los valores son idénticos, los objetos son distintos en memoria → false.
  • En la segunda, data class genera automáticamente equals, por lo que se comparan los campos → true.

Esta capacidad de comparar por contenido es fundamental para representar tipos producto, donde nos interesa el valor, no la identidad del objeto.

🤔 ¿Y si quiero comparar por referencia?

En Kotlin, existen dos operadores para comparar objetos:

  • == compara por contenido: se traduce en una llamada a equals().
    Las data class sobrescriben equals automáticamente, por lo que == compara sus campos.
  • === compara por referencia: verifica si ambas variables apuntan al mismo objeto en memoria.
val a = Boxer("Rocky", 91)
val b = a
val c = Boxer("Rocky", 91)

println(a == c) // true → mismo contenido
println(a === c) // false → objetos distintos
println(a === b) // true → misma instancia
tip

Usa === solo cuando necesites verificar identidad de objetos.
Para la mayoría de los casos con tipos producto, la comparación por contenido (==) es más adecuada y segura.

🎯 Conclusiones

A lo largo de esta lección aprendimos que los registros son estructuras diseñadas para representar datos con nombre y propósito explícito. En Kotlin, su forma idiomática son las data class, que permiten declarar tipos producto de forma concisa, expresiva y segura.

Las data class agrupan múltiples valores en una sola entidad y delegan en el compilador la generación de comportamientos comunes como equals, toString, copy y componentN. Esto reduce la repetición de código, mejora la legibilidad y evita errores frecuentes al trabajar con datos estructurados.

También discutimos cómo estas decisiones de diseño impactan en la igualdad estructural, la inmutabilidad por convención, y la facilidad para crear APIs limpias y coherentes.

🔑 Puntos clave

  • Un registro es una forma de representar un tipo producto con campos nombrados, pensado para modelar datos.
  • Las data class son la forma idiomática de definir registros en Kotlin.
  • El compilador genera automáticamente métodos útiles como equals, hashCode, toString, copy y componentN.
  • A diferencia de las clases comunes, las data class comparan por contenido, no por referencia.
  • Son especialmente útiles para diseñar tipos transparentes, inmutables y fácilmente testeables.

🧰 ¿Qué nos llevamos?

Comprender el rol de los registros y el uso adecuado de data class nos permite escribir código más claro, seguro y reutilizable. Este enfoque mejora no solo la implementación de tipos internos, sino también la legibilidad y mantenibilidad de nuestras bibliotecas.

Al adoptar data class como representación idiomática de los tipos producto, aprovechamos lo mejor del diseño declarativo con el respaldo de un compilador que nos asiste en tareas repetitivas y propensas a errores.

📖 Referencias

🔥 Recomendadas

  • 📚 "Data Classes" (pp. 198-200) en "Atomic Kotlin" de Bruce Eckel y Dmitry Jemerov: muestra cómo las data class eliminan código repetitivo al modelar tipos producto: generan automáticamente equals, toString, copy y hashCode, lo que facilita la comparación por contenido, el uso en estructuras como HashMap y HashSet, y la creación de nuevas instancias con modificaciones. Es relevante para esta lección porque muestra claramente cómo las data class implementan el concepto de registro en Kotlin de forma idiomática y eficiente.

🔹 Adicionales

  • 📚 “Objects and Classes.” (pp. 107-132) en Programming Kotlin: Create Elegant, Expressive, and Performant JVM and Android Applications de Venkat Subramaniam: Este capítulo explora cómo Kotlin permite definir clases y registros de manera concisa y expresiva, eliminando la necesidad de escribir código repetitivo. Introduce las data class como una forma idiomática de representar tipos producto inmutables y comparables por valor, destacando cómo el compilador genera automáticamente métodos como equals, hashCode, toString, copy y componentN(). Además, compara este enfoque con las clases tradicionales, analiza los beneficios de la inmutabilidad, y presenta patrones de diseño como objetos singleton y constructores personalizados. Es especialmente relevante para esta lección porque explica en profundidad cómo las data class implementan el concepto de registro en Kotlin y cómo esto mejora el diseño de estructuras de datos claras, reutilizables y seguras.