Treinamento do Kotlin para programadores 5.1: extensões

Este codelab faz parte do curso Treinamento do Kotlin para programadores. Você aproveitará mais o curso se fizer os codelabs em sequência. Dependendo do seu conhecimento, talvez você possa passar mais rápido por algumas seções. Este curso é destinado a programadores que conhecem uma linguagem orientada a objetos e querem aprender Kotlin.

Introdução

Neste codelab, você conhecerá vários recursos úteis do Kotlin, incluindo pares, coleções e funções de extensão.

Em vez de criar um único app de exemplo, as lições deste curso foram desenvolvidas para aumentar seu conhecimento, mas são semi-independentes umas das outras para que você possa ler as seções que já conhece. Para conectá-los, muitos dos exemplos usam um tema de aquário. Se você quiser ver a história completa do aquário, confira o curso Bootcamp de Kotlin para programadores na Udacity.

O que você já precisa saber

  • Sintaxe de funções, classes e métodos do Kotlin
  • Como trabalhar com REPL (Read-Eval-Print Loop) do Kotlin no IntelliJ IDEA
  • Como criar uma nova classe no IntelliJ IDEA e executar um programa

O que você vai aprender

  • Como trabalhar com pares e triplos
  • Mais sobre coleções
  • Como definir e usar constantes
  • Como escrever funções de extensão

Atividades do laboratório

  • Saiba mais sobre pares, triplos e mapas de hash no REPL
  • Aprenda maneiras diferentes de organizar constantes
  • Escrever uma função e uma propriedade de extensão

Nesta tarefa, você aprenderá sobre pares e triplos e desestruturará-los. Pares e triplos são classes de dados predefinidas para dois ou três itens genéricos. Isso pode ser útil, por exemplo, para fazer uma função retornar mais de um valor.

Digamos que você tenha um List de peixes e uma função isFreshWater() para verificar se os peixes são de água doce ou salgada. List.partition() retorna duas listas, uma com os itens em que a condição é true e a outra para itens em que a condição é false.

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

Etapa 1: criar alguns pares e triplos

  1. Abra o REPL (Tools > Kotlin > Kotlin REPL).
  2. Crie um par, associando um equipamento ao qual ele é usado e exiba os valores. Você pode criar um par criando uma expressão conectando dois valores, como duas strings, com a palavra-chave to e usando .first ou .second para fazer referência a cada valor.
val equipment = "fish net" to "catching fish"
println("${equipment.first} used for ${equipment.second}")
⇒ fish net used for catching fish
  1. Crie um triplo e imprima-o com toString(). Em seguida, converta-o em uma lista com toList(). Você cria um triplo usando Triple() com três valores. Use .first, .second e .third para se referir a cada valor.
val numbers = Triple(6, 9, 42)
println(numbers.toString())
println(numbers.toList())
⇒ (6, 9, 42)
[6, 9, 42]

Os exemplos acima usam o mesmo tipo para todas as peças do par ou triplo, mas isso não é obrigatório. As partes podem ser uma string, um número ou uma lista, por exemplo, até mesmo outro par ou triplo.

  1. Crie um par em que a primeira parte do par seja ela mesma.
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

Etapa 2: desestruturar alguns pares e triplos

A separação dos pares e dos triplos em suas partes é chamada de desestruturação. Atribua o par ou triplo ao número adequado de variáveis, e o Kotlin atribuirá o valor de cada parte na ordem.

  1. Desestruturar um par e imprimir os 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. Desestruturar um triplo e exibir os valores.
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42

Observe que a desestruturação de pares e triplos funciona da mesma forma que em classes de dados, que eram abordadas em um codelab anterior.

Nesta tarefa, você aprenderá mais sobre coleções, incluindo listas, um novo tipo de coleção, mapas de hash.

Etapa 1: saber mais sobre listas

  1. Listas e listas mutáveis foram introduzidas em uma lição anterior. Eles são uma estrutura de dados muito útil, então o Kotlin fornece várias funções integradas para listas. Consulte esta lista parcial de funções para ver as listas. Você pode encontrar listagens completas na documentação do Kotlin para List e MutableList.

Function

Objetivo

add(element: E)

