Pular para o conteúdo

Trilha de Kotlin · parte 3 de 59

Controle de fluxo: if, when e laços

Como o Kotlin escolhe e repete: if e when como expressões, ranges com .. e until, for, while, break, continue e labels, com saídas reais.

iniciantePublicada em · 30 min de leitura

Tamanho da fonte:
Largura da coluna:
Tom:
Espaço entre linhas:
Fonte:

Controle de fluxo: if, when e laços

Imagine um GPS que só sabe seguir em frente. Você digita o destino, ele calcula a rota e passa por todas as ruas da lista, na ordem em que foram escritas, uma vez só. Se a ponte estiver interditada, ele não percebe. Se você precisar dar três voltas no quarteirão até achar vaga, ele não sabe contar voltas. Um programa que só executa linhas de cima para baixo funciona desse jeito, e quase todo software útil precisa de duas coisas que ele não tem: escolher entre caminhos e repetir um trecho até uma mudar.

Essa capacidade se chama controle de fluxo, que é o conjunto de construções que decide qual vem a seguir. Em Kotlin, as peças principais são o if e o when para escolher, o for e o while para repetir, e o break e o continue para interromper um laço no meio do caminho. Você também vai encontrar a palavra , que é um trecho de código que produz um valor, como nota >= 7 ou 2 + 2. A seção 5 trata dessa ideia com calma. Por enquanto, basta saber que toda expressão tem um resultado, e que esse resultado pode ser guardado numa variável.

o mapa da aula

A seção 1 explica por que o controle de fluxo existe. A seção 2 cobre if e else, o fato de o if produzir um valor e os operadores de comparação e lógicos, incluindo a diferença entre == e ===. A seção 3 apresenta o when, e a seção 4 trata dos laços: ranges, coleções, while, do-while, break, continue e labels. As seções 5 a 9 cuidam da diferença entre expressão e instrução, de um exercício guiado, dos erros que quase todo iniciante comete, de exercícios com resposta e de um glossário.

Todo código desta aula foi compilado e executado com o kotlinc 2.1.21, o compilador que vem com o plugin Kotlin do IntelliJ IDEA instalado nesta máquina, rodando sobre o OpenJDK 21. As saídas que aparecem nas notas são as que os programas imprimiram nessas execuções, copiadas diretamente delas. A aula Null safety e tipos nuláveis usa o Kotlin 2.4.21 pelo Gradle. Aqui não montei um projeto Gradle, então a versão desta aula é a 2.1.21. Não testei estes trechos na 2.4.21.

1. por que o existe

Um programa sem controle de fluxo é como um roteiro sem plateia: as falas saem sempre na mesma ordem, aconteça o que acontecer. Pense numa função que calcula o frete de um pedido. Suponha que a regra diga que compras acima de 200 reais não pagam frete. Sem uma decisão no meio do caminho, o código não tem como escolher entre as duas respostas. A única saída seria escrever duas versões do programa, uma para cada caso, e torcer para nunca precisar de uma terceira.

A repetição tem o mesmo problema em outra escala. Se a fila tem 300 pedidos para processar, dá para escrever 300 blocos iguais, mas isso não escala e deixa de funcionar no dia em que a fila tiver 301 pedidos. O existe para dizer uma vez só o que se repete e deixar uma condição decidir quando parar. Com isso, o código fica menor, e cada linha a menos é uma linha a menos para revisar.

Por isso o controle de fluxo se divide em dois movimentos, escolher e repetir, mais um recurso para interromper um laço no meio. Quem já programou em Java, linguagem orientada a objetos que roda na mesma máquina virtual do Kotlin, vai reconhecer quase tudo. Há duas diferenças que mudam a forma de escrever: o if produz um valor, e o when substitui o switch de Java. A seção 2 trata da primeira e a seção 3 da segunda. Se você nunca viu Java, nada disso é pré-requisito para acompanhar.

2. if e else

O if é a decisão mais simples: se a condição for verdadeira, o bloco roda, senão roda o bloco do else. A condição é uma expressão do tipo Boolean, que só aceita true e false. O programa abaixo começa pela forma que quase todo mundo conhece, em que o if apenas executa ações, e depois mostra o que muda em Kotlin:

fun main() {
    val nota = 7
    if (nota >= 7) {
        println("aprovado")
    } else {
        println("recuperação")
    }

    val situacao = if (nota >= 7) "aprovado" else "recuperação"
    println(situacao)

    val faixa = if (nota >= 9) "excelente" else if (nota >= 7) "bom" else "precisa melhorar"
    println(faixa)

    val mensagem = if (nota >= 7) {
        val bonus = 1
        "aprovado com ${nota + bonus} pontos"
    } else {
        "reprovado"
    }
    println(mensagem)
}
aprovado
aprovado
bom
aprovado com 8 pontos

