C# finally
Na aula try e catch, você aprendeu a tratar uma exceção. Mas há coisas que precisam acontecer dando certo ou dando errado: fechar um arquivo, devolver uma conexão, mostrar "fim do atendimento". Para isso existe um terceiro bloco, o finally ("finalmente"). Nesta aula você vai ver quando ele roda, em que ordem, como ele salva os dados de um arquivo e os poucos casos em que ele não roda.
- Escrever
try/catch/finallye o caminho do programa em cada caso. - Usar
try/finallysemcatch, deixando a exceção subir. - O que acontece com o
finallyquando a exceção não é tratada. - O
finallyjunto comreturn,breakecontinue. - Fechar um arquivo no
finally, e o que se perde quando ele não é fechado. - Os erros
CS0157,CS1524eCS0103, e quando ofinallynão roda.
Todos os programas rodaram na máquina da Ana, com o .NET 10 (C# 14), usando dotnet run arquivo.cs na pasta ~/cs.
A ideia antes do código
No fim do expediente, a Ana fecha o caixa da lojinha. Não importa como foi o dia: vendeu muito, vendeu pouco, a maquininha deu problema, faltou troco. O caixa sempre é fechado, contado e trancado antes de ela ir embora. Se ela esquecer num dia ruim, justamente no dia em que tudo deu errado, o prejuízo é maior.
O finally é esse "fechar o caixa": um bloco que roda sempre no fim do try, tenha acontecido exceção ou não, tenha ela sido tratada ou não.
1. try, catch e finally
O programa abaixo registra duas vendas. A primeira dá certo; a segunda tem um valor inválido:
string[] valores = ["10", "abc"];
foreach (string v in valores)
{
Console.WriteLine($"--- {v} ---");
try
{
Console.WriteLine("Abrindo o caixa");
int n = int.Parse(v);
Console.WriteLine($"Venda de R$ {n}");
}
catch (FormatException)
{
Console.WriteLine("Valor inválido");
}
finally
{
Console.WriteLine("Fechando o caixa");
}
}
$ dotnet run caixa.cs --- 10 --- Abrindo o caixa Venda de R$ 10 Fechando o caixa --- abc --- Abrindo o caixa Valor inválido Fechando o caixa
- try { … }
- O código vigiado, como na aula anterior.
- catch (FormatException) { … }
- Roda só se o
trylançarFormatException. - finally
- "Finalmente." Vem depois de todos os
catch, e só pode haver um. - { Console.WriteLine("Fechando o caixa"); }
- Roda sempre: depois do
tryque deu certo, ou depois docatchque tratou o problema.
Os dois caminhos, lado a lado:
| Bloco | Com "10" (tudo certo) | Com "abc" (exceção) |
|---|---|---|
try | roda inteiro | roda até o int.Parse; o resto é pulado |
catch (FormatException) | pulado | roda |
finally | roda | roda |
| depois do bloco todo | roda | roda |
Se é para rodar sempre, por que não escrever a linha simplesmente depois do try/catch? Porque "depois" nem sempre é alcançado: a exceção pode não ser do tipo do catch, o catch pode lançar outra, o método pode ter um return no meio. O finally roda em todos esses casos, como as próximas seções mostram.
Missão: o programa "esquece" de fechar o caixa. Acrescente um finally, depois do catch, que mostre Fechando o caixa. A mensagem deve aparecer nas duas vendas.
string[] valores = ["10", "abc"];
foreach (string v in valores)
{
Console.WriteLine($"--- {v} ---");
try
{
Console.WriteLine("Abrindo o caixa");
int n = int.Parse(v);
Console.WriteLine($"Venda de R$ {n}");
}
catch (FormatException)
{
Console.WriteLine("Valor inválido");
}
}
string[] valores = ["10", "abc"];
foreach (string v in valores)
{
Console.WriteLine($"--- {v} ---");
try
{
Console.WriteLine("Abrindo o caixa");
int n = int.Parse(v);
Console.WriteLine($"Venda de R$ {n}");
}
catch (FormatException)
{
Console.WriteLine("Valor inválido");
}
finally
{
Console.WriteLine("Fechando o caixa");
}
}
2. try/finally sem catch
O catch é opcional. Um try pode ter só o finally. Isso faz sentido quando o método não sabe tratar o problema, mas precisa arrumar a casa antes de a exceção subir para quem o chamou:
try
{
Vender("abc");
}
catch (FormatException)
{
Console.WriteLine("Main: valor inválido");
}
void Vender(string valor)
{
try
{
Console.WriteLine("Abrindo o caixa");
int n = int.Parse(valor);
Console.WriteLine($"Venda de R$ {n}");
}
finally
{
Console.WriteLine("Fechando o caixa");
}
}
$ dotnet run sobe.cs Abrindo o caixa Fechando o caixa Main: valor inválido
Siga a ordem: o int.Parse lançou dentro de Vender. Ali não há catch, então a exceção vai subir, mas antes de sair do método o finally roda (Fechando o caixa). Só então a exceção chega às instruções do topo, onde o catch a trata (Main: valor inválido).
As combinações permitidas são três: try/catch, try/finally e try/catch/finally. Um try sozinho é erro (CS1524, em Quando dá errado).
3. E se ninguém tratar a exceção?
Sem nenhum catch no caminho, a exceção encerra o programa. O finally roda mesmo assim, mas repare quando:
try
{
Console.WriteLine("Abrindo o caixa");
int n = int.Parse("abc");
}
finally
{
Console.WriteLine("Fechando o caixa");
}
Console.WriteLine("Fim");
$ dotnet run naotratada.cs Abrindo o caixa Unhandled exception. System.FormatException: The input string 'abc' was not in a correct format. at System.Number.ThrowFormatException[TChar](ReadOnlySpan`1 value) at System.Int32.Parse(String s) at Program.<Main>$(String[] args) in /home/ana/cs/naotratada.cs:line 4 Fechando o caixa $ echo $? 134
O relatório da exceção apareceu antes do Fechando o caixa. O .NET trabalha em duas etapas: primeiro ele procura um catch em toda a pilha; como não achou nenhum, já escreve o relatório. Depois ele desce a pilha rodando os finally e, por fim, encerra o programa com 134. O Fim, que está fora do try, nunca roda.
Rodando este programa no editor desta página, a ordem é a mesma: o relatório, depois o Fechando o caixa e o código de saída 134. Só o rastro muda um pouco: aparecem mais linhas at System.… de dentro do int.Parse (seis em vez de duas). A linha do seu arquivo (naotratada.cs:line 4) e a mensagem são iguais nos dois.
4. finally com return, break e continue
O finally também roda quando o try termina "pulando para fora" sem exceção nenhuma. O caso mais comum é um return:
Console.WriteLine(Dobro(21));
int Dobro(int n)
{
try
{
Console.WriteLine("No try, antes do return");
return n * 2;
}
finally
{
Console.WriteLine("No finally");
}
}
$ dotnet run retorno.cs No try, antes do return No finally 42
O return n * 2; calcula o valor (42) e se prepara para sair do método. Antes de sair, roda o finally (No finally). Só depois o 42 chega ao Console.WriteLine da linha 1, que o mostra.
Num laço, break e continue (aula break e continue) também passam pelo finally:
for (int i = 1; i <= 3; i++)
{
try
{
if (i == 2) continue;
if (i == 3) break;
Console.WriteLine($"Volta {i}");
}
finally
{
Console.WriteLine($"finally da volta {i}");
}
}
$ dotnet run laco.cs Volta 1 finally da volta 1 finally da volta 2 finally da volta 3
- Volta 1
- O
tryrodou inteiro; depois, ofinally. - Volta 2
- O
continuepulou para a próxima volta, mas passou pelofinallyantes. - Volta 3
- O
breaksaiu do laço, também passando pelofinally.
5. O uso de verdade: fechar o que foi aberto
Mostrar mensagens é bom para entender, mas o trabalho real do finally é liberar recursos: fechar arquivos, conexões com bancos de dados, conexões de rede. A Ana grava a lista de compras num arquivo com um StreamWriter ("escritor de fluxo", aula StreamReader e StreamWriter). No meio, uma quantidade inválida lança uma exceção:
var escritor = new StreamWriter("compras.txt");
try
{
escritor.WriteLine("arroz");
escritor.WriteLine("feijão");
int quantidade = int.Parse("duas");
escritor.WriteLine($"café x{quantidade}");
}
catch (FormatException)
{
Console.WriteLine("Quantidade inválida");
}
Console.WriteLine("Fim do programa");
$ dotnet run lista.cs Quantidade inválida Fim do programa $ ls -l compras.txt -rw-rw-r-- 1 ana ana 0 out 17 13:02 compras.txt $ cat compras.txt
O arquivo foi criado, mas tem 0 bytes: nem o arroz nem o feijão, que foram escritos antes da exceção, chegaram ao disco. O motivo: o StreamWriter guarda o texto numa área de memória (o buffer) e só grava no disco de vez em quando, ou quando é fechado. Ninguém o fechou, e o texto se perdeu quando o programa terminou.
Com o Close() ("fechar") no finally, o arquivo é fechado com ou sem exceção:
var escritor = new StreamWriter("compras.txt");
try
{
escritor.WriteLine("arroz");
escritor.WriteLine("feijão");
int quantidade = int.Parse("duas");
escritor.WriteLine($"café x{quantidade}");
}
catch (FormatException)
{
Console.WriteLine("Quantidade inválida");
}
finally
{
escritor.Close();
Console.WriteLine("Arquivo fechado");
}
Console.WriteLine("Fim do programa");
$ dotnet run lista2.cs Quantidade inválida Arquivo fechado Fim do programa $ cat compras.txt arroz feijão
- var escritor = new StreamWriter("compras.txt");
- Abre (e cria) o arquivo. Fica fora do
try: assim a variável existe nofinally. Se a abertura falhar, não há nada para fechar, e ofinallynem é alcançado. - escritor.WriteLine("arroz");
- Escreve no buffer, ainda não no disco.
- escritor.Close();
- Grava o que estava no buffer e fecha o arquivo.
O café não entrou, porque a linha dele nunca rodou, mas o que já tinha sido escrito foi salvo. É isso que o finally garante.
Missão: rode e veja que o arquivo sai vazio (depois de No arquivo: não aparece nada). Acrescente um finally que feche o escritor com escritor.Close(); e rode de novo: o arroz e o feijão devem aparecer.
var escritor = new StreamWriter("compras.txt");
try
{
escritor.WriteLine("arroz");
escritor.WriteLine("feijão");
int quantidade = int.Parse("duas");
escritor.WriteLine($"café x{quantidade}");
}
catch (FormatException)
{
Console.WriteLine("Quantidade inválida");
}
Console.WriteLine("No arquivo:");
Console.WriteLine(File.ReadAllText("compras.txt"));
var escritor = new StreamWriter("compras.txt");
try
{
escritor.WriteLine("arroz");
escritor.WriteLine("feijão");
int quantidade = int.Parse("duas");
escritor.WriteLine($"café x{quantidade}");
}
catch (FormatException)
{
Console.WriteLine("Quantidade inválida");
}
finally
{
escritor.Close();
}
Console.WriteLine("No arquivo:");
Console.WriteLine(File.ReadAllText("compras.txt"));
Quando a abertura também pode falhar
Às vezes a própria abertura precisa estar dentro do try, porque é ela que pode lançar (uma pasta que não existe, um arquivo sem permissão). Aí a variável é declarada antes, valendo null, e o finally só fecha se ela chegou a ser criada:
StreamWriter? escritor = null;
try
{
escritor = new StreamWriter("/pasta-que-nao-existe/compras.txt");
escritor.WriteLine("arroz");
}
catch (DirectoryNotFoundException e)
{
Console.WriteLine($"Não deu para criar: {e.Message}");
}
finally
{
escritor?.Close();
Console.WriteLine("finally rodou");
}
$ dotnet run nulo.cs Não deu para criar: Could not find a part of the path '/pasta-que-nao-existe/compras.txt'. finally rodou
- StreamWriter? escritor = null;
- O
?diz que a variável pode ficarnull(aula Referências Anuláveis). Ela nasce fora dotrypara existir nofinally. - DirectoryNotFoundException
- "Pasta não encontrada": Could not find a part of the path, "não foi possível encontrar uma parte do caminho".
- escritor?.Close();
- O
?.só chamaClose()seescritornão fornull(aula Operadores ??, ?. e ??=). Aqui ele énull, porque a abertura falhou, e nada acontece.
using
Abrir, usar e fechar no finally é tão comum que o C# tem um atalho para isso: using var escritor = new StreamWriter("compras.txt");. O compilador gera o try/finally sozinho. É a forma que o guia usa com arquivos, e ela tem aula própria: using e IDisposable. Vale entender o finally primeiro, porque é exatamente isso que o using faz por baixo.
Formas diferentes, qual usar
| Situação | Forma |
|---|---|
| Tratar um problema | try/catch |
| Arrumar a casa, mas deixar a exceção subir | try/finally |
| Tratar e arrumar a casa | try/catch/finally |
Fechar arquivo, conexão ou qualquer coisa com Dispose() | using (é um try/finally pronto): using e IDisposable |
| Algo que só deve rodar quando deu certo | No fim do try, não no finally |
| Algo que só deve rodar quando deu errado | No catch, não no finally |
Quando dá errado
try
{
Console.WriteLine("oi");
}
Console.WriteLine("fim");
$ dotnet run sotry.cs /home/ana/cs/sotry.cs(4,1): error CS1524: Catch ou finally esperado Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: um try sozinho não serve para nada; depois das chaves dele, o compilador esperava um catch ou um finally.
Como consertar: acrescente o catch ou o finally que faltou, ou tire o try.
return dentro do finally
Console.WriteLine(Teste());
int Teste()
{
try
{
return 1;
}
finally
{
return 2;
}
}
$ dotnet run retfin.cs /home/ana/cs/retfin.cs(10,9): error CS0157: O controle não pode sair do corpo de uma cláusula finally Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: "o controle não pode sair do corpo de um finally". O finally é para arrumar a casa, não para decidir o resultado. return, break e continue que saiam dele são proibidos (em Java isso é permitido, e é fonte de defeitos difíceis de achar).
Como consertar: deixe o return no try (ou depois do bloco) e use o finally só para liberar recursos.
try não existe no finally
try
{
var escritor = new StreamWriter("compras.txt");
escritor.WriteLine("arroz");
}
finally
{
escritor.Close();
}
$ dotnet run escopo.cs /home/ana/cs/escopo.cs(8,5): error CS0103: O nome "escritor" não existe no contexto atual Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que significa: o try e o finally são blocos separados; o que nasce num não existe no outro (aula Escopo).
Como consertar: declare a variável antes do try, como em lista2.cs ou em nulo.cs. Ou use using.
finally não rodou
Há poucos casos, todos em que o programa é encerrado "à força":
try
{
Console.WriteLine("Saindo com Environment.Exit");
Environment.Exit(3);
}
finally
{
Console.WriteLine("finally");
}
$ dotnet run sair.cs Saindo com Environment.Exit $ echo $? 3
O que significa: Environment.Exit(3) encerra o programa na hora, com o código de saída 3, sem passar pelos finally. O mesmo acontece num estouro da pilha (Stack overflow., aula Exceções), quando a memória acaba ou quando o processo é morto de fora (kill -9, falta de luz).
Como consertar: evite Environment.Exit no meio do programa; prefira terminar normalmente (um return nas instruções do topo). E nunca confie no finally para algo que precisa sobreviver a uma queda de energia: para isso, grave os dados assim que puder.
Referência
| Sintaxe / caso | O que acontece |
|---|---|
try { … } finally { … } | O finally roda sempre; a exceção, se houver, continua subindo |
try { … } catch (T) { … } finally { … } | O finally roda depois do try ou do catch |
return no try | O valor é calculado, o finally roda, e só então o método devolve |
break / continue no try | O finally roda antes de sair da volta |
| Exceção não tratada | O relatório sai primeiro, depois roda o finally, depois o programa termina com 134 |
Environment.Exit(n), Stack overflow, kill -9 | O finally não roda |
CS1524 | try sem catch nem finally |
CS0157 | return, break ou continue saindo do finally |
CS0103 | Variável declarada no try usada no finally |
Exercícios
1. O bloco que sempre roda
Complete:
2. Fechar se existir
Complete com o operador que só chama Close() se escritor não for null:
3. O código do erro
Que erro (CSxxxx) dá um return dentro do finally?
4. O que este trecho mostra?
try
{
Console.WriteLine("A");
int x = int.Parse("x");
Console.WriteLine("B");
}
catch (FormatException)
{
Console.WriteLine("C");
}
finally
{
Console.WriteLine("D");
}
Console.WriteLine("E");
5. Um método tem try { return 5; } finally { Console.WriteLine("F"); }. O que acontece ao chamá-lo?
6. Em qual caso o finally não roda?
Missão final: o método Vender não sabe tratar o valor inválido (quem trata é o código do topo), mas precisa fechar o caixa de qualquer jeito. Acrescente ao try de Vender um finally (sem catch!) que mostre Fechando o caixa.
try
{
Vender("abc");
}
catch (FormatException)
{
Console.WriteLine("Main: valor inválido");
}
void Vender(string valor)
{
Console.WriteLine("Abrindo o caixa");
int n = int.Parse(valor);
Console.WriteLine($"Venda de R$ {n}");
}
try
{
Vender("abc");
}
catch (FormatException)
{
Console.WriteLine("Main: valor inválido");
}
void Vender(string valor)
{
try
{
Console.WriteLine("Abrindo o caixa");
int n = int.Parse(valor);
Console.WriteLine($"Venda de R$ {n}");
}
finally
{
Console.WriteLine("Fechando o caixa");
}
}
Resumo
finallyvem depois doscatche roda sempre: com sucesso, com exceção tratada e com exceção não tratada (nesse caso, depois do relatório).try/finallysemcatcharruma a casa e deixa a exceção subir para quem sabe tratá-la.return,breakecontinuenotrypassam pelofinallyantes de sair. Sair de dentro dofinallyé proibido (CS0157).- O uso de verdade é liberar recursos: um
StreamWriterque não é fechado pode perder tudo o que foi escrito. - A variável do recurso nasce antes do
try(senão,CS0103); se a abertura pode falhar, comece comnulle feche com?.Close(). - O
finallynão roda comEnvironment.Exit, estouro da pilha ou processo morto. - Na prática, para arquivos, use
using, que gera otry/finallypara você.