Adicione um item à lista mutável.

remove(element: E)

Remover um item de uma lista mutável.

reversed()

Retorne uma cópia da lista com os elementos na ordem inversa.

contains(element: E)

Retorne true se a lista contiver o item.

subList(fromIndex: Int, toIndex: Int)

Retorne parte da lista, do primeiro índice até, mas não incluindo o segundo índice.

  1. Ainda trabalhando no REPL, crie uma lista de números e chame sum() nela. Isso resume todos os elementos.
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
  1. Crie uma lista de strings e some-a.
val list2 = listOf("a", "bbb", "cc")
println(list2.sum())
⇒ error: none of the following functions can be called with the arguments supplied:
  1. Se o elemento não for algo que List sabe somar diretamente, como uma string, você poderá especificar como fazer isso usando .sumBy() com uma função lambda, por exemplo, para somar pelo tamanho de cada string. O nome padrão de um argumento lambda é it. Aqui, it se refere a cada elemento da lista à medida que a lista é transferida.
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
  1. Você pode fazer muito mais com as listas. Uma forma de ver a funcionalidade disponível é criar uma lista no IntelliJ IDEA, adicionar o ponto e, em seguida, analisar a lista de preenchimento automático na dica. Isso funciona para qualquer objeto. Teste com uma lista.

  1. Escolha listIterator() na lista, percorra a lista com uma instrução for e exiba todos os elementos separados por espaços.
val list2 = listOf("a", "bbb", "cc")
for (s in list2.listIterator()) {
    println("$s ")
}
⇒ a bbb cc

Etapa 2: testar mapas de hash

Em Kotlin, é possível fazer praticamente o mapeamento de qualquer coisa usando hashMapOf(). Os mapas hash são como uma lista de pares, em que o primeiro valor atua como uma chave.

  1. Crie um mapa de hash que corresponda aos valores, chaves e doenças dos peixes.
val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  1. Em seguida, é possível recuperar o valor da doença com base na chave de sintomas, usando get() ou até mesmo colchetes menores, [].
println(cures.get("white spots"))
⇒ Ich
println(cures["red sores"])
⇒ hole disease
  1. Tente especificar um sintoma que não esteja no mapa.
println(cures["scale loss"])
⇒ null

Se uma chave não estiver no mapa, a tentativa de retornar a doença correspondente retornará null. Dependendo dos dados do mapa, pode ser comum não haver correspondência para uma possível chave. Para casos como esse, o Kotlin oferece a função getOrDefault().

  1. Procure uma chave sem correspondência usando getOrDefault().
println(cures.getOrDefault("bloating", "sorry, I don't know"))
⇒ sorry, I don't know

Se você precisar fazer mais do que apenas retornar um valor, o Kotlin fornecerá a função getOrElse().

  1. Mude o código para usar getOrElse() em vez de getOrDefault().
println(cures.getOrElse("bloating") {"No cure for this"})
⇒ No cure for this

Em vez de retornar um valor padrão simples, qualquer código entre as chaves {} é executado. No exemplo, else simplesmente retorna uma string, mas pode ser tão sofisticado quanto encontrar uma página da Web com uma cura e retorná-la.

Assim como mutableListOf, você também pode criar um mutableMapOf. Um mapa mutável permite colocar e remover itens. "Mutável" significa apenas conseguir "mudar", e "imutável" significa não conseguir mudar.

  1. Crie um mapa de inventário que possa ser modificado, mapeando uma string do equipamento para o número de itens. Crie-a com uma rede de peixe, adicione três tanques de peixes ao inventário com put() e remova a rede de peixes com 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}

Nesta tarefa, você aprenderá sobre constantes no Kotlin e diferentes maneiras de organizá-las.

Etapa 1: saiba mais sobre a comparação entre valores constantes e constantes

  1. No REPL, tente criar uma constante numérica. Em Kotlin, você pode criar constantes de nível superior e atribuir um valor a elas no tempo de compilação usando const val.
const val rocks = 3

O valor é atribuído e não pode ser alterado. Isso soa muito parecido com a declaração de um val normal. Qual é a diferença entre const val e val? O valor de const val é determinado durante a compilação, em que o valor de val é determinado durante a execução do programa. Isso significa que val pode ser atribuído por uma função no momento da execução.