A linha que muda está em val situacao = if (nota >= 7) "aprovado" else "recuperação". Aqui o if deixa de ser só uma ação e vira uma expressão, que entrega um valor. O valor do ramo escolhido vai para situacao. Como a expressão precisa sempre ter um resultado, o else passa a ser obrigatório. Sem ele, o compilador recusa o código e diz o que falta:

erro_if_sem_else.kt:2:13: error: 'if' must have both main and 'else' branches when used as an expression.
    val x = if (true) 1
            ^^

A mensagem em inglês quer dizer, em tradução livre, que o if precisa ter os ramos principal e else quando é usado como expressão. O erro protege você de uma variável que, num dos caminhos, nunca receberia valor. O compilador pega isso antes de o programa rodar, e não depois, quando um usuário recebe uma resposta vazia.

Cadeias como if ... else if ... else funcionam, e o programa usa uma na variável faixa. Quando elas passam de duas ou três condições sobre o mesmo valor, o when da seção 3 costuma deixar o trecho mais legível. Repare também no bloco da variável mensagem: um bloco entre chaves também é expressão, e o valor dele é a última linha. A variável bonus existe só dentro das chaves, o que mantém pequeno o escopo, isto é, o trecho do código onde um nome é visível.

A figura acompanha uma nota 7 por esse if. A condição é avaliada, só o ramo sim roda (linha grossa) e o valor dele chega à val. O ramo do não aparece pontilhado porque não roda com essa nota. O caminho vermelho mostra a versão sem else, que o compilador bloqueia antes de qualquer execução.

Uma nota 7 passa pelo if: só o ramo sim roda e seu valor vai para a val situacao, e sem else o código não compila
figura 1 · if como expressão: o valor do ramo vai para uma val

operadores de comparação e lógicos

Para montar condições, você tem os operadores de comparação ==, !=, <, >, <= e >=, e os lógicos && (e), || (ou) e ! (não). Os lógicos têm : o lado direito só é avaliado quando o esquerdo não decide sozinho o resultado. O programa abaixo imprime uma linha sempre que uma das funções é chamada, para deixar visível o que roda:

fun verdadeiro(): Boolean {
    println("verdadeiro() foi chamada")
    return true
}

fun falso(): Boolean {
    println("falso() foi chamada")
    return false
}

fun main() {
    println("a) false && verdadeiro() = ${false && verdadeiro()}")
    println("b) true || falso() = ${true || falso()}")
    println("c) true && falso() = ${true && falso()}")
}
a) false && verdadeiro() = false
b) true || falso() = true
falso() foi chamada
c) true && falso() = false

Na linha a), o lado esquerdo já é falso, então && nem chama verdadeiro(). Na linha b), o || encerra no true da esquerda e falso() não roda. Na linha c), o && precisa do lado direito, por isso falso() aparece. Esse comportamento importa quando o lado direito tem efeito ou custo, como uma consulta ao banco. Ele também permite escrever verificações em ordem, primeiro confirmando que o dado existe e só depois usando o dado.

igualdade: == contra ===

Igualdade tem dois operadores que parecem iguais e não são. O == compara conteúdo. A documentação oficial descreve a == b como uma tradução para a?.equals(b) ?: (b === null), ou seja, uma chamada a equals. O === pergunta outra coisa: se a e b apontam para o mesmo objeto na memória. Em Java, == entre duas Strings compara referências, e esse é um dos tropeços mais comuns de quem migra. Em Kotlin, == compara conteúdo, que é a comparação que você quase sempre quer.

O exemplo cria duas Strings com o mesmo texto. A primeira é um literal. A segunda é montada em tempo de execução com StringBuilder, uma classe que constrói texto aos poucos:

fun main() {
    val idade = 20
    val temCarteira = true
    println(idade >= 18 && temCarteira)
    println(idade < 18 || !temCarteira)

    val a = "kotlin"
    val b = StringBuilder("kot").append("lin").toString()
    println("a == b: ${a == b}")
    println("a === b: ${a === b}")
    println("a === a: ${a === a}")
}
true
false
a == b: true
a === b: false
a === a: true

Os dois primeiros == dão true, porque o conteúdo bate. O === dá false entre a e b, porque a segunda String é outro objeto, criado depois. O a === a dá true, porque é o mesmo objeto comparado consigo mesmo. A armadilha aparece quando os dois lados são literais iguais no mesmo programa:

fun main() {
    val a = "kotlin"
    val c = "kotlin"
    println(a === c)
}
true

Aqui o === responde true, mesmo sendo dois textos escritos separadamente. A página de igualdade da documentação mostra um caso parecido, sem explicar o motivo. O que sei é que a JVM costuma reaproveitar literais de texto iguais, e então eles são o mesmo objeto. Esse detalhe é comportamento da JVM e não consta na página consultada. O que importa para você: num teste pequeno, === pode parecer correto por acaso. Por isso, para comparar Strings, use ==.

