CodeGym /Cursos /Kotlin SELF /try/catch em torno de operacoes de arquivo

try/catch em torno de operacoes de arquivo

Kotlin SELF
Nível 45, Lição 2
Disponível

1. Por que try/catch e necessario mesmo depois de pre-verificacoes

Quando voce faz pre-verificacoes pela primeira vez, aparece a sensacao: “Pronto, eu verifiquei exists() e canRead() — entao da para ler!” E e aqui que o Kotlin (e o SO) lembram, com jeitinho: “ha-ha, nao”. As pre-verificacoes dizem apenas que no momento da verificacao tudo parecia ok. Mas a operacao real readText() ja e uma tentativa de verdade de ler dados, e ela ainda pode terminar com uma excecao.

Vamos imaginar uma situacao simples: voce verificou exists() == true, e um milissegundo depois o arquivo foi removido (ou renomeado) por outro processo/usuario. Isso nao e ficcao — e a vida normal em um computador.

import java.io.File

fun main() {
    val file = File("data/input.txt")

    if (file.exists()) {
        // Between exists() and readText() the file may disappear.
        val text = file.readText()
        println("OK: ${text.length}") // if you are lucky
    }
}

Se nao der sorte — voce vai receber uma excecao ja na leitura. E isso e normal: try/catch nao e um substituto para pre-verificacoes, e sim a segunda barreira, que precisa estar em torno do I/O real.

2. try estreito: por dentro — somente I/O real

O erro mais comum de iniciante e este: “vou envolver tudo em um try grande, para com certeza nao cair”. E como vestir um colete a prova de balas na geladeira: pesado, desconfortavel, e a geladeira tanto faz. Um try grande deixa o codigo turvo: nao fica claro o que exatamente poderia quebrar, e no catch voce frequentemente tenta “dar um jeito de continuar”, sem entender em que estado estao os dados.

Antiexemplo: try largo demais.

import java.io.File
import java.io.IOException

fun main() {
    val file = File("data/amount.txt")

    try {
        val text = file.readText()
        val amount = text.trim().toInt() // this is NOT I/O anymore, this is data format
        println("amount=$amount")
    } catch (e: IOException) {
        println("Nao foi possivel ler o arquivo")
    }
}

O problema aqui e sutil: toInt() pode lancar NumberFormatException, e isso nao e IOException. Ou seja, o programa vai cair do mesmo jeito, mas ja “de um jeito estranho”: afinal, voce “envolveu tudo”. Mas nao envolveu tudo — so a parte de I/O.

E melhor separar responsabilidades: ler o arquivo e I/O e e tratado como IOException, e analisar o texto ja e um “erro de dados/formato” e deve ficar separado (mesmo que hoje a gente esteja focando especificamente em I/O).

import java.io.File
import java.io.IOException

fun readTextOrNull(file: File): String? =
    try {
        file.readText()
    } catch (e: IOException) {
        null
    }

fun main() {
    val file = File("data/amount.txt")

    val text = readTextOrNull(file) ?: run {
        println("Arquivo nao foi lido: ${file.path}")
        return
    }

    val amount = text.trim().toIntOrNull()
    println("amount=$amount") // amount=123 or amount=null
}

Repare na sensacao do codigo: a leitura esta separada, o try e estreito, e depois voce ja decide com calma o que fazer com o conteudo.

Um pequeno “mapa” de responsabilidades e util ate em projetos de console:

flowchart TD
    A[CLI / main] --> B["loadRawText()"]
    B -->|I/O: readText| C[(File)]
    B -->|String? / erro| A
    A --> D[parse / validate]
    D -->|erro de formato| A

3. try como expressao: devolvemos um valor, nao “desmontamos”

O Kotlin sabe fazer algo que deixa o tratamento de erros bem mais agradavel: try/catch e uma expressao, ou seja, pode retornar um valor. Isso permite escrever funcoes com um contrato claro: “se deu certo — retorno o resultado; se nao — retorno uma alternativa”. No Kotlin, esse estilo e considerado idiomatico: try retorna um valor, e catch retorna outro.

A variante mais simples para ler um arquivo: “texto ou null”.

import java.io.File
import java.io.IOException