Isso significa que o valor de uma função pode ser atribuído a val, mas const val não.

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

Além disso, const val só funciona no nível superior e em classes singleton declaradas com object, não com classes regulares. Você pode usar isso para criar um arquivo ou objeto Singleton que contenha apenas constantes e importá-las conforme necessário.

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

Etapa 2: criar um objeto complementar

O Kotlin não tem um conceito de constantes de nível de classe.

Para definir constantes dentro de uma classe, é necessário unir esses objetos em objetos complementares declarados com a palavra-chave companion. O objeto complementar é basicamente um objeto Singleton dentro da classe.

  1. Crie uma classe com um objeto complementar que contenha uma constante de string.
class MyClass {
    companion object {
        const val CONSTANT3 = "constant in companion"
    }
}

A diferença básica entre objetos complementares e objetos comuns é:

  • Objetos complementares são inicializados a partir do construtor estático da classe contida, ou seja, eles são criados quando o objeto é criado.
  • Objetos regulares são inicializados lentamente no primeiro acesso a esse objeto, ou seja, quando são usados pela primeira vez.

Há mais, mas tudo o que você precisa saber por enquanto é unir constantes em classes de um objeto complementar.

Nesta tarefa, você aprenderá a ampliar o comportamento das classes. É muito comum escrever funções utilitárias para ampliar o comportamento de uma classe. O Kotlin oferece uma sintaxe conveniente para declarar essas funções utilitárias: funções de extensão.

As funções de extensão permitem adicionar funções a uma classe existente sem precisar acessar o código-fonte. Por exemplo, você pode declará-las em um arquivo Extensions.kt que faz parte do seu pacote. Isso não modifica a classe, mas permite que você use a notação de ponto ao chamar a função em objetos dessa classe.

Etapa 1: criar uma função de extensão

  1. Ainda trabalhando no REPL, escreva uma função de extensão simples, hasSpaces(), para verificar se uma string contém espaços. O nome da função é prefixado com a classe em que ela opera. Dentro da função, this refere-se ao objeto em que é chamado, e it refere-se ao iteração na chamada find().
fun String.hasSpaces(): Boolean {
    val found = this.find { it == ' ' }
    return found != null
}
println("Does it have spaces?".hasSpaces())
⇒ true
  1. É possível simplificar a função hasSpaces(). O this não é explicitamente necessário, e a função pode ser reduzida a uma única expressão e retornada, então as chaves {} ao redor dela também não são necessárias
fun String.hasSpaces() = find { it == ' ' } != null

Etapa 2: conhecer as limitações das extensões

As funções de extensão só têm acesso à API pública da classe que estão sendo estendidas. As variáveis private não podem ser acessadas.

  1. Tente adicionar funções de extensão a uma propriedade 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. Analise o código abaixo e descubra o que ele exibirá.
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() exibe GreenLeafyPlant. Você também pode esperar que aquariumPlant.print() imprima GreenLeafyPlant, porque o valor de plant foi atribuído a ele. Como o tipo é resolvido no momento da compilação, AquariumPlant é impresso.

Etapa 3: adicionar uma propriedade de extensão

Além das funções de extensão, o Kotlin também permite adicionar propriedades de extensão. Assim como nas funções de extensão, é possível especificar a classe que será estendida, seguida por um ponto, seguido pelo nome da propriedade.

  1. Ainda trabalhando no REPL, adicione uma propriedade de extensão isGreen a AquariumPlant, que será true se a cor estiver verde.
val AquariumPlant.isGreen: Boolean
   get() = color == "green"

A propriedade isGreen pode ser acessada como uma propriedade normal. Quando acessado, o getter de isGreen é chamado para receber o valor.

  1. Exiba a propriedade isGreen para a variável aquariumPlant e observe o resultado.
aquariumPlant.isGreen
⇒ res4: kotlin.Boolean = true

Etapa 4: conheça os receptores anuláveis