3. when: o switch do Kotlin

Quando uma variável é comparada com muitos valores, a cadeia de if e else if fica longa e repete a variável em cada linha. O when resolve isso. Ele compara um valor com vários ramos e executa o primeiro que casar. Quem conhece o switch de Java vai reconhecer a ideia, com três diferenças práticas. O ramo não passa para o seguinte, o que em Java exige um break em cada caso. Um ramo aceita vários valores separados por vírgula. E um ramo pode ser um intervalo, uma verificação de tipo ou uma condição qualquer.

O programa abaixo tem uma função para cada forma de when:

fun classificarCodigo(codigo: Int): String = when (codigo) {
    200 -> "OK"
    404 -> "não encontrado"
    500, 503 -> "erro no servidor"
    in 300..399 -> "redirecionamento"
    else -> "outro"
}

fun descrever(valor: Any): String = when (valor) {
    is String -> "texto com ${valor.length} caracteres"
    is Int -> if (valor > 0) "inteiro positivo" else "inteiro não positivo"
    else -> "tipo que não tratei"
}

fun temperaturaEm(celsius: Int): String = when {
    celsius < 0 -> "abaixo de zero"
    celsius < 25 -> "agradável"
    else -> "quente"
}

fun main() {
    println(classificarCodigo(200))
    println(classificarCodigo(503))
    println(classificarCodigo(301))
    println(classificarCodigo(418))
    println(descrever("kotlin"))
    println(descrever(-3))
    println(descrever(2.5))
    println(temperaturaEm(-5))
    println(temperaturaEm(30))
}
OK
erro no servidor
redirecionamento
outro
texto com 6 caracteres
inteiro não positivo
tipo que não tratei
abaixo de zero
quente

A função classificarCodigo usa valores exatos, um ramo com vírgula (500, 503) e um intervalo (in 300..399). O else pega qualquer código que não casou. A função descrever usa is, que verifica o tipo do valor. Dentro do ramo is String, o compilador já trata o valor como String, então valor.length funciona sem conversão manual. Essa conversão automática se chama . A função temperaturaEm não tem valor no when. Cada ramo é uma condição, e vale a primeira que for verdadeira. Nesse caso o else é obrigatório, porque a documentação diz que um when sem argumento precisa dele.

Assim como o if, o when pode ser expressão. Quando isso acontece, ele precisa cobrir todos os casos possíveis. Para um Int, que tem bilhões de valores possíveis, a forma prática de cobrir tudo é o else. O compilador confirma:

