Capacitación de Kotlin para programadores 5.1: extensiones

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

Introducción

En este codelab, conocerás varias funciones útiles de Kotlin, como pares, colecciones y funciones de extensión.

En lugar de compilar una sola app de muestra, las lecciones de este curso están diseñadas para aumentar tu conocimiento, pero son semiindependientes entre sí a fin de que puedas leer las secciones con las que estás familiarizado. Para vincularlos, muchos de los ejemplos usan un tema de acuario. Si deseas ver la historia completa del acuario, consulta el curso de Udacity Capacitación de Kotlin para programadores.

Conocimientos que ya deberías tener

  • La sintaxis de las funciones, las clases y los métodos de Kotlin
  • Cómo trabajar con REPL (Read-Eval-Print Loop) de Kotlin en IntelliJ IDEA
  • Cómo crear una clase nueva en IntelliJ IDEA y cómo ejecutar un programa

Qué aprenderás

  • Cómo trabajar con pares y triples
  • Más información sobre las colecciones
  • Cómo definir y usar constantes
  • Escribir funciones de extensión

Actividades

  • Obtén información sobre pares, triples y mapas hash en el REPL
  • Aprende diferentes formas de organizar las constantes
  • Escribe una función y una propiedad de la extensión

En esta tarea, aprenderá sobre pares y triples, y los desestructurará. Los pares y triples son clases de datos prediseñadas para 2 o 3 elementos genéricos. Esto puede ser útil, por ejemplo, para que una función muestre más de un valor.

Supongamos que tienes un List de pescado y una función isFreshWater() para verificar si era un pescado de agua dulce o salada. List.partition() muestra dos listas, una con los elementos en los que la condición es true y la otra para 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: Haz pares y triples

  1. Abre REPL (Tools > Kotlin > Kotlin REPL).
  2. Crea un par, asocia un equipo con para qué se usa y, luego, imprime los valores. Para crear un par, crea una expresión que conecte dos valores, como dos strings, con la palabra clave to y, luego, usa .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 un triple y, luego, imprímelo con toString() y conviértelo en una lista con toList(). Puedes crear un triple con Triple() con 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 de triples, aunque no es obligatorio. Las partes pueden ser una string, un número o una lista, por ejemplo, un par o un triple.

  1. Crea un par donde la primera parte del par sea en sí misma.
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 triples

La separación de pares y triples en sus partes se denomina desestructuración. Asigna el par o triple a la cantidad apropiada 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 un objeto triple 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 triples funciona de la misma manera que con las clases de datos, que se trataron en un codelab anterior.

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

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

  1. Las listas y las listas mutables se introdujeron en una lección anterior. Son una estructura de datos muy útil, por lo que Kotlin ofrece varias funciones integradas para las listas. Revisa esta lista parcial de funciones para las listas. Puedes encontrar fichas completas en la documentación de Kotlin para List y MutableList.

Función

Propósito

add(element: E)

Agregar un elemento a la lista mutable

remove(element: E)

Quitar un elemento de una lista mutable

reversed()

Mostrar una copia de la lista con los elementos en orden inverso

contains(element: E)

Muestra true si la lista contiene el elemento.

subList(fromIndex: Int, toIndex: Int)

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

  1. Aún trabajando en el REPL, crea una lista de números y llama a sum() en él. Esto suma todos los elementos.
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
  1. Crea una lista de strings y suma 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 sepa cómo sumar directamente, como una string, puedes especificar cómo sumarlo con .sumBy() con una función lambda, por ejemplo, para sumar por la longitud de cada string. El nombre predeterminado de 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. Con las listas, puedes hacer mucho más. Una forma de ver la funcionalidad disponible es crear una lista en IntelliJ IDEA, agregar el punto y, luego, ver la lista de autocompletado en la información sobre la herramienta. Esto funciona para cualquier objeto. Pruébalo con una lista.

  1. Elige listIterator() de la lista, pasa por la lista con una sentencia for e 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 mapear prácticamente cualquier cosa con hashMapOf(). Los mapas hash son similares a una lista de pares, en la que el primer valor actúa como una clave.

  1. Crea un mapa hash que coincida con los síntomas, las teclas 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 de síntoma, mediante get(), o incluso corchetes más cortos [].
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 se encuentra en el mapa, al intentar mostrar la enfermedad coincidente, se muestra null. Según los datos del mapa, puede ser común no encontrar coincidencias para una posible clave. En casos como ese, Kotlin proporciona la función getOrDefault().

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

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

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

