Ao desenvolver aplicações em C# e .NET, a adoção de certas práticas e o conhecimento de funcionalidades específicas da lingaugem podem levar a um código mais limpo, fácil de manter e, em alguns casos, mais performático. Este artigo explora diversas técnicas que podem ser aplicadas para refatorar e otimizar seu código.
1. O Operador Coalescente Nulo (??)
É comum em programação a necessidade de verificar se uma variável ou objeto é nulo e, se for, atribuir um valor padrão. Tradicionalmente, isso poderia ser feito com uma estrutura condicional if:
string nomeUsuario = valorRecebido;
if (nomeUsuario == null)
{
nomeUsuario = string.Empty;
}
Uma versão mais concisa utiliza o operador ternário (?:):
string nomeUsuario = valorRecebido == null ? string.Empty : valorRecebido;
O C# oferece o operador coalescente nulo (??), que simplifica ainda mais essa lógica. Ele retorna o operando esquerdo se não for nulo; caso contrário, retorna o operando direito.
string nomeUsuario = valorRecebido ?? string.Empty;
Podemos até combinar isso com um método de extensão para tratamento de strings, garantindo que mesmo strings com apenas espaços em branco sejam tratadas como nulas antes da coalescência:
public static class UtilitariosDeString
{
public static string ParaNuloSeVazioOuEspacos(this string entrada)
{
return string.IsNullOrWhiteSpace(entrada) ? null : entrada.Trim();
}
}
A utilização seria:
string tituloProcesso = nomeTarefa?.ParaNuloSeVazioOuEspacos() ?? "Nenhum Título Informado";
2. Conversão de Tipos com o Operador as
A conversão de tipos em C# pode ser feita de várias maneiras. Ao realizar um cast explícito, é comum verificar o tipo primeiro com is para evitar uma exceção InvalidCastException:
if (minhaEntidade is FuncionarioAssalariado)
{
var funcAssalariado = (FuncionarioAssalariado)minhaEntidade;
// Lógica específica para funcionário assalariado
Console.WriteLine($"Salário semanal: {funcAssalariado.SalarioSemanal}");
}
Essa abordagem, embora segura, executa duas verificações de tipo: uma para is e outra para o cast. O operador as realiza a conversão e, se o tipo não for compatível, retorna null em vez de lançar uma exceção. Além disso, a verificação e a conversão são feitas em uma única operação, o que pode ser mais eficiente.
var funcAssalariado = minhaEntidade as FuncionarioAssalariado;
if (funcAssalariado != null)
{
// Lógica específica para funcionário assalariado
Console.WriteLine($"Salário semanal: {funcAssalariado.SalarioSemanal}");
}
3. Propriedades Automáticas
Introduzidas no C# 3.0, as propriedades automáticas simplificam significativamente a declaração de propriedades, eliminando a necessidade de declarar campos de apoio explícitos. Antes, a definição de uma propriedade com um campo de apoio privado era comum:
public class Coordenada
{
private int _x;
private int _y;
public int X
{
get { return _x; }
set { _x = value; }
}
public int Y
{
get { return _y; }
set { _y = value; }
}
}
Com propriedades automáticas, o compilador gera o campo de apoio automaticamente, resultando em um código muito mais conciso:
public class Coordenada
{
public int X { get; set; }
public int Y { get; set; }
}
É possível também atribuir modificadores de acesso diferentes aos acessadores get e set, criando propriedades somente leitura ou somente escrita internamente:
public class Configuracao
{
public string NomeConfiguracao { get; private set; } // Somente leitura externa
public double ValorInterno { private get; set; } // Somente escrita externa
}
4. Medindo o Tempo de Execução com Stopwatch
Para medir com precisão o tempo de execução de um método ou de um bloco de código, é tentador usar DateTime.Now:
DateTime inicio = DateTime.Now;
ExecutarOperacaoDemorada();
DateTime fim = DateTime.Now;
Console.WriteLine($"A operação levou {(fim - inicio).TotalMilliseconds} ms");
No entanto, DateTime.Now não é ideal para medições de desempenho de alta precisão, pois sua granularidade pode não ser suficiente e é suscetível a ajustes de relógio do sistema. Para medições precisas, utilize a classe Stopwatch do namespace System.Diagnostics:
using System.Diagnostics;
// ...
var cronometro = Stopwatch.StartNew();
ExecutarOperacaoDemorada();
cronometro.Stop();
Console.WriteLine($"A operação levou {cronometro.ElapsedMilliseconds} ms");
Stopwatch utiliza um contador de desempenho de alta resolução, se disponível no sistema, garantindo medições mais exatas.
5. Utilizando Métodos Estáticos de TimeSpan
Ao definir intervalos de tempo ou durações, é comum encontrar métodos que recebem um int ou long como argumento, deixando a unidade de tempo ambígua:
void ProcessarDadosRemotos(int tempoLimite) { /* ... */ }
// É segundos? Milissegundos?
ProcessarDadosRemotos(3000);
A classe TimeSpan é a escolha apropriada para representar durações. No entanto, seus construtores podem ser confusos devido às múltiplas sobrecargas:
TimeSpan(int horas, int minutos, int segundos);
TimeSpan(int dias, int horas, int minutos, int segundos, int milissegundos);
Para melhorar a clareza, use os métodos estáticos de fábrica de TimeSpan:
void ProcessarDadosRemotos(TimeSpan tempoLimite) { /* ... */ }
// ...
// Mais claro
ProcessarDadosRemotos(TimeSpan.FromMilliseconds(50));
ProcessarDadosRemotos(TimeSpan.FromSeconds(30));
// Declaração de campo
private static readonly TimeSpan _tempoLimitePadrao = TimeSpan.FromMinutes(5);
Esses métodos tornam a intenção do código explícita e reduzem a chance de erros.
6. Verificação de Strings: IsNullOrEmpty() e IsNullOrWhiteSpace()
Para validar se uma string é nula ou vazia, o .NET Framework 2.0 introduziu string.IsNullOrEmpty(). Uma verificação manual anterior seria:
public string ObterNomeArquivo(string caminhoCompleto)
{
if (caminhoCompleto == null || caminhoCompleto.Length == 0)
{
throw new ArgumentNullException(nameof(caminhoCompleto));
}
// ... lógica
return "nome_arquivo.txt";
}
Com IsNullOrEmpty, o código fica mais limpo:
public string ObterNomeArquivo(string caminhoCompleto)
{
if (string.IsNullOrEmpty(caminhoCompleto))
{
throw new ArgumentNullException(nameof(caminhoCompleto));
}
// ... lógica
return "nome_arquivo.txt";
}
No .NET Framework 4.0, string.IsNullOrWhiteSpace() foi adicionado. Ele verifica se a string é nula, vazia ou contém apenas caracteres de espaço em branco. Anteriormente, para esta verificação, poderia ser necessário usar Trim(), o que cria um novo objeto string:
public string MontarNomeCompleto(string primeiro, string meio, string ultimo)
{
if (meio == null || meio.Trim().Length == 0)
{
return $"{primeiro} {ultimo}";
}
return $"{primeiro} {meio} {ultimo}";
}
Com IsNullOrWhiteSpace(), o código é mais eficiente e legível:
public string MontarNomeCompleto(string primeiro, string meio, string ultimo)
{
if (string.IsNullOrWhiteSpace(meio))
{
return $"{primeiro} {ultimo}";
}
return $"{primeiro} {meio} {ultimo}";
}
7. Comparando Strings com string.Equals()
Ao comparar strings, a utilização do método .Equals() em uma instância pode levar a um NullReferenceException se a string for nula:
public Pedido CriarPedido(string tipoPedido, string produto)
{
// Se tipoPedido for null, isso lançará uma exceção
if (tipoPedido.Equals("online"))
{
// ...
}
return new Pedido();
}
Para evitar isso, é comum adicionar uma verificação de nulo:
if (tipoPedido != null && tipoPedido.Equals("online"))
Uma forma mais robusta e concisa é usar a versão estática de string.Equals(), que trata nulos sem exceções:
if (string.Equals(tipoPedido, "online")) // Retorna false se tipoPedido for null
Para comparações que ignoram maiúsculas/minúsculas ou são independentes da cultura, o enum StringComparison deve ser usado. Evite converter para maiúsculas/minúsculas antes de comparar, pois isso cria novas strings:
// Comparação case-insensitive e invariante à cultura, tratando nulos
if (string.Equals(tipoPedido, "online", StringComparison.InvariantCultureIgnoreCase))
8. Gerenciamento de Recursos com a Declaração using
Objetos que implementam a interface IDisposable (como conexões de banco de dados, fluxos de arquivo) precisam ter seus recursos liberados explicitamente através do método Dispose(). Sem a declaração using, o código pode se tornar verboso e propenso a vazamentos de recursos se exceções ocorrerem:
public IEnumerable<string> ObterRegistros()
{
var registros = new List<string>();
var conexao = new SqlConnection("ConnectionStringExemplo");
var comando = new SqlCommand("SELECT Nome FROM Tabela", conexao);
SqlDataReader leitor = null;
try
{
conexao.Open();
leitor = comando.ExecuteReader();
while (leitor.Read())
{
registros.Add(leitor["Nome"].ToString());
}
}
finally
{
leitor?.Dispose();
comando.Dispose();
conexao.Dispose();
}
return registros;
}
A declaração using garante que Dispose() seja chamado automaticamente no final do bloco, mesmo que uma exceção ocorra. Isso simplifica o código e melhora a confiabilidade:
public IEnumerable<string> ObterRegistros()
{
var registros = new List<string>();
using (var conexao = new SqlConnection("ConnectionStringExemplo"))
using (var comando = new SqlCommand("SELECT Nome FROM Tabela", conexao))
{
conexao.Open();
using (var leitor = comando.ExecuteReader())
{
while (leitor.Read())
{
registros.Add(leitor["Nome"].ToString());
}
}
}
return registros;
}
Múltiplas declarações using podem ser encadeadas para maior concisão.
9. Classes Estáticas
Classes estáticas são úteis para agrupar métodos e propriedades utilitárias que não dependem do estado de um objeto e não precisam ser instanciadas. Marcar uma classe como static garante que ela só possa conter membros estáticos e não possa ser instanciada, o que melhora a segurança e a intenção do design. Considere uma classe utilitária para formatação de texto:
public class FormatadorTexto
{
// Construtor privado impede instanciação
private FormatadorTexto() { }
public static string InverterString(string texto)
{
char[] array = texto.ToCharArray();
Array.Reverse(array);
return new string(array);
}
}
Ao invés de um construtor privado, uma classe estática impõe essas regras em tempo de compilação:
public static class FormatadorTextoEstatico
{
public static string InverterString(string texto)
{
char[] array = texto.ToCharArray();
Array.Reverse(array);
return new string(array);
}
// Não pode ter membros não estáticos, como:
// public string AlgumMembroNaoEstatico { get; set; }
}
O uso de membros estáticos é direto, sem a necessidade de criar um objeto:
string textoOriginal = "Olá Mundo";
string textoInvertido = FormatadorTextoEstatico.InverterString(textoOriginal);
Console.WriteLine(textoInvertido); // Saída: odnuM álO
10. Inicializadores de Objeto e Coleção
Os inicializadores de objeto e coleção, introduzidos no C# 3.0, permitem atribuir valores a propriedades ou campos públicos de um objeto e adicionar elementos a uma coleção diretamente no momento da criação. Isso reduz a verbosidade e melhora a legibilidade.
Considere uma estrutura simples:
public struct Ponto
{
public int X { get; set; }
public int Y { get; set; }
}
Em vez de inicializar o objeto e suas propriedades em várias linhas:
var pontoInicial = new Ponto();
pontoInicial.X = 10;
pontoInicial.Y = 25;
Use um inicializador de objeto:
var pontoInicial = new Ponto { X = 10, Y = 25 };
Para coleções, o benefício é similar. Adicionar elementos a uma List<int>:
var listaNumeros = new List<int>();
listaNumeros.Add(5);
listaNumeros.Add(15);
listaNumeros.Add(25);
Com um inicializador de coleção:
var listaNumeros = new List<int> { 5, 15, 25 };
Ambos podem ser combinados para inicializar coleções de objetos complexos:
var listaDePontos = new List<Ponto>
{
new Ponto { X = 1, Y = 2 },
new Ponto { X = 3, Y = 4 },
new Ponto { X = 5, Y = 6 }
};
Essa sintaxe não apenas economiza linhas de código, mas também pode, em alguns cenários, impactar positivamente o desempenho da inicialização de tipos estáticos, por exemplo, ao evitar o construtor estático (.cctor) em favor de inicializadores de campo estáticos (marcados com beforefieldinit no IL).
11. Tipagem Implícita com var
A palavra-chave var permite declarar uma variável sem especificar seu tipo explicitamente. O compilador infere o tipo em tempo de compilação. É crucial entender que var não torna a variável dinamicamente tipada; ela ainda é fortemente tipada, mas a declaração é mais concisa. Por exemplo:
// Declaração explícita
MinhaClasseDeDados objDados = new MinhaClasseDeDados();
// Declaração implícita com var
var objDados2 = new MinhaClasseDeDados(); // objDados2 é fortemente tipado como MinhaClasseDeDados
var é particularmente útil quando o tipo é longo, complexo ou óbvio pelo lado direito da atribuição, como em coleções genéricas aninhadas ou expressões LINQ:
// Tipo explícito: verboso
Dictionary<string, List<Produto>> produtosPorCategoria = new Dictionary<string, List<Produto>>();
// Tipo implícito: mais limpo
var produtosPorCategoria2 = new Dictionary<string, List<Produto>>();
Em consultas LINQ, onde os tipos anônimos são comuns, var é frequentemente indispensável:
// Resultado de LINQ com tipo anônimo
var resultadosConsulta = from p in produtos
where p.Preco > 50
select new { Nome = p.Nome, PrecoComDesconto = p.Preco * 0.9 };
// Sem var, teríamos que usar o tipo explícito (se não for anônimo)
// IEnumerable<IGrouping<string, Pedido>> resultadosAgrupados = ...;
12. Métodos de Extensão LINQ
LINQ (Language Integrated Query) oferece uma poderosa sintaxe para consultar e manipular dados. Em vez de escrever loops foreach personalizados e lógica complexa para filtragem, ordenação ou agrupamento, os métodos de extensão LINQ fornecem um conjunto robusto e testado de operações. Esses métodos são altamente otimizados e evitam a introdução de bugs comuns de lógica de coleção.
Considere uma lista de objetos ItemEstoque com propriedades como Categoria e Quantidade. Para encontrar itens com quantidade maior que 10 e agrupá-los por categoria, um código manual poderia ser:
public class ItemEstoque { public string Categoria { get; set; } public int Quantidade { get; set; } }
var itensFiltradosAgrupados = new Dictionary<string, List<ItemEstoque>>();
foreach (var item in listaDeItens)
{
if (item.Quantidade > 10)
{
if (!itensFiltradosAgrupados.ContainsKey(item.Categoria))
{
itensFiltradosAgrupados[item.Categoria] = new List<ItemEstoque>();
}
itensFiltradosAgrupados[item.Categoria].Add(item);
}
}
Com os métodos de extensão LINQ, o mesmo resultado é obtido de forma muito mais concisa e legível:
var itensFiltradosAgrupadosLinq = listaDeItens
.Where(item => item.Quantidade > 10)
.GroupBy(item => item.Categoria);
Ou usando a sintaxe de consulta LINQ:
var itensFiltradosAgrupadosConsulta = from item in listaDeItens
where item.Quantidade > 10
group item by item.Categoria;
13. Métodos de Extensão
Os métodos de extensão permitem "adicionar" novos métodos a tipos existentes sem modificar seu código-fonte ou criar uma nova classe derivada. Eles são métodos estáticos definidos em uma classe estática, mas são chamados como se fossem métodos de instância no tipo estendido. O primeiro parâmetro do método de extensão é precedido pela palavra-chave this para indicar o tipo que está sendo estendido.
Exemplo de um método de extensão para verificar se um número é par:
public static class ExtensoesNumericas
{
public static bool EhPar(this int numero)
{
return numero % 2 == 0;
}
}
A utilização é simples e intuitiva:
int meuNumero = 7;
if (meuNumero.EhPar()) // Chama o método de extensão como se fosse um método de instância
{
Console.WriteLine("É par!");
}
else
{
Console.WriteLine("É ímpar!");
}
// Também pode ser chamado como método estático (menos comum)
bool isEven = ExtensoesNumericas.EhPar(10);
Embora poderosos, o uso excessivo de métodos de extensão pode levar à poluição do Intellisense e tornar o código menos claro, pois não é imediatamente óbvio de onde o método vem. Use-os com moderação para funcionalidades que realmente estendam um tipo de forma natural e útil.
14. Manipulação de Caminhos com System.IO.Path
Manipular caminhos de arquivo e diretório manualmente, concatenando strings, é uma fonte comum de erros (por exemplo, barras invertidas ausentes ou duplicadas). A classe estática System.IO.Path fornece métodos robustos para lidar com essas operações de forma segura e independente do sistema operacional.
Para combinar partes de um caminho:
string diretorioBase = "C:\\Aplicacao\\Dados";
string nomeArquivo = "relatorio.txt";
// Evita problemas com barras invertidas
string caminhoCompleto = Path.Combine(diretorioBase, nomeArquivo);
Console.WriteLine(caminhoCompleto); // Saída: C:\Aplicacao\Dados\relatorio.txt
Para extrair componentes de um caminho:
string caminho = "C:\\Usuario\\Documentos\\projeto\\arquivo.doc";
string raiz = Path.GetPathRoot(caminho); // C:\
string nomeDoArquivo = Path.GetFileName(caminho); // arquivo.doc
string extensao = Path.GetExtension(caminho); // .doc
string nomeSemExtensao = Path.GetFileNameWithoutExtension(caminho); // arquivo
string diretorio = Path.GetDirectoryName(caminho); // C:\Usuario\Documentos\projeto
Sempre que precisar manipular caminhos de arquivo ou diretório, prefira os métodos de System.IO.Path.
15. Delegates Genéricos (Action, Func, Predicate)
Delegates em C# permitem passar métodos como parâmetros, funcionando como ponteiros de função em outras linguagens. Eles são extremamente versáteis para a criação de código reutilizável e flexível, especialmente em cenários como callbacks, eventos e estratégias personalizadas.
O .NET oferece delegates genéricos predefinidos que cobrem os casos de uso mais comuns, evitando a necessidade de declarar um novo tipo de delegate para cada cenário:
Action<T>: Representa um método que não retorna um valor (void) e pode aceitar de 0 a 16 parâmetros genéricos.Func<TResult>: Representa um método que retorna um valor e pode aceitar de 0 a 16 parâmetros genéricos (o último tipo genérico é o tipo de retorno).Predicate<T>: Representa um método que define um conjunto de critérios e determina se o objeto especificado os atende. Ele aceita um parâmetro genérico e retorna umbool.
Considere uma classe de cache genérica que precisa de uma estratégia para determinar se um item expirou. Em vez de usar herança para diferentes políticas de expiração, um Predicate pode ser usado para injetar a lógica de expiração:
using System.Collections.Concurrent;
public sealed class ItemCache<T>
{
public DateTime CriadoEm { get; set; }
public DateTime UltimoAcesso { get; set; }
public T Valor { get; set; }
}
public sealed class MeuCache<T>
{
private ConcurrentDictionary<string, ItemCache<T>> _itens;
private Predicate<ItemCache<T>> _estrategiaExpiracao;
public MeuCache(Predicate<ItemCache<T>> estrategiaExpiracao)
{
_itens = new ConcurrentDictionary<string, ItemCache<T>>();
_estrategiaExpiracao = estrategiaExpiracao;
}
public void Adicionar(string chave, T valor)
{
_itens[chave] = new ItemCache<T> { Valor = valor, CriadoEm = DateTime.Now, UltimoAcesso = DateTime.Now };
}
// Método para verificar e remover itens expirados
public void LimparItensExpirados()
{
foreach (var par in _itens)
{
if (_estrategiaExpiracao(par.Value)) // Invoca o delegate com a estratégia
{
_itens.TryRemove(par.Key, out _);
}
}
}
}
A criação e uso do cache com uma estratégia de expiração flexível (aqui, expirando após 30 segundos de inatividade) é concisa usando uma expressão lambda:
var cacheDeDados = new MeuCache<int>(item =>
DateTime.Now - item.UltimoAcesso > TimeSpan.FromSeconds(30));
cacheDeDados.Adicionar("chave1", 123);
// ... após algum tempo
cacheDeDados.LimparItensExpirados();
Delegates genéricos e expressões lambda promovem a reutilização de código e a flexibilidade sem a complexidade da hierarquia de classes.