erro_when_sem_else.kt:3:13: error: 'when' expression must be exhaustive. Add an 'else' branch.
    val s = when (n) {
            ^^^^

Em tradução livre, a mensagem diz que a expressão when precisa ser exaustiva e que é preciso adicionar um ramo else. Há casos em que o else não é necessário. Com Boolean, cobrir true e false basta:

fun main() {
    val ativo = true
    val texto = when (ativo) {
        true -> "ligado"
        false -> "desligado"
    }
    println(texto)
}
ligado

O when usado só para executar ações, sem entregar valor, também dispensa o else:

fun main() {
    val n = 3
    when (n) {
        1 -> println("um")
        2 -> println("dois")
    }
    println("when usado só para executar ações não precisa de else")
}
when usado só para executar ações não precisa de else

Quando o tipo é sealed, o que você vê na aula sobre sealed e when exaustivo, o when também dispensa o else desde que todos os subtipos estejam cobertos. Esse é o assunto daquela aula.

O when testa os ramos de cima para baixo e para no primeiro que casa. A figura acompanha o código 503 pela cascata. Os ramos 200 e 404 não casam, o terceiro casa, e o quarto ramo e o else ficam pontilhados porque nem chegam a rodar. A ordem dos ramos é a parte que mais causa erro quando as condições se sobrepõem, e a seção 7 mostra um caso.

O when testa 200, 404 e 500 ou 503 de cima para baixo, para no ramo que casa com o código 503 e deixa os demais sem rodar
figura 2 · when testa os ramos de cima para baixo e para no primeiro que casa

A figura usa losangos para as perguntas e caixas para os resultados. Cada losango só passa o valor adiante quando a resposta é não, e a caixa da direita só recebe o controle quando a resposta é sim.

4. laços

Um laço repete um trecho enquanto houver trabalho. O for percorre uma sequência de valores, como uma faixa de números ou os itens de uma coleção. O while repete enquanto uma condição for verdadeira. O for aparece em quase todo código Kotlin, então começamos por ele.

ranges e progressões

Um é um conjunto ordenado de valores com início e fim. A documentação define assim: um range representa um conjunto ordenado de valores com início e fim definidos, e por padrão avança de um em um (tradução livre). Para inteiros, o Kotlin trata a faixa como uma aritmética, um tipo chamado IntProgression, que guarda o primeiro valor, o último e o passo. Você não precisa decorar o nome do tipo para usar a faixa. Ele explica por que (1..5).toList() funciona: a faixa é uma sequência de números, e toList() copia esses números para uma lista.

A forma mais comum é 1..5, que inclui os dois extremos: começa em 1 e termina em 5. Para excluir o fim, use until, e 1 until 5 vai de 1 até 4. A página de ranges da documentação oficial apresenta a forma exclusiva como ..< e não menciona until. A referência da biblioteca padrão documenta until desde o Kotlin 1.0, e as duas formas funcionam na versão 2.1.21. Você vai encontrar until em muito código existente, por isso a aula usa as duas. O teste 4 in 1..<4 do exemplo abaixo mostra a forma nova.

downTo conta para trás: 5 downTo 1 vai de 5 até 1. step define o passo, então 1..10 step 3 começa em 1 e anda de três em três. O último valor é o último que cabe no passo, e nem sempre é o limite. Por isso 1..9 step 3 termina em 7: 1, 4 e 7 cabem, e o próximo valor, 10, passa do limite. A documentação descreve essa regra da mesma forma, para passos positivos. Uma faixa cujo início é maior que o fim é vazia, e o laço não roda nenhuma vez, como mostra (5..1).toList().

fun main() {
    for (i in 1..5) print("$i ")
    println()
    for (i in 1 until 5) print("$i ")
    println()
    for (i in 5 downTo 1) print("$i ")
    println()
    for (i in 1..10 step 3) print("$i ")
    println()
    println((1..5).toList())
    println((1 until 5).toList())
    println((5 downTo 1 step 2).toList())
    println((1..10 step 3).toList())
    println((5..1).toList())
    println((1 until 5).last)
    println(4 in 1..4)
    println(4 in 1..<4)
}
1 2 3 4 5 
1 2 3 4 
5 4 3 2 1 
1 4 7 10 
[1, 2, 3, 4, 5]
[1, 2, 3, 4]
[5, 3, 1]
[1, 4, 7, 10]
[]
4
true
false

A figura compara as três faixas. Cada círculo é um valor que o laço visita, na ordem das setas. Na primeira linha o 5 entra. Na segunda, a seta para no 4, porque o 5 fica do lado de fora do until. Na terceira, o laço pula os valores 4 e 2, que aparecem pontilhados.

Três faixas de números: 1..5 inclui o 5, 1 until 5 para antes dele, e 5 downTo 1 step 2 visita só 5, 3 e 1 pulando o 4 e o 2
figura 3 · 1..5 inclui o 5, 1 until 5 deixa o 5 de fora

for sobre coleções e índices

Quando você já tem uma coleção, o for percorre os elementos diretamente, sem índice: for (fruta in frutas). Se precisar da posição, há duas saídas. frutas.indices é a faixa de índices válidos, de 0 até o último, e equivale a 0 until frutas.size, com a vantagem de não errar o limite. withIndex() entrega cada elemento junto com o índice, e a desestruturação (indice, fruta) separa os dois em variáveis. O exemplo mostra as três formas e, por último, um passo de 2 para visitar só as posições pares:

fun main() {
    val frutas = listOf("maçã", "banana", "uva")
    for (fruta in frutas) println(fruta)
    for (i in frutas.indices) println("$i: ${frutas[i]}")
    for ((indice, fruta) in frutas.withIndex()) println("$indice -> $fruta")
    for (i in 0 until frutas.size step 2) println("par: ${frutas[i]}")
}
maçã
banana
uva
0: maçã
1: banana
2: uva
0 -> maçã
1 -> banana
2 -> uva
par: maçã
par: uva

A última linha usa 0 until frutas.size step 2, que visita as posições 0 e 2. Por isso par aparece para maçã e para uva, que estão nas posições pares.

A figura mostra o ciclo que o for executa por baixo dos panos. Antes de cada volta, o laço confere se ainda há valor na faixa ou na coleção. Se há, a variável recebe o valor e o corpo roda. A seta de volta leva ao losango, e a conferência se repete. Quando os valores acabam, a seta do não leva ao código depois do laço.

Ciclo do for: o laço confere se há próximo valor, guarda o valor na variável, roda o corpo e volta à conferência até a faixa acabar
figura 4 · for percorre a faixa: confere se há próximo valor, executa o corpo e volta

while e do-while

O while confere a condição antes de cada volta. Se ela já começa falsa, o corpo nunca roda. O do-while inverte a ordem: executa o corpo primeiro e só depois confere a condição, então o corpo roda pelo menos uma vez. O exemplo usa uma condição falsa desde o começo, n < 5 com n = 10, para mostrar a diferença, e também um while que roda três vezes:

fun main() {
    var n = 10
    while (n < 5) println("while: $n")
    println("while terminou sem executar")

    var m = 10
    do {
        println("do-while executou com m = $m")
    } while (m < 5)

    var contador = 0
    while (contador < 3) {
        contador++
        println("contador = $contador")
    }
}
while terminou sem executar
do-while executou com m = 10
contador = 1
contador = 2
contador = 3

Na saída, nenhuma linha de while: ... aparece, porque o laço nunca entrou no corpo. O do-while imprime a sua linha mesmo com a condição falsa. Use do-while quando o corpo precisa rodar pelo menos uma vez, como ler uma entrada antes de validá-la. Na maior parte do código, o while é o que você quer.

A figura coloca as duas formas lado a lado com a mesma condição falsa. No while, a condição vem antes do corpo, então a seta do sim fica pontilhada: o corpo não roda. No do-while, o corpo vem primeiro e roda uma vez, e só depois a condição é testada. Nos dois casos o fluxo termina no mesmo ponto.

Com a mesma condição falsa, o while nunca roda o corpo, enquanto o do-while roda uma vez antes de testar a condição
figura 5 · while testa antes do corpo; do-while executa o corpo antes de testar

break, continue e labels

break encerra o laço mais próximo. continue interrompe só a volta atual e passa para a próxima. O exemplo usa os dois na mesma faixa: quando i vale 2, continue pula o println; quando vale 4, break encerra o laço antes do 5.

fun main() {
    for (i in 1..5) {
        if (i == 2) continue
        if (i == 4) break
        println("i = $i")
    }
    println("fim do primeiro laço")

    externo@ for (i in 1..3) {
        for (j in 1..3) {
            if (i * j == 4) break@externo
            println("i=$i j=$j")
        }
    }
    println("fim do segundo laço")
}
i = 1
i = 3
fim do primeiro laço
i=1 j=1
i=1 j=2
i=1 j=3
i=2 j=1
fim do segundo laço

A segunda parte do arquivo mostra o caso que mais confunde: laços aninhados. Um break sozinho sai só do laço interno, porque é ele o mais próximo. Para sair dos dois de uma vez, marque o laço externo com um , que é um nome seguido de @ colocado antes do laço, e use break@nome dentro dele. No exemplo, externo@ marca o laço de i, e break@externo encerra os dois quando i * j vale 4. A saída mostra que i=2 j=2 nunca aparece, e que o laço externo também não chega ao i=3.

No laço, continue pula o valor 2 e volta ao próximo, e break com o valor 4 sai do laço inteiro e leva o fluxo para depois dele
figura 6 · continue pula para o próximo valor; break sai do laço inteiro

A figura da seção acompanha os cinco valores. O losango do continue manda o fluxo de volta ao próximo valor, pelo caminho pontilhado. O losango do break tem a saída sim em linha grossa, que vai direto para o código depois do laço. Os valores que não disparam nenhum dos dois passam pelo corpo, e depois o fluxo volta ao início.

O continue também aceita label. O teste abaixo tem a mesma estrutura de antes, com continue@loop no lugar do break:

fun main() {
    loop@ for (i in 1..3) {
        for (j in 1..3) {
            if (j == 2) continue@loop
            println("$i, $j")
        }
    }
}
1, 1
2, 1
3, 1

Quando j chega a 2, o laço interno é abandonado para essa volta, e o laço externo segue normalmente. Por isso cada i imprime apenas o j igual a 1.

A figura abaixo mostra o break@externo saindo dos dois laços. A caixa maior é o laço externo, e a menor, dentro dela, é o laço interno. A seta grossa atravessa as duas fronteiras, e o fluxo chega direto ao código depois do laço externo.

Dois laços aninhados: quando i vezes j dá 4, o break@externo sai dos dois laços de uma vez e o fluxo chega ao código depois
figura 7 · break@externo sai dos dois laços de uma vez
Pergunta de entrevistaComo o when do Kotlin difere do switch do Java?

O when compara um valor com vários ramos e executa o primeiro que casar, sem passar para o seguinte, então não precisa de break. Os ramos aceitam vírgulas, intervalos como in 300..399 e verificações de tipo com is, e dentro de um ramo is String o compilador já faz o smart cast. Assim como o if, o when pode ser uma expressão, e então precisa ser exaustivo. Para um Int, a forma prática é um else. Para Boolean basta cobrir true e false. Com um tipo sealed, basta cobrir todos os subtipos, sem else. O benefício é que, ao adicionar um subtipo, o compilador aponta os when que ficaram incompletos, em vez de o erro aparecer em execução.

5. expressões e instruções

Esta é a ideia que mais pesa na escrita de Kotlin. Uma expressão é um trecho que produz um valor: nota >= 7, "aprovado" e um if com else são expressões. Uma instrução é um trecho que executa uma ação sem entregar valor a quem o escreveu, como uma atribuição, um for ou um while. Algumas linguagens tratam essa separação com flexibilidade. O Kotlin a aplica, e o compilador recusa uma instrução onde espera uma expressão.

Essa separação muda a escrita de duas formas. A primeira é que você pode colocar o resultado de um if ou de um when direto numa val, em vez de declarar uma variável sem valor e preenchê-la em cada ramo. A segunda é que algumas construções que parecem expressões não são. Os exemplos a seguir mostram o que o compilador responde a três dessas tentativas, com a mensagem real de cada uma.

O primeiro caso é o operador ternário, que em Java escreve uma decisão numa linha só, no formato condição ? a : b. O Kotlin não tem esse operador, e a documentação trata o if como o equivalente, já que ele funciona como expressão. Tentar usar o ternário dá este erro:

erro_ternario.kt:2:18: error: syntax error: Unexpected tokens (use ';' to separate expressions on the same line).
    val x = true ? 1 : 2
                 ^^^^^^^

Em tradução livre, a mensagem diz que há um erro de sintaxe e que os tokens inesperados não podem ficar na mesma linha sem ;. Ela não é amigável, mas aponta a coluna exata. O conserto é escrever if (condicao) a else b.

O segundo caso é a atribuição. Em C e em Java, atribuição também é expressão, e um bug clássico é escrever = no lugar de == dentro de um if. Em Kotlin, atribuição não é expressão, e o compilador pega o erro:

erro_if_atribuicao.kt:3:9: error: only expressions are allowed in this context.
    if (x = 2) println("x vale dois")
        ^^^^^

A mesma regra vale para val b = (a = 2), que dá a mensagem only expressions are allowed in this context. (tradução livre: apenas expressões são permitidas neste contexto).

O terceiro caso é o while, que não produz valor. A tentativa de guardá-lo numa variável não compila:

erro_while_expressao.kt:2:13: error: only expressions are allowed here.
    val r = while (false) { }
            ^^^^^^^^^^^^^^^^^

A mensagem diz, em tradução livre, que apenas expressões são permitidas aqui. Se você precisa de um resultado de um laço, declare a variável antes do laço e atualize-a dentro dele. O for tem o mesmo comportamento, e o compilador responde a ele com a mesma mensagem.

Na prática, a pergunta que vale fazer ao escrever é se o trecho produz um valor que alguém vai usar. Se sim, escreva como expressão, com if ou when e os respectivos else. Se não, é uma instrução, e o else só aparece quando a lógica pedir.

6. exercício guiado: boletim de notas

O enunciado é este. Uma turma tem cinco notas, de 0 a 100. Para cada aluno, precisamos mostrar o conceito (A, B, C ou D), se foi aprovado (nota 60 ou mais) e, no fim, quantos foram aprovados. Uma nota fora de 0 a 100 deve aparecer como inválida, e não como aprovada ou reprovada.

O conceito fica com um when sobre o valor, usando intervalos com in. Os intervalos não se sobrepõem, e o else pega o que não cai em nenhum deles. A situação usa um when sem argumento, porque cada ramo é uma condição diferente. A ordem dos ramos importa de verdade nessa função: a validação nota !in 0..100 vem primeiro, porque é ela que impede a nota 101 de cair no ramo nota >= 60. Testei a ordem invertida, e a nota 101 saiu como aprovado. A seção 7 volta a esse caso.

O laço usa withIndex() para ter a posição de cada aluno. A posição recebe 1 a mais na impressão, porque pessoas contam a partir de um e índices começam em zero. O contador aprovados é incrementado por um if sem else, o que é válido porque esse if é uma instrução e nenhum valor é usado dele.

fun conceito(nota: Int): String = when (nota) {
    in 90..100 -> "A"
    in 75..89 -> "B"
    in 60..74 -> "C"
    in 0..59 -> "D"
    else -> "inválida"
}

fun situacao(nota: Int): String = when {
    nota !in 0..100 -> "inválida"
    nota >= 60 -> "aprovado"
    else -> "reprovado"
}

fun main() {
    val notas = listOf(95, 82, 60, 59, 101)
    var aprovados = 0
    for ((posicao, nota) in notas.withIndex()) {
        if (situacao(nota) == "aprovado") aprovados++
        println("aluno ${posicao + 1}: nota $nota, conceito ${conceito(nota)}, ${situacao(nota)}")
    }
    println("aprovados: $aprovados de ${notas.size}")
}
aluno 1: nota 95, conceito A, aprovado
aluno 2: nota 82, conceito B, aprovado
aluno 3: nota 60, conceito C, aprovado
aluno 4: nota 59, conceito D, reprovado
aluno 5: nota 101, conceito inválida, inválida
aprovados: 3 de 5
Pergunta de entrevistaQual a diferença entre expressão e instrução em Kotlin, e por que isso importa?

Uma expressão produz um valor, como nota >= 7 ou um if com else. Uma instrução executa uma ação sem entregar valor, como uma atribuição, um for ou um while. O Kotlin aplica essa separação, e isso muda a escrita. Posso colocar o resultado de um if ou de um when direto numa val, em vez de declarar uma variável vazia e preenchê-la em cada ramo. Isso também explica por que não existe operador ternário, já que o if com else já é uma expressão, e por que a atribuição não é expressão. Em Java, escrever = no lugar de == dentro de um if é um bug clássico, e em Kotlin o compilador recusa o código.

7. erros comuns e boas práticas

Alguns destes erros o compilador pega sozinho, e os outros só aparecem na execução, que é o pior momento para descobri-los.

esquecer o else numa expressão if

Já vimos a mensagem na seção 2. Vale repetir a lição: quando o if está do lado direito de um =, ele precisa de else. O compilador é o colega que lembra do else que você esqueceu, e ainda bem que ele não tira folga.

com .. e until

Off-by-one é o erro de passar uma posição do limite, para mais ou para menos. A faixa com .. é a causa mais comum entre iniciantes. A segunda parte do código abaixo usa 0..nomes.size, que inclui a posição 3, enquanto a lista tem só três elementos, nas posições 0, 1 e 2:

fun main() {
    val nomes = listOf("Ana", "Bruno", "Caio")
    for (i in 0 until nomes.size) println("ok: ${nomes[i]}")
    for (i in 0..nomes.size) println(nomes[i])
}
ok: Ana
ok: Bruno
ok: Caio
Ana
Bruno
Caio
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 3 out of bounds for length 3
	at java.base/java.util.Arrays$ArrayList.get(Arrays.java:4266)
	at Off_by_oneKt.main(off_by_one.kt:4)

A primeira parte, com 0 until nomes.size, roda certinho. A segunda passa do último índice e lança a exceção. Nesta execução, com listOf, a exceção foi ArrayIndexOutOfBoundsException, e a pilha de chamadas mostra que a lista por trás era um Arrays$ArrayList. A classe da exceção depende da implementação de List que está por trás, e este teste mostrou só essa. A correção é trocar .. por until, ou usar nomes.indices.

comparar Strings com === em vez de ==

Este é o tropeço da seção 2, e ele é perigoso porque às vezes funciona. O === parece correto quando os dois lados são literais iguais, como no teste da seção 2. Quando a String é montada em tempo de execução, o === responde false mesmo com o mesmo conteúdo, como vimos com StringBuilder. Para comparar texto, use ==. Se você não tem certeza de qual operador quer, o == é quase sempre a resposta.

modificar a variável do laço

A variável de um for não pode ser reatribuída dentro do corpo. A tentativa de incrementá-la dá este erro:

erro_reatribui_laco.kt:3:9: error: 'val' cannot be reassigned.
        i = i + 1
        ^

Em tradução livre, a mensagem diz que 'val' não pode ser reatribuída. Se você precisa de um valor que muda, copie o valor da variável do laço para uma var antes de mexer nele. A variável do laço continua sendo o valor que a faixa entregou.

ordem dos ramos no when

Um when para no primeiro ramo que casa. Se dois ramos podem casar com o mesmo valor, quem vem antes ganha. O FizzBuzz é o exemplo clássico: 15 é múltiplo de 3 e de 5, então um ramo que testa múltiplo de 3 antes do ramo de 15 nunca deixa o 15 chegar ao próprio ramo.

fun fizzBuzzErrado(n: Int): String = when {
    n % 3 == 0 -> "Fizz"
    n % 5 == 0 -> "Buzz"
    n % 15 == 0 -> "FizzBuzz"
    else -> n.toString()
}

fun main() {
    println("15 -> ${fizzBuzzErrado(15)}")
    println("10 -> ${fizzBuzzErrado(10)}")
}
15 -> Fizz
10 -> Buzz

A saída mostra o problema: o 15 virou Fizz, enquanto o 10 saiu certo como Buzz. A regra prática é colocar primeiro o caso mais específico, o que exige todas as condições, e deixar os casos gerais para depois. O else fica sempre por último. O mesmo erro aparece no boletim da seção 6, quando a validação vem depois do ramo de aprovação:

fun situacaoErrada(nota: Int): String = when {
    nota >= 60 -> "aprovado"
    nota !in 0..100 -> "inválida"
    else -> "reprovado"
}

fun main() {
    println(situacaoErrada(101))
}
aprovado

usar when sem else quando o tipo tem casos abertos

Quando o when é expressão sobre um tipo com casos que podem crescer, o else resolve o erro de compilação, mas esconde um problema de manutenção. Se alguém adiciona um caso novo ao tipo e o else continua lá, o caso novo cai no else em silêncio, sem nenhum aviso. Para tipos com casos conhecidos, a opção melhor é sealed, porque o compilador passa a apontar cada when que deixou de cobrir o caso novo.

boas práticas que decorrem disso

Use indices ou withIndex() em vez de contar posições na mão. Escreva if e when com else quando forem expressões, e confira se o else não está escondendo casos que deveriam ser tratados. Dê nome às condições longas, como uma val aprovado calculada a partir da nota, para que a decisão apareça no código. E prefira when a uma cadeia de else if quando a mesma variável é comparada várias vezes. Cada uma dessas práticas evita um dos erros acima, pelo mesmo motivo: o código deixa a intenção escrita, e nem o compilador nem o leitor precisam adivinhá-la.

8. exercícios

Tente cada exercício antes de olhar a resposta, e rode o trecho para conferir.

  1. Escreva um programa que imprima os números de 1 a 15 com a regra do FizzBuzz: múltiplo de 3 vira Fizz, múltiplo de 5 vira Buzz, múltiplo de 15 vira FizzBuzz, e os demais imprimem o próprio número. A resposta vem abaixo. Repare que o teste do 15 vem primeiro.
fun fizzBuzzCorreto(n: Int): String = when {
    n % 15 == 0 -> "FizzBuzz"
    n % 3 == 0 -> "Fizz"
    n % 5 == 0 -> "Buzz"
    else -> n.toString()
}

fun main() {
    for (n in 1..15) println(fizzBuzzCorreto(n))
}
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
  1. O código do off-by-one tem um erro de limite. Corrija-o para imprimir cada nome sem lançar exceção, usando indices. A resposta vem abaixo, com a versão que imprime também a posição.
fun main() {
    val nomes = listOf("Ana", "Bruno", "Caio")
    for (i in nomes.indices) println(nomes[i])
    for (i in 0 until nomes.size) println("posição $i: ${nomes[i]}")
}
Ana
Bruno
Caio
posição 0: Ana
posição 1: Bruno
posição 2: Caio
  1. Escreva um laço while que some 1, 2, 3 e assim por diante até a soma passar de 100, e depois imprima quantos números foram somados e a soma final. Antes de rodar, faça a conta: a soma de 1 a 14 dá 105, e a soma de 1 a 13 dá 91, que ainda não passa de 100. A resposta vem abaixo.
fun main() {
    var soma = 0
    var n = 0
    while (soma <= 100) {
        n++
        soma += n
    }
    println("somou $n números e chegou a $soma")
}
somou 14 números e chegou a 105
  1. Sem rodar, diga qual é a saída de (1..10 step 4).toList() e de (10 downTo 1 step 4).toList(). Depois rode para conferir. A resposta é [1, 5, 9] e [10, 6, 2]. A primeira faixa começa em 1 e anda de quatro em quatro sem passar de 10. A segunda começa em 10 e desce de quatro em quatro sem passar de 1.
fun main() {
    println((1..10 step 4).toList())
    println((10 downTo 1 step 4).toList())
}
[1, 5, 9]
[10, 6, 2]

9. glossário

  • Expressão: trecho de código que produz um valor, como nota >= 7 ou um if com else.
  • Instrução: trecho que executa uma ação sem entregar valor, como uma atribuição, um for ou um while.
  • Fluxo de controle: conjunto de construções que decide qual instrução roda em seguida.
  • Condição: expressão do tipo Boolean, que vale true ou false.
  • Curto-circuito: regra de && e || que deixa de avaliar o lado direito quando o esquerdo já decide o resultado.
  • Igualdade estrutural: comparação com ==, que verifica se o conteúdo é igual.
  • Igualdade referencial: comparação com ===, que verifica se os dois nomes apontam para o mesmo objeto.
  • Smart cast: conversão de tipo que o compilador faz sozinho depois de uma checagem como is String.
  • Range: conjunto ordenado de valores com início e fim, escrito com .., until ou ..<.
  • Progressão: sequência de valores com primeiro valor, último valor e passo. Em Kotlin, o tipo se chama IntProgression para inteiros.
  • Laço: trecho que se repete enquanto uma condição ou uma faixa ainda tiver valores.
  • Label: nome seguido de @, usado para identificar um laço e referenciá-lo em break@nome ou continue@nome.
  • Off-by-one: erro de passar uma posição do limite de uma faixa ou de um índice.

Fontes

Consultadas em 04/11/2026.

Os testes rodaram com kotlinc 2.1.21, o compilador do plugin Kotlin do IntelliJ IDEA instalado nesta máquina, executando os programas com java -jar sobre o OpenJDK 21.0.8 (Temurin). Nenhum trecho gerou aviso de compilação. Não testei versões posteriores à 2.1.21. As citações de documentação em inglês estão traduzidas livremente e marcadas no texto. Não encontrei na documentação consultada a explicação para o reaproveitamento de literais pela JVM, e a afirmação sobre ele está marcada como comportamento observado.

Relacionados

↑ Voltar ao topo

Atalhos de teclado

Abrir busca
/
Alternar tema claro/escuro
t
Aula anterior na série
←
Próxima aula na série
→
Lista de atalhos
?