Treinamento do Kotlin para programadores 5.1: extensões

Este codelab faz parte do curso de treinamento do Kotlin para programadores. Você vai aproveitar mais este 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ê vai 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 criadas para aumentar seu conhecimento, mas são semi-independentes umas das outras para que você possa ler as seções com que já está familiarizado. Para unificar os exemplos, muitos deles usam um tema de aquário. Se quiser conferir a história completa do aquário, acesse o curso da Udacity Treinamento do Kotlin para programadores (link em inglês).

O que você já precisa saber

  • A sintaxe de funções, classes e métodos do Kotlin
  • Como trabalhar com o 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 trios
  • Saiba mais sobre as coleções
  • Como definir e usar constantes
  • Como escrever funções de extensão

Atividades deste laboratório

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

Nesta tarefa, você vai aprender sobre pares e triplas e como desestruturá-los. Pares e trios são classes de dados pré-criadas para 2 ou 3 itens genéricos. Isso pode ser útil, por exemplo, para uma função retornar mais de um valor.

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

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

Etapa 1: fazer alguns pares e trios

  1. Abra o REPL (Tools > Kotlin > Kotlin REPL).
  2. Crie um par, associando um equipamento à finalidade dele, e imprima os valores. Para criar um par, crie uma expressão que conecte dois valores, como duas strings, com a palavra-chave to e use .first ou .second para se referir 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 uma tupla e imprima-a com toString(). Depois, converta em uma lista com toList(). Você cria uma tripla 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 partes do par ou da tripla, mas isso não é obrigatório. As partes podem ser uma string, um número ou uma lista, por exemplo, ou até mesmo outro par ou tripla.

  1. Crie um par em que a primeira parte também seja um 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

Etapa 2: desestruturar alguns pares e trios

Separar pares e trios em partes é chamado de desestruturação. Atribua o par ou a tripla ao número adequado de variáveis, e o Kotlin vai atribuir o valor de cada parte em ordem.

  1. Desestruture um par e imprima 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. Desestruture uma tupla tripla e imprima os valores.
val numbers = Triple(6, 9, 42)
val (n1, n2, n3) = numbers
println("$n1 $n2 $n3")
⇒ 6 9 42

A desestruturação de pares e trios funciona da mesma forma que com classes de dados, o que foi abordado em um codelab anterior.

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

Etapa 1: saiba mais sobre as listas

  1. As listas e listas mutáveis foram apresentadas em uma lição anterior. Elas são uma estrutura de dados muito útil, então o Kotlin oferece várias funções integradas para listas. Confira esta lista parcial de funções para listas. Você pode encontrar listagens completas na documentação do Kotlin para List e MutableList.

Function

Purpose

add(element: E)

Adicione um item à lista mutável.

remove(element: E)

Remova um item de uma lista mutável.

reversed()

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

contains(element: E)

Retorna true se a lista contiver o item.

subList(fromIndex: Int, toIndex: Int)

Retorna parte da lista, do primeiro índice até o segundo, mas sem incluí-lo.

  1. Ainda trabalhando no REPL, crie uma lista de números e chame sum() nela. Isso soma todos os elementos.
val list = listOf(1, 5, 3, 4)
println(list.sum())
⇒ 13
  1. Crie uma lista de strings e some os valores dela.
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 o List saiba somar diretamente, como uma string, especifique como somar usando .sumBy() com uma função lambda, por exemplo, para somar pelo comprimento de cada string. O nome padrão de um argumento lambda é it, e aqui it se refere a cada elemento da lista à medida que ela é percorrida.
val list2 = listOf("a", "bbb", "cc")
println(list2.sumBy { it.length })
⇒ 6
  1. Há muito mais que você pode fazer com as listas. Uma maneira de conferir a funcionalidade disponível é criar uma lista no IntelliJ IDEA, adicionar o ponto e consultar 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 imprima 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, você pode mapear quase tudo para qualquer outra coisa usando hashMapOf(). Os mapas de hash são como uma lista de pares, em que o primeiro valor funciona como uma chave.

  1. Crie um mapa de hash que corresponda aos sintomas, às chaves e às doenças dos peixes, os valores.
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 do sintoma usando get() ou, de forma ainda mais curta, colchetes [].
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, tentar retornar a doença correspondente vai retornar null. Dependendo dos dados do mapa, é comum não haver correspondência para uma possível chave. Para casos como esse, o Kotlin oferece a função getOrDefault().

  1. Tente pesquisar uma chave que não tenha 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 oferece 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 e retornar uma página da Web com uma cura.

