En este tutorial sobre Scala vamos a ver las ventajas de declarar una clase con la palabra clave case y las implicaciones que puede tener a nivel de generación de código. Posteriormente, veremos la ventaja de utilizarlo en una estructura match, a través del pattern matching.
En caso de que necesites una introducción a Scala, puedes consultar nuestro tutorial de Primeros pasos en Scala. Si quieres seguir aprendiendo sobre este potente lenguaje de programación, te recomendamos que consultes la página de nuestro curso specializado en Scala.
Las Clases Case en Scala
El primer uso importante es poder utilizar la clase en una estructura match. Para poder entenderlo al completo, veremos la forma en la que el compilador de Scala realiza el pattern matching, que en esencia se basa en la identificación de patrones para facilitar el trabajo de programación y la simplificación del código.
Declaración de una clase
Empecemos por lo básico, cuando declaramos una clase con la palabra clave case, de la siguiente forma:
1case class Persona(nombre: String, edad: Int)
El compilador genera el siguiente código:
1class Persona(val nombre: String, val edad: Int)
2 extends Product with Serializable
3{
4 def copy(nombre: String = this.nombre, edad: Int = this.edad): Persona =
5 new Persona(nombre, edad)
6
7 def productArity: Int = 2
8
9 def productElement(i: Int): Any = i match {
10 case 0 => nombre
11 case 1 => edad
12 case _ => throw new IndexOutOfBoundsException(i.toString)
13 }
14
15 def productIterator: Iterator[Any] =
16 scala.runtime.ScalaRunTime.typedProductIterator(this)
17
18 def productPrefix: String = "Persona"
19
20 def canEqual(obj: Any): Boolean = obj.isInstanceOf[Persona]
21
22 override def hashCode(): Int = scala.runtime.ScalaRunTime._hashCode(this)
23
24 override def equals(obj: Any): Boolean = this.eq(obj) || obj match {
25 case that: Persona => this.nombre == that.nombre && this.edad == that.edad
26 case _ => false
27 }
28
29 override def toString: String =
30 scala.runtime.ScalaRunTime._toString(this)
31}
Además, genera un objeto acompañante con los siguientes métodos:
1object Persona extends AbstractFunction2[String, Int, Persona] with Serializable {
2 def apply(nombre: String, edad: Int): Persona = new Persona(nombre, edad)
3
4 def unapply(p: Persona): Option[(String, Int)] =
5 if(p == null) None else Some((p.nombre, p.edad))
6}
Ciertamente, es mucho el código generado para una palabra tan corta como case, ¿verdad?.
Esta es una de las grandes ventajas de Scala. Es un lenguaje que está pensado para hacernos la vida muy fácil. Pero, ¿qué hace una clase case que no haga una clase normal? ¿Qué ganamos con todo este código generado?
Ventajas de Scala
Estas son las ventajas:
- Los objetos que provienen de clases case, se pueden comparar a nivel de valor. Si tenemos p1 y p2, siendo de la clase Persona, p1 p2, comparará si el nombre y la edad son idénticos, y devolverá true en el caso de que ambos atributos (nombre y edad) devuelvan true a la hora de compararlos. Es como hacer p1.nombre p2.nombre && p1.edad == p2.edad. Útil, ¿no es así?.
- Podemos copiar de una manera sencilla un objeto. Para ello, es tan fácil como invocar el método copy sobre un objeto de tipo Persona. Por ejemplo, haciendo val p2 = p1.copy() haremos una copia de p1 en p2. También podemos copiar el objeto pero modificando un atributo. Haciendo val p2 = p1.copy(nombre = "Pepe"), conseguiremos una persona con los mismos atributos que p1 pero de nombre Pepe.
- Se pueden crear objetos sin utilizar la palabra new. No es que sea el mejor truco del mundo, pero ahí está. Ahora para crear una Persona sólo tienes que hacer val persona = Persona("Pepe", 20).
- VENTAJA NÚMERO 1: A partir de ahora, podremos utilizar cualquier variable de tipo Persona dentro de una estructura match.
1persona match {
2 case Persona("Pepe", _) => println("Hemos encontrado a Pepe")
3 case Persona(_, 40) => println("Alguien tiene 40 años")
4 case _ => println("Ni es Pepe ni tiene 40")
5}
Si observas con atención, utilizar match sobre un objeto definido por nosotros es muy ventajoso. En el primer caso, cualquier objeto que tenga como nombre "Pepe" hará match y ejecutará el código de la derecha de la flecha. En el segundo caso, buscamos un objeto cuyo nombre no nos importe pero cuya edad sea concretamente 40.
NOTA para los Java. LoversSi vienes de Java, te parecerá curioso no ver ningún break. Esto es así en Scala, no es necesario que cada línea se finalice con un break. Sólo se ejecutará la línea o bloque de la derecha de la flecha y una vez hecho, el compilador irá a la siguiente línea de después del match.
Esta breve introducción a la funcionalidad de las clases case, ya a simple vista, aporta a Scala una versatilidad que es difícil de encontrar en otros lenguajes, como por ejemplo Java. En el siguiente apartado, veremos formas de escribir nuestros bloques match.
Pattern Matching en Scala
Si has llegado a este punto, seguro que estás diciendo: "Scala mola, pero… ¿cómo comparo los valores de los atributos del objeto?" o "¿Cómo recupero el objeto para modificarlo o utilizarlo después de la flecha?".
Tienen mucho sentido tus dudas. Veamos por lo tanto las posibilidades que nos ofrece la estructura match.
Acceso a atributos
El siguiente código muestra un ejemplo para recuperar el nombre de un objeto Persona:
1persona match {
2 case Persona("Pepe", _) => println("Se trata de Pepe")
3 case Persona(nombre, _) => println(s"Se trata de ${nombre}")
4}
Como puedes observar, dándole un nombre al atributo en cuestión, lo podremos utilizar en la parte de la acción del case.
Acceso a los objetos
Veamos el siguiente código:
1persona match {
2 case Persona("Pepe", _) => println("Se trata de Pepe")
3 case p: Persona => println(s"Se trata de ${p.nombre}")
4}
Esta es una forma muy típica cuando la variable persona, puede ser de varios tipos (o incluso Any). Gracias al pattern matching, identificamos el tipo y accedemos al objeto a través de p.
Lo lógico es que te hagas la siguiente pregunta: "Pero si ya tengo la variable persona, ¿por qué no accedo directamente a ella?". Ok, tienes razón. Vamos con un ejemplo más completo:
1abstract class Persona {
2 def nombre: String
3 def edad: Int
4}
5
6case class Conductor(val nombre: String, val edad: Int) extends Persona
7{
8 def conducir() = println(s"${nombre} está conduciendo")
9}
10
11case class Viajero (val nombre: String, val edad: Int) extends Persona
12{
13 def pagarBillete() = println(s"El viajero ${nombre} ha comprado un billete")
14}
15
16def realizarAccion(persona: Persona) = {
17 persona match {
18 case c: Conductor => c.conducir()
19 case v: Viajero => v.pagarBillete()
20 }
21}
22
23val listadoPersonas : List[Persona] = List(Viajero("Antonio", 25), Conductor("Pepe", 40))
24
25listadoPersonas.map(realizarAccion(_))
Y su funcionamiento explicado:
- Definimos la clase Persona como abstracta, porque no nos interesa que se instancie. Además, ya no necesita que exista un constructor con signatura (anteriormente el constructor era Persona(nombre: String, edad: Int)).
- Conductor y Viajero heredan de Persona pero cada uno implementa un método diferente.
- Creamos una lista con 2 personas, un Viajero y un Conductor.
- Sobre esa lista y utilizando map, ejecutamos sobre cada objeto de la lista la función realizarAccion.
- Dentro de realizarAccion y según el objeto persona sea de tipo Conductor o Viajero, ejecutaremos el método correspondiente (conducir o pagarBillete).
Y este es un gran comienzo si te estás adentrando en el mundo de Scala, porque es la herramienta base para construir programas fáciles de leer, fáciles de mantener y potentes como en Java.
Aprende el lenguaje de programación Scala
Si deseas aprender más sobre Scala y aprovechar al máximo sus capacidades, te invitamos a consultar nuestro curso specializado, donde encontrarás recursos y tutoriales prácticos para dominar este emocionante lenguaje. ¡No pierdas la oportunidad de llevar tus habilidades de programación al siguiente nivel con Scala!