Exceções (Exceptions) - Lidando com o Inesperado
Em programação, nem tudo sai como planejado. Exceções são a maneira estruturada e robusta que o C# e a plataforma .NET nos dão para lidar com erros e situações inesperadas que ocorrem durante a execução de um programa. Pense em uma exceção como um "sinal de alerta". Quando um problema ocorre (ex: tentar dividir por zero), o sistema dispara esse sinal. Se não houver um plano para "capturar" esse alerta, o programa inteiro para e quebra.
1. Introdução a Exceções
Uma exceção é um objeto que representa um erro. Em C#, todas as exceções herdam da classe base System.Exception
. Elas contêm informações valiosas sobre o erro, como uma mensagem descritiva (Message
) e o "rastro da pilha" (StackTrace
), que é um mapa de onde no código o erro ocorreu. O objetivo de usar exceções é separar a lógica de tratamento de erros da lógica principal do programa, tornando o código mais limpo e organizado.
2. Realizando a Leitura de um Arquivo
A leitura de arquivos é um exemplo clássico onde exceções são comuns, pois a operação depende de fatores externos ao seu programa:
- O arquivo pode não existir (
FileNotFoundException
). - O caminho para o arquivo pode estar incorreto (
DirectoryNotFoundException
). - Seu programa pode não ter permissão para ler o arquivo (
UnauthorizedAccessException
). - O arquivo pode estar em uso por outro processo (
IOException
).
Exemplo (código que pode quebrar):
// Se "arquivo_inexistente.txt" não existir, esta linha vai disparar
// uma FileNotFoundException e o programa irá travar.
string conteudo = File.ReadAllText("caminho/para/arquivo_inexistente.txt");
Console.WriteLine(conteudo);
3. Disparando uma Exceção (throw
)
Além de tratar exceções do sistema, você pode criar e "disparar" suas próprias exceções para sinalizar erros específicos da sua regra de negócio. Isso é feito com a palavra-chave throw
.
Informação Adicional: É uma boa prática criar suas próprias classes de exceção personalizadas para erros de negócio (ex: SaldoInsuficienteException), herdando de ApplicationException.
public void Sacar(decimal valor)
{
if (valor > this.Saldo)
{
// Dispara uma exceção para sinalizar que a regra de negócio foi violada.
throw new Exception("Saldo insuficiente para realizar o saque.");
}
this.Saldo -= valor;
}
4. Tratando uma Exceção (try-catch
)
Para evitar que o programa quebre, colocamos o código "perigoso" dentro de um bloco try
e o código que trata o erro dentro de um bloco catch
.
try
{
// Tenta executar o código que pode gerar uma exceção.
string conteudo = File.ReadAllText("caminho/para/arquivo_inexistente.txt");
Console.WriteLine(conteudo);
}
catch (Exception ex)
{
// Se uma exceção ocorrer no bloco 'try', o código aqui é executado.
Console.WriteLine($"Ocorreu um erro ao ler o arquivo: {ex.Message}");
}
// O programa continua a execução normalmente a partir daqui.
5. Exceção Genérica e Específica
É possível ter múltiplos blocos catch
para tratar diferentes tipos de exceções de maneiras diferentes.
- Específica: Captura um tipo de erro bem definido (ex:
FileNotFoundException
). Esta é a melhor prática, pois permite dar um tratamento adequado para cada tipo de erro. - Genérica: Captura
Exception
, a classe base. Funciona como um "pega-tudo" para qualquer erro não tratado pelos blocoscatch
específicos.
Informação Adicional: A ordem importa! Os blocos catch
devem ser organizados do mais específico para o mais genérico.
try
{
// ... código ...
}
catch (FileNotFoundException ex)
{
Console.WriteLine("Erro: O arquivo não foi encontrado. Verifique o caminho.");
}
catch (UnauthorizedAccessException ex)
{
Console.WriteLine("Erro: Sem permissão para ler o arquivo.");
}
catch (Exception ex) // Genérico, sempre por último
{
Console.WriteLine($"Ocorreu um erro inesperado: {ex.Message}");
}
6. Entendendo o Bloco finally
O bloco finally
contém um código que será executado sempre, não importa se:
- O bloco
try
foi executado com sucesso. - Uma exceção foi disparada e capturada por um
catch
. - Uma exceção foi disparada e não foi capturada.
Seu principal uso é para "limpeza", ou seja, liberar recursos importantes como fechar conexões de banco de dados ou arquivos.
StreamReader leitor = null;
try
{
leitor = new StreamReader("meu_arquivo.txt");
// ... lê o arquivo ...
}
catch (Exception ex)
{
// ... trata o erro ...
}
finally
{
// Este bloco executa sempre!
if (leitor != null)
{
leitor.Close(); // Garante que o arquivo seja fechado.
}
}
Informação Adicional (Melhor Prática): Em C#, para objetos que precisam ser "limpos" (que implementam a interface IDisposable
), a instrução using
é a forma moderna e preferida de garantir a limpeza, pois ela gera um bloco try...finally
por baixo dos panos.
// Jeito moderno e mais seguro:
using (StreamReader leitor = new StreamReader("meu_arquivo.txt"))
{
// ... usa o leitor ...
} // O leitor é fechado automaticamente aqui, mesmo se ocorrer um erro.
Exemplo com Try Catch e Finally
try
{
string[] linhas = File.ReadAllLines("arquivos/arquivoLeitura.txt");
foreach (string linha in linhas)
{
Console.WriteLine(linha);
}
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"Ocorreu um erro na leitura do arquivo. Arquivo não encontrado. {ex.Message}");
}
catch (DirectoryNotFoundException ex)
{
Console.WriteLine($"Ocorreu um erro na leitura do arquivo. Caminho da pasta não encontrado. {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Ocorreu um erro: {ex.Message}");
}
finally
{
Console.WriteLine("Chegou no bloco finally.");
}
7. Usando o Throw
(dentro de um catch
)
Às vezes, você captura uma exceção, faz algo com ela (como registrar o erro em um log) e depois quer "relançá-la" para que uma camada superior do sistema também possa tratá-la.
Informação Adicional (Ponto Crítico):
throw;
: Relança a exceção original, preservando oStackTrace
. É a forma correta de se fazer.throw ex;
: Lança a exceção como se ela tivesse se originado no blococatch
, destruindo oStackTrace
original. Isso dificulta muito a depuração e deve ser evitado.
try
{
// ...
}
catch (Exception ex)
{
// 1. Loga o erro para análise futura.
Console.WriteLine("Registrando log do erro...");
// 2. Relança a exceção para a camada superior (forma correta).
throw;
}
---
Parte 2: Coleções Genéricas - Organizando Seus Dados
Coleções são classes projetadas para armazenar e gerenciar grupos de objetos de forma eficiente. O C# oferece várias delas no namespace System.Collections.Generic
, cada uma com um comportamento e caso de uso específico.
8. Introdução a Filas e Fila na Prática (Queue<T>
)
Uma Fila (Queue<T>
) é uma coleção baseada no princípio FIFO (First-In, First-Out) — o primeiro a entrar é o primeiro a sair.
- Analogia: Uma fila de banco ou de supermercado.
- Métodos Principais:
Enqueue(item)
: Adiciona um item no final da fila. Dequeue()
: Remove e retorna o item do início da fila.Peek()
: Retorna o item do início da fila sem removê-lo.
Caso de Uso Prático: Processamento de tarefas em ordem de chegada, como uma fila de impressão ou o atendimento de chamados em um sistema de suporte.
// Fila de senhas para atendimento
Queue<string> filaAtendimento = new Queue<string>();
// Pessoas chegam e entram no final da fila
filaAtendimento.Enqueue("Senha-001");
filaAtendimento.Enqueue("Senha-002");
filaAtendimento.Enqueue("Senha-003");
// Atendendo na ordem de chegada
while (filaAtendimento.Count > 0)
{
string proximaSenha = filaAtendimento.Dequeue();
Console.WriteLine($"Chamando para atendimento: {proximaSenha}");
}
FIFO
Em programação, FIFO (First-In, First-Out ou "Primeiro a Entrar, Primeiro a Sair") refere-se a uma estrutura de dados, como uma fila, onde o primeiro elemento adicionado é o primeiro a ser removido. É um princípio fundamental em sistemas operacionais para o gerenciamento de processos, em redes para o roteamento de dados e em serviços de impressão para gerenciar trabalhos, garantindo a ordem de processamento.
9. Introdução a Pilhas e Pilha na Prática (Stack<T>
)
Uma Pilha (Stack<T>
) é o oposto da fila. Ela é baseada no princípio LIFO (Last-In, First-Out) — o último a entrar é o primeiro a sair.
- Analogia: Uma pilha de pratos. Você coloca um prato no topo e pega o prato do topo.
- Métodos Principais:
Push(item)
: Adiciona um item no topo da pilha. Pop()
: Remove e retorna o item do topo da pilha.Peek()
: Retorna o item do topo da pilha sem removê-lo.
Caso de Uso Prático: Histórico de navegação ("voltar"), função "Desfazer" (Undo) em editores de texto.
// Histórico de páginas visitadas
Stack<string> historicoNavegador = new Stack<string>();
// Navegando em páginas
historicoNavegador.Push("google.com");
historicoNavegador.Push("docs.microsoft.com");
historicoNavegador.Push("github.com"); // Última página visitada
// Clicando no botão "Voltar"
Console.WriteLine($"Voltando da página: {historicoNavegador.Pop()}"); // Sai github.com
Console.WriteLine($"Voltando da página: {historicoNavegador.Pop()}"); // Sai docs.microsoft.com
LIFO
Em programação, LIFO (do inglês Last-In-First-Out, que significa "o último a entrar, o primeiro a sair") é um princípio de estrutura de dados utilizado em pilhas. O funcionamento é análogo a uma pilha de pratos: o último item adicionado ao topo da pilha é o primeiro a ser retirado, com as operações de inserção chamadas de push
e remoção de pop
. Essa lógica é aplicada em mecanismos de desfazer/refazer, funções recursivas e na navegação de páginas web.
10. Introdução ao Dictionary
e Removendo/Alterando Elementos
Um Dicionário (Dictionary<TKey, TValue>
) é uma coleção que armazena pares de chave-valor. Cada valor é associado a uma chave única.
- Analogia: Um dicionário de palavras ou uma agenda de contatos (o nome é a chave, o telefone é o valor).
- Principal Vantagem: Busca de valores extremamente rápida através da chave.
Métodos e Operações:
- Adicionar:
meuDicionario.Add(chave, valor);
- Acessar/Alterar:
meuDicionario[chave] = novoValor;
- Remover:
meuDicionario.Remove(chave);
- Verificar se existe:
meuDicionario.ContainsKey(chave);
Informação Adicional (Melhor Prática): Para obter um valor de forma segura, useTryGetValue
. Ele é mais eficiente do que verificar comContainsKey
e depois acessar o valor, pois faz apenas uma busca na coleção.