fun readTextOrNull(file: File): String? = try {
    file.readText()
} catch (e: IOException) {
    null
}

fun main() {
    val file = File("data/input.txt")

    val text = readTextOrNull(file)
    println(text?.take(20) ?: "NO DATA") // for example: NO DATA
}

Do mesmo jeito, para escrita voce pode escolher o contrato “sucesso/fracasso”.

import java.io.File
import java.io.IOException

fun writeTextSafely(file: File, text: String): Boolean = try {
    file.writeText(text)
    true
} catch (e: IOException) {
    false
}

fun main() {
    val ok = writeTextSafely(File("data/out/report.txt"), "Hello!\n")
    println("written=$ok") // written=true or written=false
}

A ideia parece simples, mas ela disciplina o programa: em vez de esperar “ah, nao vai cair”, voce ja projeta com antecedencia: o que vamos retornar quando der problema.

4. Contrato de funcao em I/O: T?, Boolean ou propagacao de excecao

Quando voce coloca try/catch, voce sempre toma uma decisao: o que o codigo chamador vai ver? Em aplicativos de console para estudo, geralmente tres contratos bastam: retornar T?, retornar Boolean, ou nao capturar a excecao aqui e deixar ela subir (as vezes isso tambem e uma estrategia valida).

E bom manter essa comparacao na cabeca como uma tabela:

Contrato da funcao Como fica Quando e conveniente Custo do erro
T?
String?, List<String>?
Quando ausencia de resultado e um ramo normal da logica Voce precisa lembrar de tratar
null
Boolean
true/false
Quando os dados nao importam, so importa “se deu certo” Diagnostico mais fraco: por que
false
?
Propagar a excecao sem try/catch (ou throw adiante) Quando, sem o arquivo, o programa nao faz sentido O programa pode cair se, acima, ninguem tratar

Importante: nao existe “o correto de uma vez por todas”. Nos escolhemos o que for mais conveniente no seu aplicativo e neste ponto do codigo.

Por exemplo, no nosso aplicativo de console para estudo, vamos ter um “cofrinho de gastos”: armazenamos despesas em um arquivo de texto e mostramos um relatorio curto. Se o arquivo nao foi lido, da para exibir uma mensagem clara e iniciar com uma lista vazia — isso e um otimo candidato ao contrato List<String>?.

import java.io.File
import java.io.IOException

fun readLinesOrNull(file: File): List<String>? = try {
    file.readLines()
} catch (e: IOException) {
    null
}

fun main() {
    val file = File("data/expenses.txt")
    val lines = readLinesOrNull(file) ?: emptyList()

    println("loaded=${lines.size}") // loaded=0 if the file doesn't exist/can't be read
}

Ja a escrita do relatorio — pode ser Boolean: se nao escreveu, apenas avisamos.

5. Mensagens de erro: para o usuario e para voce

Uma mensagem de erro nao e uma obra de arte e nao e “algo deu errado”. Uma boa mensagem responde pelo menos a tres perguntas: o que voce queria fazer, com o que exatamente (caminho), e qual e o tipo do problema. Mesmo sem sistema de logging, isso ja ajuda bastante.

Aqui vale a pena criar um pequeno formatador de mensagens para I/O. Ele nao precisa ser inteligente — precisa ser estavel e consistente em todo o aplicativo.

import java.io.File
import java.io.IOException

fun ioErrorMessage(action: String, file: File, e: IOException): String {
    val type = e::class.simpleName ?: "IOException"
    val detail = e.message ?: "no details"
    return "Nao foi possivel $action: ${file.path}. Motivo: $type: $detail"
}

E usar no catch:

import java.io.File
import java.io.IOException

fun readTextOrNullVerbose(file: File): String? = try {
    file.readText()
} catch (e: IOException) {
    println(ioErrorMessage("ler o arquivo", file, e))
    null
}

fun main() {
    val text = readTextOrNullVerbose(File("data/input.txt"))
    println(text?.length ?: "no text") // no text
}

Sim, nos imprimimos direto no console — e isso e normal para um CLI de estudo. Em projetos mais serios, voce separaria “mensagem para o usuario” e “diagnostico para logs”, mas hoje nos conscientemente nao vamos entrar em logging.

Varios catch: ordem e sentido da separacao