Assim como mutableListOf, você também pode criar um mutableMapOf. Um mapa mutável permite inserir e remover itens. Mutável significa capaz de mudar, e imutável significa incapaz de mudar.

  1. Crie um mapa de inventário que possa ser modificado, mapeando uma string de equipamento para o número de itens. Crie um com uma rede de pesca, adicione três esfregões de tanque ao inventário com put() e remova a rede de pesca 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ê vai aprender sobre constantes em Kotlin e diferentes maneiras de organizá-las.

Etapa 1: saiba mais sobre const e val

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

O valor é atribuído e não pode ser mudado, o que parece muito com a declaração de um val regular. Então, qual é a diferença entre const val e val? O valor de const val é determinado no momento da compilação, enquanto o valor de val é determinado durante a execução do programa. Isso significa que val pode ser atribuído por uma função no ambiente de execução.

Isso significa que val pode receber um valor de uma função, 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 no nível da classe.

Para definir constantes dentro de uma classe, é preciso envolvê-las 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 regulares é:

  • Os objetos complementares são inicializados pelo construtor estático da classe contida, ou seja, são criados quando o objeto é criado.
  • Objetos regulares são inicializados de forma lenta no primeiro acesso a eles, ou seja, quando são usados pela primeira vez.

Há mais informações, mas tudo o que você precisa saber por enquanto é encapsular constantes em classes em um objeto complementar.

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

As funções de extensão permitem adicionar funções a uma classe 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: gravar 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 se refere ao objeto em que ela é chamada, e it se refere ao iterador 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. Portanto, as chaves {} ao redor dela também não são necessárias.
fun String.hasSpaces() = find { it == ' ' } != null

Etapa 2: saiba mais sobre as limitações das extensões

As funções de extensão só têm acesso à API pública da classe que estão estendendo. 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 vai 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() impressões GreenLeafyPlant. Você pode esperar que aquariumPlant.print() também imprima GreenLeafyPlant, porque recebeu o valor de plant. Mas o tipo é resolvido no momento da compilação, entã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, você especifica a classe que está estendendo, seguida por um ponto e pelo nome da propriedade.

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

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

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

Etapa 4: saiba mais sobre receptores anuláveis

A classe que você estende é chamada de receptor, e é possível tornar essa classe anulável. Se você fizer isso, a variável this usada no corpo poderá ser null. Portanto, teste isso. Você vai querer usar um receptor anulável se espera que os chamadores queiram chamar seu método de extensão em variáveis anuláveis ou se quiser fornecer um comportamento padrão quando sua função for aplicada a null.

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

val plant: AquariumPlant? = null
plant.pull()
  1. Nesse caso, não há saída quando você executa o programa. Como plant é null, o println() interno não é chamado.

As funções de extensão são muito úteis, 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, constantes e conheceu o poder das funções e propriedades de extensão.

  • Pares e trios 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 a valores. Por 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. Você pode colocá-los no nível superior, organizar em um objeto singleton ou colocar em um objeto complementar.
  • Um objeto complementar é um objeto singleton dentro de uma definição de classe, definido com a palavra-chave companion.
  • 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 criar extensões em uma classe que pode ser null. O operador ?. pode ser combinado com apply para verificar null antes de executar o código. Por exemplo:
    this?.apply { println("removing $this") }

Documentação do Kotlin

Se você quiser mais informações sobre algum tópico deste curso ou se tiver dúvidas, https://kotlinlang.org é o melhor ponto de partida.

Tutoriais do Kotlin

O site https://try.kotlinlang.org inclui tutoriais avançados chamados Kotlin Koans, um interpretador baseado na Web e um conjunto completo de documentação de referência com exemplos.

Curso Udacity

Para conferir o curso da Udacity sobre esse tema, consulte Treinamento do Kotlin para programadores (link em inglês).

IntelliJ IDEA

A documentação do IntelliJ IDEA está disponível no site da JetBrains.

Esta seção lista as possíveis atividades de dever de casa para os alunos que estão fazendo este codelab como parte de um curso ministrado por um professor. Cabe ao professor fazer o seguinte:

  • Atribuir o dever de casa, se necessário.
  • Informar aos alunos como enviar deveres de casa.
  • Atribuir nota aos deveres de casa.

Os professores podem usar essas sugestões o quanto quiserem, podendo passar os exercícios que acharem mais apropriados como dever de casa.

Se você estiver seguindo este codelab por conta própria, sinta-se à vontade para usar esses deveres de casa para testar seu conhecimento.

Responda estas perguntas

Pergunta 1

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

add()

remove()

reversed()

contains()

Pergunta 2

Qual dessas funções de extensão em class AquariumPlant(val color: String, val size: Int, private val cost: Double, val leafy: Boolean) vai gerar um erro de 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 opções a seguir 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

Acesse a próxima lição: 5.2 Genéricos

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