En lugar de mostrar un valor predeterminado simple, el código que se encuentre entre las llaves {} se ejecutará. En el ejemplo, else simplemente muestra una string, pero podría ser tan sofisticada como encontrar una página web con una cura y mostrarla.

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

  1. Cree un mapa de inventario que se pueda modificar y asigne una string de equipo a la cantidad de elementos. Créala con una red para peces, agrega 3 fosas de tanque al inventario con put() y quítala 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 constantes en Kotlin y las distintas formas de organizarlas.

Paso 1: Más información sobre la función const. y el valor

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

El valor está asignado y no se puede cambiar, lo que suena muy similar a declarar una val normal. ¿Cuál es la diferencia entre const val y val? El valor de const val se determina durante el tiempo de compilación, mientras que el valor de val se determina durante la ejecución del programa; es decir, val puede asignarse una función en el tiempo de ejecución.

Eso significa que se puede asignar un valor a una función val, 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 clases singleton declaradas con object, no con clases regulares. Puedes usar esta opción para crear un archivo o un objeto singleton que contenga solo 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 clase.

Para definir las constantes dentro de una clase, debes unirlas en objetos complementarios declarados con la palabra clave companion. Básicamente, el objeto complementario es un objeto singleton dentro de la clase.

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

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

  • Los objetos complementarios se inicializan desde el constructor estático de la clase que los contiene, 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 información, pero, por ahora, lo que necesitas es unir las constantes en clases en 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 forma 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. Aún en proceso de REPL, escribe una función de extensión simple hasSpaces() para verificar si una string contiene espacios. El nombre de la función tiene el prefijo de la clase en la que opera. Dentro de la función, this se refiere al objeto en el que se llama y it al iterador en la llamada 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(). La this no es necesaria explícitamente, y la función se puede reducir a una sola expresión y mostrarla, por lo que tampoco se necesitan las llaves {} que la rodean.
fun String.hasSpaces() = find { it == ' ' } != null

Paso 2: Obtén información sobre 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 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 código que aparece a continuación y descubre qué se 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() imprime GreenLeafyPlant. Es de esperar 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 extenderás, seguida de un punto y el nombre de la propiedad.

  1. Aún en proceso en 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 de la misma manera que una normal; cuando se accede a ella, se llama al método get para isGreen a fin de 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 nulos

La clase que extiendes se denomina 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 realizar una prueba. Te recomendamos que tomes un receptor anulable si esperas que los emisores quieran llamar a tu método de extensión en variables anulables, o si deseas proporcionar un comportamiento predeterminado cuando tu función se aplique a null.

  1. Aún en proceso en el REPL, define un método pull() que tome un receptor anulable. Esto se indica con un signo de interrogación ? después del tipo, antes del punto. Dentro del cuerpo, puedes probar si this no es null si utilizas ?.apply. para indicar punto de aplicación.
fun AquariumPlant?.pull() {
   this?.apply {
       println("removing $this")
   }
}

val plant: AquariumPlant? = null
plant.pull()
  1. En este caso, no hay resultados 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, sobre las constantes y conoces el poder de las funciones y propiedades de la extensión.

  • Se pueden usar pares y triples para mostrar más de un valor de 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 mediante la palabra clave const. Puedes ubicarlas en el nivel superior, organizarlas en un objeto singleton o en un objeto complementario.
  • Un objeto complementario es un objeto singleton dentro de una definición de clase, definida 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 vincular con apply para buscar null antes de ejecutar código. Por ejemplo:
    this?.apply { println("removing $this") }

Documentación de Kotlin

Si necesitas más información sobre algún tema de este curso o si no puedes avanzar, https://kotlinlang.org es tu 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 el Capacitación de Kotlin para programadores.

IntelliJ IDEA

Puedes encontrar la documentación de IntelliJ IDEA en el sitio web de JetBrains.

En esta sección, se enumeran las posibles tareas para los alumnos que trabajan con este codelab como parte de un curso que dicta un instructor. Depende del instructor hacer lo siguiente:

  • Si es necesario, asigna la tarea.
  • Informa a los alumnos cómo enviar los deberes.
  • Califica las tareas.

Los instructores pueden usar estas sugerencias lo poco o lo que quieran, y deben asignar cualquier otra tarea que consideren apropiada.

Si estás trabajando en este codelab por tu cuenta, usa estas tareas para poner a prueba tus conocimientos.

Responde estas preguntas

Pregunta 1

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

add()

remove()

reversed()

contains()

Pregunta 2

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

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 en el que puedes definir constantes con const val?

▢ en un nivel superior de un archivo

▢ en clases normales

▢ en objetos singleton

▢ en objetos complementarios

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

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