As vezes da vontade de capturar tudo com um unico catch (IOException). Isso e ok, mas muitas vezes e util distinguir pelo menos um caso particular: “arquivo nao encontrado/nao abre”, ou seja, FileNotFoundException. Apos isso, voce pode dar um texto mais humano: “verifique o caminho” em vez de “ops, I/O”.

Regra no Kotlin (e, no geral, na JVM): primeiro capturamos o mais especifico, depois o mais geral, senao o especifico nunca vai disparar.

import java.io.File
import java.io.FileNotFoundException
import java.io.IOException

fun main() {
    val file = File("data/input.txt")

    try {
        val text = file.readText()
        println("OK: ${text.length}") // OK: ...
    } catch (e: FileNotFoundException) {
        println("Arquivo nao encontrado ou nao abre: ${file.path}")
    } catch (e: IOException) {
        println("Erro de entrada/saida ao ler: ${file.path}")
        println("Detalhes: ${e.message}")
    }
}

Aqui, o importante e nao complicar demais: separamos apenas o caso mais frequente, e o resto deixamos em IOException.

6. Onde colocar try/catch: limite de responsabilidade

Da muita vontade de capturar excecoes no main, porque “da para ver tudo por la”. Mas conforme o aplicativo cresce, isso vira um trambolho enorme: o main le entrada, faz parse de comandos, trabalha com dados, e ainda captura todos os possiveis problemas. Um bom estilo e capturar I/O onde ele acontece e devolver para fora um resultado compreensivel.

Para o nosso rastreador de gastos, isso significa: funcoes de “armazenamento” por si so envolvem readText()/writeText() em try/catch, e o main recebe ou dados, ou um sinal de “nao deu”.

O diagrama fica simples:

flowchart TD
    M[main: comandos do usuario] --> S[storage: read/write file]
    S -->|List⟨String⟩? / Boolean| M
    S -->|IOException dentro| S

Exemplo de uma mini-camada de armazenamento (sem “magica”, tudo curto e explicito):

import java.io.File
import java.io.IOException

fun loadExpensesOrNull(file: File): List<String>? = try {
    file.readLines()
} catch (e: IOException) {
    null
}

fun saveExpenses(file: File, lines: List<String>): Boolean = try {
    file.writeText(lines.joinToString(separator = "\n"))
    true
} catch (e: IOException) {
    false
}

E uso no main sem a sensacao de que voce esta operando uma usina nuclear:

import java.io.File

fun main() {
    val file = File("data/expenses.txt")

    val lines = loadExpensesOrNull(file)
    if (lines == null) {
        println("Nao foi possivel carregar as despesas; vamos iniciar com uma lista vazia.")
    } else {
        println("Registros carregados: ${lines.size}") // for example: Registros carregados: 12
    }
}

Psicologicamente isso e importante: o main nao precisa saber como exatamente o SO recusou a leitura. O main precisa tomar uma decisao: continuar com dados vazios, pedir para o usuario corrigir o caminho, encerrar — e assim por diante.

7. finally e use {}: quando sao necessarios

A palavra finally muitas vezes soa como “tecnica secreta de jedi”. Na pratica, ela serve para uma coisa so: garantir que um certo trecho de codigo execute no final, mesmo que dentro do try aconteca um erro. Em tarefas com arquivos, isso e especialmente importante para fechar recursos, se voce abre streams/readers manualmente.

Mas o Kotlin oferece um estilo ainda mais conveniente: use {}, que fecha automaticamente o recurso (mais ou menos como finally, so que mais limpo). Isso e considerado o approach idiomatico para AutoCloseable.

Hoje nao vamos nos aprofundar em streams, mas um pequeno exemplo e util para ver o limite: readText() nao exige use, mas bufferedReader() ja exige.

import java.io.File
import java.io.IOException

fun readFirstLineOrNull(file: File): String? = try {
    file.bufferedReader().use { reader ->
        reader.readLine()
    }
} catch (e: IOException) {
    null
}

fun main() {
    val first = readFirstLineOrNull(File("data/input.txt"))
    println(first ?: "<empty>") // for example: <empty>
}