A classe que você estende é chamada de receiver, e é possível torná-la anulável. Se você fizer isso, a variável this usada no corpo poderá ser null. Portanto, teste essa variável. Use um receptor anulável se quiser que os autores das chamadas chamem seu método de extensão em variáveis anuláveis ou forneça um comportamento padrão quando a função for aplicada a null.

  1. Ainda trabalhando no REPL, defina um método pull() que usa um receptor anulável. Isso é indicado com um ponto de interrogação ? após o tipo, antes do ponto. No corpo, é possível testar se this não é null usando questionmark-dot-apply ?.apply..
fun AquariumPlant?.pull() {
   this?.apply {
       println("removing $this")
   }
}

val plant: AquariumPlant? = null
plant.pull()
  1. Nesse caso, não haverá saída ao executar o programa. Como plant é null, o println() interno não é chamado.

As funções de extensão são muito eficientes, e a maior parte da biblioteca padrão do Kotlin é implementada como funções de extensão.

Nesta lição, você aprendeu mais sobre coleções e de constantes e aprendeu sobre o poder das funções e propriedades de extensão.

  • Pares e triplos podem ser usados para retornar mais de um valor de uma função. Exemplo:
    val twoLists = fish.partition { isFreshWater(it) }
  • O Kotlin tem muitas funções úteis para List, como reversed(), contains() e subList().
  • Um HashMap pode ser usado para mapear chaves para valores. Exemplo:
    val cures = hashMapOf("white spots" to "Ich", "red sores" to "hole disease")
  • Declare constantes de tempo de compilação usando a palavra-chave const. É possível colocá-los no nível superior, organizá-los em um objeto Singleton ou colocá-los em um objeto complementar.
  • Um objeto complementar é um objeto Singleton em uma definição de classe, definida com a palavra-chave companion.
  • As funções e propriedades de extensão podem adicionar funcionalidades a uma classe. Exemplo:
    fun String.hasSpaces() = find { it == ' ' } != null
  • Um receptor anulável permite que você crie extensões em uma classe que pode ser null. O operador ?. pode ser pareado com apply para verificar null antes de executar o código. Exemplo:
    this?.apply { println("removing $this") }

Documentação do Kotlin

Se quiser mais informações sobre qualquer assunto deste curso ou se tiver dificuldades, https://kotlinlang.org é seu melhor ponto de partida.

Tutoriais do Kotlin

O site https://try.kotlinlang.org (link em inglês) tem tutoriais elaborados chamados Kotlin Koans, um intérprete baseado na Web, e um conjunto completo de documentação de referência com exemplos.

Curso Udacity

Para ver o curso da Udacity sobre esse assunto, consulte Bootcamp de Kotlin para programadores (link em inglês).

IntelliJ IDEA

A documentação do IntelliJ IDEA (em inglês) pode ser encontrada no site da JetBrains.

Esta seção lista as possíveis atividades para os alunos que estão trabalhando neste codelab como parte de um curso ministrado por um instrutor. Cabe ao instrutor fazer o seguinte:

  • Se necessário, atribua o dever de casa.
  • Informe aos alunos como enviar o dever de casa.
  • Atribua nota aos trabalhos de casa.

Os professores podem usar essas sugestões o quanto quiserem, e eles devem se sentir à vontade para passar o dever de casa como achar adequado.

Se você estiver fazendo este codelab por conta própria, use essas atividades para testar seu conhecimento.

Responda a estas perguntas

Pergunta 1

Qual das seguintes opções retorna uma cópia de uma lista?

add()

remove()

reversed()

contains()

Pergunta 2

Qual destas funções de extensão em class AquariumPlant(val color: String, val size: Int, private val cost: Double, val leafy: Boolean) causa um erro no compilador?

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

fun AquariumPlant.isBig() = size > 45

fun AquariumPlant.isExpensive() = cost > 10.00

fun AquariumPlant.isNotLeafy() = leafy == false

Pergunta 3

Qual das seguintes opções não é um lugar em que você pode definir constantes com const val?

▢ no nível superior de um arquivo

▢ em turmas regulares

▢ em objetos Singleton

▢ em objetos complementares

Vá para a próxima lição: 5.2 Genéricos

Para ter uma visão geral do curso, incluindo links para outros codelabs, consulte "Bootcamp de Kotlin para programadores: bem-vindo ao curso."