Capacitación de Kotlin para programadores 5.1: Extensiones

Este codelab es parte del curso de capacitación de Kotlin para programadores. Aprovecharás al máximo este curso si trabajas con los codelabs de forma secuencial. Según tus conocimientos, es posible que puedas cambiar algunas secciones. Este curso está dirigido a programadores que conocen un lenguaje orientado a objetos y quieren aprender Kotlin.

Introducción

En este codelab, se te presentarán varias funciones útiles diferentes en Kotlin, como pares, colecciones y funciones de extensión.

En lugar de crear una sola app de ejemplo, las lecciones de este curso están diseñadas para ampliar tus conocimientos, pero son semiindependientes entre sí para que puedas revisar las secciones con las que ya estás familiarizado. Para unirlos, muchos de los ejemplos usan un tema de acuario. Si quieres ver la historia completa del acuario, consulta el curso de Udacity Kotlin Bootcamp for Programmers.

Conocimientos que ya deberías tener

  • La sintaxis de las funciones, las clases y los métodos de Kotlin
  • Cómo trabajar con el REPL (bucle de lectura, evaluación e impresión) de Kotlin en IntelliJ IDEA
  • Cómo crear una clase nueva en IntelliJ IDEA y ejecutar un programa

Qué aprenderás

  • Cómo trabajar con pares y tríos
  • Más información sobre las colecciones
  • Cómo definir y usar constantes
  • Cómo escribir funciones de extensión

Actividades

  • Aprende sobre pares, tríos y mapas de hash en el REPL
  • Aprende diferentes formas de organizar las constantes
  • Escribe una función de extensión y una propiedad de extensión

En esta tarea, aprenderás sobre los pares y las tríadas, y cómo desestructurarlos. Los pares y las tríadas son clases de datos prediseñadas para 2 o 3 elementos genéricos. Por ejemplo, esto puede ser útil para que una función devuelva más de un valor.

Supongamos que tienes un List de peces y una función isFreshWater() para verificar si el pez es de agua dulce o salada. List.partition() devuelve dos listas: una con los elementos en los que la condición es true y otra con los elementos en los que la condición es false.

val twoLists = fish.partition { isFreshWater(it) }
println("freshwater: ${twoLists.first}")
println("saltwater: ${twoLists.second}")

Paso 1: Crea algunos pares y tríos

  1. Abre el REPL (Tools > Kotlin > Kotlin REPL).
  2. Crea un par que asocie un equipo con su uso y, luego, imprime los valores. Puedes crear un par con una expresión que conecte dos valores, como dos cadenas, con la palabra clave to y, luego, usar .first o .second para hacer referencia a cada valor.
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
  1. Crea una tripleta y muéstrala con toString(). Luego, conviértela en una lista con toList(). Creas una tripleta con Triple() y 3 valores. Usa .first, .second y .third para hacer referencia a cada valor.
val numbers = Triple(6, 9, 42)
println(numbers.toString())
println(numbers.toList())
⇒ (6, 9, 42)
[6, 9, 42]

En los ejemplos anteriores, se usa el mismo tipo para todas las partes del par o la tripleta, pero no es obligatorio. Las partes pueden ser una cadena, un número o una lista, por ejemplo, incluso otro par o trío.

  1. Crea un par en el que la primera parte del par sea en sí misma un par.
val equipment2 = ("fish net" to "catching fish") to "equipment"
println("${equipment2.first} is ${equipment2.second}\n")
println("${equipment2.first.second}")
⇒ (fish net, catching fish) is equipment
⇒ catching fish

Paso 2: Desestructura algunos pares y tríos

La separación de pares y tríos en sus partes se denomina desestructuración. Asigna el par o la tripleta a la cantidad adecuada de variables, y Kotlin asignará el valor de cada parte en orden.

  1. Desestructura un par e imprime los valores.
val equipment = "fish net" to "catching fish"
val (tool, use) = equipment
println("$tool is used for $use")
⇒ fish net is used for catching fish
  1. Desestructura una tupla y, luego, imprime los valores.
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42

Ten en cuenta que la desestructuración de pares y tríos funciona de la misma manera que con las clases de datos, lo que se abordó en un codelab anterior.

En esta tarea, aprenderás más sobre las colecciones, incluidas las listas, y un nuevo tipo de colección, los mapas de hash.

Paso 1: Obtén más información sobre las listas

  1. En una lección anterior, se presentaron las listas y las listas mutables. Son una estructura de datos muy útil, por lo que Kotlin proporciona varias funciones integradas para las listas. Revisa esta lista parcial de funciones para listas. Puedes encontrar listados completos en la documentación de Kotlin para List y MutableList.

Función

Purpose

add(element: E)

Agrega un elemento a la lista mutable.

remove(element: E)

Quita un elemento de una lista mutable.

reversed()

Devuelve una copia de la lista con los elementos en orden inverso.

contains(element: E)

Devuelve true si la lista contiene el elemento.

subList(fromIndex: Int, toIndex: Int)

Devuelve parte de la lista, desde el primer índice hasta el segundo, sin incluirlo.

  1. Mientras sigues trabajando en el REPL, crea una lista de números y llama a sum() en ella. Esto suma todos los elementos.
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
  1. Crea una lista de cadenas y suma los elementos de la lista.
val list2 = listOf("a", "bbb", "cc")
println(list2.sum())
⇒ error: none of the following functions can be called with the arguments supplied:
  1. Si el elemento no es algo que List sabe cómo sumar directamente, como una cadena, puedes especificar cómo sumarlo con .sumBy() con una función lambda, por ejemplo, para sumar según la longitud de cada cadena. El nombre predeterminado para un argumento lambda es it, y aquí it hace referencia a cada elemento de la lista a medida que se recorre.
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
  1. Hay mucho más que puedes hacer con las listas. Una forma de ver la funcionalidad disponible es crear una lista en IntelliJ IDEA, agregar el punto y, luego, observar la lista de autocompletado en la sugerencia. Esto funciona para cualquier objeto. Pruébalo con una lista.

  1. Elige listIterator() en la lista, luego recorre la lista con una instrucción for y, por último, imprime todos los elementos separados por espacios.
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
    println("$s ")
}
⇒ a bbb cc

Paso 2: Prueba los mapas hash

En Kotlin, puedes asignar casi cualquier cosa a cualquier otra cosa con hashMapOf(). Los mapas de hash son como una lista de pares, en la que el primer valor actúa como clave.

  1. Crea un mapa de hash que coincida con los síntomas (las claves) y las enfermedades de los peces (los valores).
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  1. Luego, puedes recuperar el valor de la enfermedad según la clave del síntoma con get() o, incluso, con corchetes [].
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
  1. Intenta especificar un síntoma que no esté en el mapa.
println(cures["scale loss"])
⇒ null

Si una clave no está en el mapa, intentar devolver la enfermedad coincidente devuelve null. Según los datos del mapa, es posible que no haya coincidencias para una posible clave. Para esos casos, Kotlin proporciona la función getOrDefault().

  1. Intenta buscar una clave que no coincida con getOrDefault().
println(cures.getOrDefault("bloating", "sorry, I don't know"))
⇒ sorry, I don't know

Si necesitas hacer algo más que devolver un valor, Kotlin proporciona la función getOrElse().

  1. Cambia tu código para usar getOrElse() en lugar de getOrDefault().
println(cures.getOrElse("bloating") {"No cure for this"})
⇒ No cure for this

En lugar de devolver un valor predeterminado simple, se ejecuta el código que se encuentre entre las llaves {}. En el ejemplo, else simplemente devuelve una cadena, pero podría ser tan sofisticado como encontrar una página web con una cura y devolverla.

Al igual que mutableListOf, también puedes crear un mutableMapOf. Un mapa mutable te permite agregar y quitar elementos. Mutable solo significa que se puede cambiar, mientras que inmutable significa que no se puede cambiar.

  1. Crea un mapa de inventario que se pueda modificar y que asigne una cadena de equipo a la cantidad de elementos. Crea el tanque con una red de pesca, luego agrega 3 esponjas para tanques al inventario con put() y quita la red de pesca con remove().
val inventory = mutableMapOf("fish net" to 1)
inventory.put("tank scrubber", 3)
println(inventory.toString())
inventory.remove("fish net")
println(inventory.toString())
⇒ {fish net=1, tank scrubber=3}{tank scrubber=3}

En esta tarea, aprenderás sobre las constantes en Kotlin y las diferentes formas de organizarlas.

Paso 1: Obtén información sobre const y val

  1. En el REPL, intenta crear una constante numérica. En Kotlin, puedes crear constantes de nivel superior y asignarles un valor en el tiempo de compilación con const val.
const val rocks = 3

El valor se asigna y no se puede cambiar, lo que se parece mucho a declarar un val normal. Entonces, ¿cuál es la diferencia entre const val y val? El valor de const val se determina en el momento de la compilación, mientras que el valor de val se determina durante la ejecución del programa, lo que significa que una función puede asignar val en el tiempo de ejecución.

Esto significa que se le puede asignar un valor a val desde una función, pero no a const val.

val value1 = complexFunctionCall() // OK
const val CONSTANT1 = complexFunctionCall() // NOT ok

Además, const val solo funciona en el nivel superior y en las clases singleton declaradas con object, no con clases normales. Puedes usarlo para crear un archivo o un objeto singleton que solo contenga constantes y, luego, importarlos según sea necesario.

object Constants {
    const val CONSTANT2 = "object constant"
}
val foo = Constants.CONSTANT2

Paso 2: Crea un objeto complementario

Kotlin no tiene un concepto de constantes a nivel de la clase.

Para definir constantes dentro de una clase, debes incluirlas en objetos complementarios declarados con la palabra clave companion. El objeto complementario es básicamente un objeto singleton dentro de la clase.

  1. Crea una clase con un objeto complementario que contenga una constante de cadena.
class MyClass {
    companion object {
        const val CONSTANT3 = "constant in companion"
    }
}

La diferencia básica entre los objetos complementarios y los objetos normales es la siguiente:

  • Los objetos complementarios se inicializan desde el constructor estático de la clase contenedora, es decir, se crean cuando se crea el objeto.
  • Los objetos normales se inicializan de forma diferida en el primer acceso a ese objeto, es decir, cuando se usan por primera vez.

Hay más, pero todo lo que necesitas saber por ahora es que debes incluir las constantes en clases dentro de un objeto complementario.

En esta tarea, aprenderás a extender el comportamiento de las clases. Es muy común escribir funciones de utilidad para extender el comportamiento de una clase. Kotlin proporciona una sintaxis conveniente para declarar estas funciones de utilidad: funciones de extensión.

Las funciones de extensión te permiten agregar funciones a una clase existente sin tener que acceder a su código fuente. Por ejemplo, puedes declararlos en un archivo Extensions.kt que forme parte de tu paquete. En realidad, esto no modifica la clase, pero te permite usar la notación de puntos cuando llamas a la función en objetos de esa clase.

Paso 1: Escribe una función de extensión

  1. Mientras sigues trabajando en el REPL, escribe una función de extensión simple, hasSpaces(), para verificar si una cadena contiene espacios. El nombre de la función tiene el prefijo de la clase en la que opera. Dentro de la función, this hace referencia al objeto en el que se llama, y it hace referencia al iterador en la llamada a find().
fun String.hasSpaces(): Boolean {
    val found = this.find { it == ' ' }
    return found != null
}
println("Does it have spaces?".hasSpaces())
⇒ true
  1. Puedes simplificar la función hasSpaces(). No se necesita this de forma explícita, y la función se puede reducir a una sola expresión y devolverse, por lo que tampoco se necesitan las llaves {} que la rodean.
fun String.hasSpaces() = find { it == ' ' } != null

Paso 2: Conoce las limitaciones de las extensiones

Las funciones de extensión solo tienen acceso a la API pública de la clase que extienden. No se puede acceder a las variables que son private.

  1. Intenta agregar funciones de extensión a una propiedad marcada como private.
class AquariumPlant(val color: String, private val size: Int)

fun AquariumPlant.isRed() = color == "red"    // OK
fun AquariumPlant.isBig() = size > 50         // gives error
⇒ error: cannot access 'size': it is private in 'AquariumPlant'
  1. Examina el siguiente código y determina qué imprimirá.
open class AquariumPlant(val color: String, private val size: Int)

class GreenLeafyPlant(size: Int) : AquariumPlant("green", size)

fun AquariumPlant.print() = println("AquariumPlant")
fun GreenLeafyPlant.print() = println("GreenLeafyPlant")

val plant = GreenLeafyPlant(size = 10)
plant.print()
println("\n")
val aquariumPlant: AquariumPlant = plant
aquariumPlant.print()  // what will it print?
⇒ GreenLeafyPlant
AquariumPlant

plant.print() copias GreenLeafyPlant. Es posible que esperes que aquariumPlant.print() también imprima GreenLeafyPlant, ya que se le asignó el valor de plant. Sin embargo, el tipo se resuelve en el tiempo de compilación, por lo que se imprime AquariumPlant.

Paso 3: Agrega una propiedad de extensión

Además de las funciones de extensión, Kotlin también te permite agregar propiedades de extensión. Al igual que con las funciones de extensión, debes especificar la clase que extiendes, seguida de un punto y el nombre de la propiedad.

  1. Mientras sigues trabajando en el REPL, agrega una propiedad de extensión isGreen a AquariumPlant, que es true si el color es verde.
val AquariumPlant.isGreen: Boolean
   get() = color == "green"

Se puede acceder a la propiedad isGreen como a cualquier otra propiedad. Cuando se accede a ella, se llama al método get de isGreen para obtener el valor.

  1. Imprime la propiedad isGreen para la variable aquariumPlant y observa el resultado.
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true

Paso 4: Obtén información sobre los receptores que admiten valores nulos

La clase que extiendes se llama receptor, y es posible hacer que esa clase sea anulable. Si lo haces, la variable this que se usa en el cuerpo puede ser null, así que asegúrate de probarla. Te convendría tomar un receptor anulable si esperas que los llamadores quieran llamar a tu método de extensión en variables anulables o si quieres proporcionar un comportamiento predeterminado cuando tu función se aplique a null.

  1. Aún en el REPL, define un método pull() que tome un receptor que admite valores nulos. Esto se indica con un signo de interrogación ? después del tipo y antes del punto. Dentro del cuerpo, puedes probar si this no es null usando questionmark-dot-apply ?.apply.
fun AquariumPlant?.pull() {
   this?.apply {
       println("removing $this")
   }
}

val plant: AquariumPlant? = null
plant.pull()
  1. En este caso, no se genera ningún resultado cuando ejecutas el programa. Como plant es null, no se llama al println() interno.

Las funciones de extensión son muy potentes, y la mayor parte de la biblioteca estándar de Kotlin se implementa como funciones de extensión.

En esta lección, aprendiste más sobre las colecciones y las constantes, y probaste el poder de las funciones y propiedades de extensión.

  • Los pares y las tuplas se pueden usar para devolver más de un valor desde una función. Por ejemplo:
    val twoLists = fish.partition { isFreshWater(it) }
  • Kotlin tiene muchas funciones útiles para List, como reversed(), contains() y subList().
  • Se puede usar un HashMap para asignar claves a valores. Por ejemplo:
    val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  • Declara constantes de tiempo de compilación con la palabra clave const. Puedes colocarlos en el nivel superior, organizarlos en un objeto singleton o colocarlos en un objeto complementario.
  • Un objeto complementario es un objeto singleton dentro de una definición de clase, definido con la palabra clave companion.
  • Las funciones y propiedades de extensión pueden agregar funcionalidad a una clase. Por ejemplo:
    fun String.hasSpaces() = find { it == ' ' } != null
  • Un receptor anulable te permite crear extensiones en una clase que puede ser null. El operador ?. se puede combinar con apply para verificar null antes de ejecutar el código. Por ejemplo:
    this?.apply { println("removing $this") }

Documentación de Kotlin

Si deseas obtener más información sobre algún tema de este curso o si te quedas atascado, https://kotlinlang.org es el mejor punto de partida.

Instructivos de Kotlin

El sitio web https://try.kotlinlang.org incluye instructivos enriquecidos llamados Kotlin Koans, un intérprete basado en la Web y un conjunto completo de documentación de referencia con ejemplos.

Curso de Udacity

Para ver el curso de Udacity sobre este tema, consulta Capacitación de Kotlin para programadores.

IntelliJ IDEA

Encontrarás la documentación de IntelliJ IDEA en el sitio web de JetBrains.

En esta sección, se enumeran las posibles actividades para el hogar para los alumnos que trabajan en este codelab como parte de un curso dirigido por un instructor. Depende del instructor hacer lo siguiente:

  • Si es necesario, asigna una tarea.
  • Comunicarles a los alumnos cómo enviar las actividades para el hogar.
  • Califica las actividades para el hogar.

Los instructores pueden usar estas sugerencias en la medida que quieran y deben asignar cualquier otra actividad para el hogar que consideren apropiada.

Si estás trabajando en este codelab por tu cuenta, usa estas actividades para el hogar para probar tus conocimientos.

Responde estas preguntas:

Pregunta 1

¿Cuál de las siguientes opciones devuelve una copia de una lista?

add()

remove()

reversed()

contains()

Pregunta 2

¿Cuál de estas funciones de extensión en class AquariumPlant(val color: String, val size: Int, private val cost: Double, val leafy: Boolean) generará un error de compilación?

fun AquariumPlant.isRed() = color == "red"

fun AquariumPlant.isBig() = size > 45

fun AquariumPlant.isExpensive() = cost > 10.00

fun AquariumPlant.isNotLeafy() = leafy == false

Pregunta 3

¿Cuál de las siguientes opciones no es un lugar donde puedes definir constantes con const val?

▢ en el nivel superior de un archivo

▢ En clases regulares

▢ en objetos singleton

▢ en objetos complementarios

Continúa con la siguiente lección: 5.2 Elementos genéricos

Para obtener una descripción general del curso, incluidos los vínculos a otros codelabs, consulta "Capacitación de Kotlin para programadores: Bienvenido al curso".