Guarde uma regra simples: se voce ve ...Reader(), ...Writer(), InputStream, OutputStream — quase sempre deve existir use {} por perto (ou, no minimo, finally). Se voce usa funcoes “prontas” como readText()/writeText(), elas mesmas abrem e fecham recursos internamente, e voce nao precisa escrever finally manualmente.

Um pequeno detalhe do Kotlin 2.x: o compilador ficou melhor em rastrear informacoes de smart-cast atraves de catch/finally, entao em alguns cenarios ele sugere com mais cuidado onde uma variavel volta a ficar nullable. Isso esta relacionado a melhorias no tratamento de excecoes no K2.

8. Erros tipicos ao usar try/catch em torno de operacoes com arquivos

Erro nº 1: um try enorme, dentro do qual ficam leitura do arquivo, parse de dados e metade da logica de negocio.
Esse codigo e ruim porque voce perde a nocao da causa da falha: foi I/O, foi formato, foi sua logica? E, alem disso, no catch aparece a tentacao de “terminar alguma coisa”, embora os dados possam nem ter sido carregados. A cura e simples: extraia o I/O para funcoes pequenas e mantenha o try estreito — em torno de readText()/writeText() e operacoes proximas.

Erro nº 2: “abafar” a excecao em silencio.
Retornar null ou false e normal, mas, se voce nao reporta absolutamente nada, quando houver um problema real voce vai depurar “por que esta vazio” em vez de “por que nao foi lido”. Mesmo em um CLI de estudo, e util imprimir pelo menos uma vez: acao, caminho, e.message. Senao, o diagnostico vira chute.

Erro nº 3: capturar Exception “por via das duvidas”.
Parece cuidado com estabilidade, mas na pratica costuma ser um estilo de varrer para debaixo do tapete: voce comeca a misturar erros de I/O, erros de formato e seus proprios bugs. Como resultado, o programa “parece funcionar”, mas faz coisas estranhas. Para arquivos, mantenha o foco em IOException (e, as vezes, FileNotFoundException como caso particular), e nao esconda outros erros sem motivo.

Erro nº 4: retornar Boolean, mas nao dar nenhuma informacao sobre “por que false”.
Boolean e conveniente quando so importa o fato de sucesso. Mas se, mesmo assim, voce as vezes precisa entender a causa, da para (no escopo de hoje) ao menos imprimir uma mensagem dentro do catch, ou retornar null/String? com o texto do erro. O principal e nao criar uma API em que false significa dez causas diferentes e voce nao tem como descobrir qual delas.

Erro nº 5: ordem errada dos catch.
Se voce escrever primeiro catch (e: IOException) e depois catch (e: FileNotFoundException), o segundo bloco vira inalcançavel, porque FileNotFoundException e um caso particular de IOException. A ordem deve ser “primeiro o especifico, depois o geral”, como nas regras classicas de tratamento de excecoes.

Erro nº 6: continuar usando dados depois de uma leitura malsucedida.
Esse e um bug sutil: voce capturou a excecao, mostrou “nao foi possivel”, mas a variavel ficou, por exemplo, como string vazia, e voce continua fazendo parse como se aquilo fosse o conteudo do arquivo. No fim, voce recebe um erro secundario (por exemplo, NumberFormatException) e se confunde sobre o que realmente quebrou. Se a leitura falhou — retorne null/false e interrompa esse caminho de execucao explicitamente.

Erro nº 7: achar que canRead()/canWrite() e garantia.
Essas verificacoes ajudam, mas nao prometem sucesso, porque permissoes podem mudar, o arquivo pode estar ocupado, o sistema de arquivos pode “cair”, e assim por diante. Por isso, a combinacao correta e: pre-verificacoes melhoram mensagens e o fail fast, e try/catch assegura a operacao real.

1
Tarefa
Kotlin SELF, nível 45, lição 2
Bloqueado
Prévia da nota
Prévia da nota
1
Tarefa
Kotlin SELF, nível 45, lição 2
Bloqueado
Gravação segura
Gravação segura
1
Tarefa
Kotlin SELF, nível 45, lição 2
Bloqueado
Mensagem para o log
Mensagem para o log
1
Tarefa
Kotlin SELF, nível 45, lição 2
Bloqueado
Quantia do arquivo
Quantia do arquivo
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION