Código C# com erros: os 10 erros mais comuns na programação C#
Publicados: 2022-03-11Sobre C Afiado
C# é uma das várias linguagens que visam o Microsoft Common Language Runtime (CLR). As linguagens que visam o CLR se beneficiam de recursos como integração entre linguagens e tratamento de exceções, segurança aprimorada, um modelo simplificado para interação de componentes e serviços de depuração e criação de perfil. Das linguagens CLR atuais, o C# é o mais usado para projetos de desenvolvimento profissionais complexos voltados para ambientes de desktop, dispositivos móveis ou servidores do Windows.
C# é uma linguagem orientada a objetos e fortemente tipada. A verificação de tipo estrita em C#, tanto em tempo de compilação quanto em tempo de execução, resulta na maioria dos erros típicos de programação em C# sendo relatados o mais cedo possível e seus locais identificados com bastante precisão. Isso pode economizar muito tempo na programação C Sharp, em comparação com rastrear a causa de erros intrigantes que podem ocorrer muito tempo depois que a operação ofensiva ocorre em linguagens que são mais liberais com a aplicação de segurança de tipo. No entanto, muitos codificadores de C# involuntariamente (ou descuidadamente) descartam os benefícios dessa detecção, o que leva a alguns dos problemas discutidos neste tutorial de C#.
Sobre este tutorial de programação em C Sharp
Este tutorial descreve 10 dos erros de programação C# mais comuns cometidos, ou problemas a serem evitados, por programadores C# e fornece ajuda.
Embora a maioria dos erros discutidos neste artigo sejam específicos do C#, alguns também são relevantes para outras linguagens que visam o CLR ou fazem uso da Framework Class Library (FCL).
Erro comum de programação C# nº 1: usando uma referência como um valor ou vice-versa
Os programadores de C++ e muitas outras linguagens estão acostumados a controlar se os valores que eles atribuem às variáveis são simplesmente valores ou referências a objetos existentes. Na programação em C Sharp, no entanto, essa decisão é tomada pelo programador que escreveu o objeto, não pelo programador que instancia o objeto e o atribui a uma variável. Essa é uma “pegadinha” comum para quem está tentando aprender programação C#.
Se você não sabe se o objeto que está usando é um tipo de valor ou um tipo de referência, você pode se deparar com algumas surpresas. Por exemplo:
Point point1 = new Point(20, 30); Point point2 = point1; point2.X = 50; Console.WriteLine(point1.X); // 20 (does this surprise you?) Console.WriteLine(point2.X); // 50 Pen pen1 = new Pen(Color.Black); Pen pen2 = pen1; pen2.Color = Color.Blue; Console.WriteLine(pen1.Color); // Blue (or does this surprise you?) Console.WriteLine(pen2.Color); // Blue
Como você pode ver, os objetos Point
e Pen
foram criados exatamente da mesma maneira, mas o valor de point1
permaneceu inalterado quando um novo valor de coordenada X
foi atribuído a point2
, enquanto o valor de pen1
foi modificado quando uma nova cor foi atribuída a pen2
. Podemos, portanto, deduzir que point1
e point2
contêm sua própria cópia de um objeto Point
, enquanto pen1
e pen2
contêm referências ao mesmo objeto Pen
. Mas como podemos saber isso sem fazer esse experimento?
A resposta é examinar as definições dos tipos de objeto (o que você pode fazer facilmente no Visual Studio colocando o cursor sobre o nome do tipo de objeto e pressionando F12):
public struct Point { ... } // defines a “value” type public class Pen { ... } // defines a “reference” type
Conforme mostrado acima, na programação C#, a palavra-chave struct
é usada para definir um tipo de valor, enquanto a palavra-chave class
é usada para definir um tipo de referência. Para aqueles com experiência em C++, que foram enganados por uma falsa sensação de segurança pelas muitas semelhanças entre as palavras-chave C++ e C#, esse comportamento provavelmente é uma surpresa que pode fazer com que você peça ajuda de um tutorial C#.
Se você vai depender de algum comportamento que difere entre os tipos de valor e referência - como a capacidade de passar um objeto como um parâmetro de método e fazer com que esse método altere o estado do objeto - certifique-se de estar lidando com o tipo correto de objeto para evitar problemas de programação C#.
Erro comum de programação C# nº 2: valores padrão mal-entendidos para variáveis não inicializadas
Em C#, os tipos de valor não podem ser nulos. Por definição, os tipos de valor têm um valor, e mesmo as variáveis não inicializadas de tipos de valor devem ter um valor. Isso é chamado de valor padrão para esse tipo. Isso leva ao seguinte resultado, geralmente inesperado, ao verificar se uma variável não foi inicializada:
class Program { static Point point1; static Pen pen1; static void Main(string[] args) { Console.WriteLine(pen1 == null); // True Console.WriteLine(point1 == null); // False (huh?) } }
Por que point1
não é nulo? A resposta é que Point
é um tipo de valor e o valor padrão para um Point
é (0,0), não nulo. A falha em reconhecer isso é um erro muito fácil (e comum) de se cometer em C#.
Muitos (mas não todos) tipos de valor têm uma propriedade IsEmpty
que você pode verificar para ver se é igual ao seu valor padrão:
Console.WriteLine(point1.IsEmpty); // True
Quando você estiver verificando se uma variável foi inicializada ou não, certifique-se de saber qual valor uma variável não inicializada desse tipo terá por padrão e não confie que ela seja nula.
Erro comum de programação C# nº 3: usando métodos de comparação de string impróprios ou não especificados
Há muitas maneiras diferentes de comparar strings em C#.
Embora muitos programadores usem o operador ==
para comparação de strings, na verdade é um dos métodos menos desejáveis a serem empregados, principalmente porque não especifica explicitamente no código qual tipo de comparação é desejado.
Em vez disso, a maneira preferida de testar a igualdade de strings na programação C# é com o método Equals
:
public bool Equals(string value); public bool Equals(string value, StringComparison comparisonType);
A primeira assinatura de método (ou seja, sem o parâmetro comparisonType
), é na verdade o mesmo que usar o operador ==
, mas tem o benefício de ser aplicado explicitamente a strings. Ele realiza uma comparação ordinal das strings, que é basicamente uma comparação byte a byte. Em muitos casos, este é exatamente o tipo de comparação que você deseja, especialmente ao comparar strings cujos valores são definidos programaticamente, como nomes de arquivos, variáveis de ambiente, atributos, etc. Nesses casos, desde que uma comparação ordinal seja realmente o tipo correto de comparação para essa situação, a única desvantagem de usar o método Equals
sem um tipo de comparisonType
é que alguém lendo o código pode não saber que tipo de comparação você está fazendo.
Usar a assinatura do método Equals
que inclui um tipo de comparisonType
toda vez que você compara strings, no entanto, não apenas tornará seu código mais claro, mas também fará com que você pense explicitamente sobre qual tipo de comparação você precisa fazer. Isso vale a pena fazer, porque mesmo que o inglês não forneça muitas diferenças entre comparações ordinais e culturais, outros idiomas fornecem muito, e ignorar a possibilidade de outros idiomas está se abrindo para um grande potencial de erros no caminho. Por exemplo:
string s = "strasse"; // outputs False: Console.WriteLine(s == "straße"); Console.WriteLine(s.Equals("straße")); Console.WriteLine(s.Equals("straße", StringComparison.Ordinal)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("straße", StringComparison.OrdinalIgnoreCase)); // outputs True: Console.WriteLine(s.Equals("straße", StringComparison.CurrentCulture)); Console.WriteLine(s.Equals("Straße", StringComparison.CurrentCultureIgnoreCase));
A prática mais segura é sempre fornecer um parâmetro de comparisonType
para o método Equals
. Aqui estão algumas orientações básicas:
- Ao comparar cadeias de caracteres que foram inseridas pelo usuário ou devem ser exibidas para o usuário, use uma comparação sensível à cultura (
CurrentCulture
ouCurrentCultureIgnoreCase
). - Ao comparar strings programáticas, use comparação ordinal (
Ordinal
ouOrdinalIgnoreCase
). -
InvariantCulture
eInvariantCultureIgnoreCase
geralmente não devem ser usados, exceto em circunstâncias muito limitadas, porque as comparações ordinais são mais eficientes. Se for necessária uma comparação com reconhecimento de cultura, ela geralmente deve ser realizada em relação à cultura atual ou outra cultura específica.
Além do método Equals
, as strings também fornecem o método Compare
, que fornece informações sobre a ordem relativa das strings em vez de apenas um teste de igualdade. Esse método é preferível aos operadores <
, <=
, >
e >=
, pelos mesmos motivos discutidos acima – para evitar problemas em C#.
Erro comum de programação C# nº 4: usando instruções iterativas (em vez de declarativas) para manipular coleções
No C# 3.0, a adição da consulta integrada à linguagem (LINQ) à linguagem mudou para sempre a maneira como as coleções são consultadas e manipuladas. Desde então, se você estiver usando instruções iterativas para manipular coleções, você não usou LINQ quando provavelmente deveria.
Alguns programadores de C# nem sabem da existência do LINQ, mas felizmente esse número está se tornando cada vez menor. Muitos ainda pensam, porém, que devido à semelhança entre palavras-chave LINQ e instruções SQL, seu único uso é em código que consulta bancos de dados.
Embora a consulta de banco de dados seja um uso muito comum de instruções LINQ, elas realmente funcionam em qualquer coleção enumerável (ou seja, qualquer objeto que implemente a interface IEnumerable). Então, por exemplo, se você tivesse uma matriz de contas, em vez de escrever uma lista C# foreach:
decimal total = 0; foreach (Account account in myAccounts) { if (account.Status == "active") { total += account.Balance; } }
você poderia apenas escrever:
decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();
Embora este seja um exemplo bastante simples de como evitar esse problema comum de programação C#, há casos em que uma única instrução LINQ pode substituir facilmente dezenas de instruções em um loop iterativo (ou loops aninhados) em seu código. E menos código geral significa menos oportunidades para a introdução de bugs. Tenha em mente, no entanto, que pode haver um trade-off em termos de desempenho. Em cenários de desempenho crítico, especialmente onde seu código iterativo é capaz de fazer suposições sobre sua coleção que o LINQ não pode, certifique-se de fazer uma comparação de desempenho entre os dois métodos.
Erro comum de programação C# nº 5: não considerar os objetos subjacentes em uma instrução LINQ
O LINQ é ótimo para abstrair a tarefa de manipular coleções, sejam eles objetos na memória, tabelas de banco de dados ou documentos XML. Em um mundo perfeito, você não precisaria saber quais são os objetos subjacentes. Mas o erro aqui é supor que vivemos em um mundo perfeito. Na verdade, instruções LINQ idênticas podem retornar resultados diferentes quando executadas exatamente nos mesmos dados, se esses dados estiverem em um formato diferente.
Por exemplo, considere a seguinte afirmação:
decimal total = (from account in myAccounts where account.Status == "active" select account.Balance).Sum();
O que acontece se uma conta do account.Status
for igual a "Ativo" (observe o A maiúsculo)? Bem, se myAccounts
fosse um objeto DbSet
(que foi configurado com a configuração padrão que não diferencia maiúsculas de minúsculas), a expressão where
ainda corresponderia a esse elemento. No entanto, se myAccounts
estivesse em uma matriz na memória, ela não corresponderia e, portanto, produziria um resultado diferente para o total.
Mas espere um minuto. Quando falamos sobre comparação de strings anteriormente, vimos que o operador ==
realizava uma comparação ordinal de strings. Então, por que, neste caso, o operador ==
está realizando uma comparação que não diferencia maiúsculas de minúsculas?
A resposta é que quando os objetos subjacentes em uma instrução LINQ são referências a dados da tabela SQL (como é o caso do objeto Entity Framework DbSet neste exemplo), a instrução é convertida em uma instrução T-SQL. Os operadores seguem as regras de programação T-SQL, não as regras de programação C#, portanto, a comparação no caso acima acaba não diferenciando maiúsculas de minúsculas.
Em geral, embora o LINQ seja uma maneira útil e consistente de consultar coleções de objetos, na realidade você ainda precisa saber se sua instrução será ou não traduzida para algo diferente de C# nos bastidores para garantir que o comportamento do seu código seja ser como esperado em tempo de execução.
Erro comum de programação C# nº 6: ficar confuso ou enganado por métodos de extensão
Conforme mencionado anteriormente, as instruções LINQ funcionam em qualquer objeto que implemente IEnumerable. Por exemplo, a seguinte função simples somará os saldos de qualquer coleção de contas:
public decimal SumAccounts(IEnumerable<Account> myAccounts) { return myAccounts.Sum(a => a.Balance); }
No código acima, o tipo do parâmetro myAccounts é declarado como IEnumerable<Account>
. Como myAccounts
faz referência a um método Sum
(o C# usa a conhecida “notação de ponto” para fazer referência a um método em uma classe ou interface), esperamos ver um método chamado Sum()
na definição da IEnumerable<T>
. No entanto, a definição de IEnumerable<T>
, não faz referência a nenhum método Sum
e simplesmente se parece com isso:
public interface IEnumerable<out T> : IEnumerable { IEnumerator<T> GetEnumerator(); }
Então, onde o método Sum()
é definido? C# é fortemente tipado, portanto, se a referência ao método Sum
for inválida, o compilador C# certamente a sinalizará como um erro. Sabemos, portanto, que deve existir, mas onde? Além disso, onde estão as definições de todos os outros métodos que o LINQ fornece para consultar ou agregar essas coleções?
A resposta é que Sum()
não é um método definido na interface IEnumerable
. Em vez disso, é um método estático (chamado de “método de extensão”) que é definido na classe System.Linq.Enumerable
:
namespace System.Linq { public static class Enumerable { ... // the reference here to “this IEnumerable<TSource> source” is // the magic sauce that provides access to the extension method Sum public static decimal Sum<TSource>(this IEnumerable<TSource> source, Func<TSource, decimal> selector); ... } }
Então, o que torna um método de extensão diferente de qualquer outro método estático e o que nos permite acessá-lo em outras classes?
A característica distintiva de um método de extensão é o modificador this
em seu primeiro parâmetro. Esta é a “mágica” que o identifica para o compilador como um método de extensão. O tipo do parâmetro que ele modifica (neste caso IEnumerable<TSource>
) denota a classe ou interface que aparecerá para implementar esse método.
(Como um ponto lateral, não há nada mágico sobre a semelhança entre o nome da interface IEnumerable
e o nome da classe Enumerable
na qual o método de extensão é definido. Essa semelhança é apenas uma escolha estilística arbitrária.)
Com esse entendimento, também podemos ver que a função sumAccounts
que introduzimos acima poderia ter sido implementada da seguinte forma:

public decimal SumAccounts(IEnumerable<Account> myAccounts) { return Enumerable.Sum(myAccounts, a => a.Balance); }
O fato de podermos ter implementado dessa maneira levanta a questão de por que existem métodos de extensão? Os métodos de extensão são essencialmente uma conveniência da linguagem de programação C# que permite “adicionar” métodos a tipos existentes sem criar um novo tipo derivado, recompilar ou modificar o tipo original.
Os métodos de extensão são trazidos ao escopo incluindo um using [namespace];
declaração no topo do arquivo. Você precisa saber qual namespace C# inclui os métodos de extensão que está procurando, mas isso é muito fácil de determinar quando você sabe o que está procurando.
Quando o compilador C# encontra uma chamada de método em uma instância de um objeto e não encontra esse método definido na classe de objeto referenciada, ele examina todos os métodos de extensão que estão dentro do escopo para tentar encontrar um que corresponda ao método necessário assinatura e classe. Se encontrar um, ele passará a referência de instância como o primeiro argumento para esse método de extensão, então o restante dos argumentos, se houver, será passado como argumentos subsequentes para o método de extensão. (Se o compilador C# não encontrar nenhum método de extensão correspondente no escopo, ele gerará um erro.)
Os métodos de extensão são um exemplo de “açúcar sintático” por parte do compilador C#, que nos permite escrever código que é (geralmente) mais claro e mais sustentável. Mais claro, isto é, se você estiver ciente de seu uso. Caso contrário, pode ser um pouco confuso, especialmente no início.
Embora certamente haja vantagens em usar métodos de extensão, eles podem causar problemas e um pedido de ajuda de programação em C# para os desenvolvedores que não os conhecem ou não os entendem adequadamente. Isso é especialmente verdadeiro ao analisar amostras de código on-line ou qualquer outro código pré-escrito. Quando esse código produz erros de compilador (porque invoca métodos que claramente não estão definidos nas classes em que são invocados), a tendência é pensar que o código se aplica a uma versão diferente da biblioteca ou a uma biblioteca totalmente diferente. Muito tempo pode ser gasto procurando por uma nova versão, ou “biblioteca ausente” fantasma, que não existe.
Mesmo os desenvolvedores que estão familiarizados com métodos de extensão ainda são pegos ocasionalmente, quando há um método com o mesmo nome no objeto, mas sua assinatura de método difere sutilmente daquela do método de extensão. Muito tempo pode ser desperdiçado procurando por um erro de digitação ou erro que simplesmente não existe.
O uso de métodos de extensão em bibliotecas C# está se tornando cada vez mais prevalente. Além do LINQ, o Unity Application Block e a estrutura de API da Web são exemplos de duas bibliotecas modernas muito usadas pela Microsoft que também fazem uso de métodos de extensão, e há muitos outros. Quanto mais moderno o framework, mais provável é que ele incorpore métodos de extensão.
Claro, você também pode escrever seus próprios métodos de extensão. Perceba, no entanto, que enquanto os métodos de extensão parecem ser invocados como métodos de instância regulares, isso é realmente apenas uma ilusão. Em particular, seus métodos de extensão não podem fazer referência a membros privados ou protegidos da classe que estão estendendo e, portanto, não podem servir como um substituto completo para a herança de classe mais tradicional.
Erro comum de programação C# nº 7: usando o tipo errado de coleção para a tarefa em mãos
C# fornece uma grande variedade de objetos de coleção, sendo o seguinte apenas uma lista parcial:
Array
, ArrayList
, BitArray
, BitVector32
, Dictionary<K,V>
, HashTable
, HybridDictionary
, List<T>
, NameValueCollection
, OrderedDictionary
, Queue, Queue<T>
, SortedList
, Stack, Stack<T>
, StringCollection
, StringDictionary
.
Embora possa haver casos em que muitas opções sejam tão ruins quanto poucas opções, esse não é o caso de objetos de coleção. O número de opções disponíveis pode definitivamente funcionar a seu favor. Reserve um pouco mais de tempo para pesquisar e escolher o tipo de coleta ideal para sua finalidade. Isso provavelmente resultará em melhor desempenho e menos espaço para erros.
Se houver um tipo de coleção especificamente direcionado ao tipo de elemento que você possui (como string ou bit), incline-se a usar esse primeiro. A implementação geralmente é mais eficiente quando direcionada a um tipo específico de elemento.
Para aproveitar a segurança de tipo do C#, você geralmente deve preferir uma interface genérica em vez de uma não genérica. Os elementos de uma interface genérica são do tipo que você especifica quando declara seu objeto, enquanto os elementos de interfaces não genéricas são do tipo objeto. Ao usar uma interface não genérica, o compilador C# não pode verificar seu código. Além disso, ao lidar com coleções de tipos de valor primitivos, usar uma coleção não genérica resultará em repetidos boxing/unboxing desses tipos, o que pode resultar em um impacto negativo significativo no desempenho quando comparado a uma coleção genérica do tipo apropriado.
Outro problema comum do C# é escrever seu próprio objeto de coleção. Isso não quer dizer que nunca seja apropriado, mas com uma seleção tão abrangente quanto a que o .NET oferece, você provavelmente pode economizar muito tempo usando ou estendendo um que já existe, em vez de reinventar a roda. Em particular, a C5 Generic Collection Library para C# e CLI oferece uma ampla variedade de coleções adicionais “prontas para uso”, como estruturas de dados de árvore persistentes, filas de prioridade baseadas em heap, listas de matrizes indexadas por hash, listas vinculadas e muito mais.
Erro comum de programação C# nº 8: Negligenciar recursos gratuitos
O ambiente CLR emprega um coletor de lixo, portanto, você não precisa liberar explicitamente a memória criada para nenhum objeto. Na verdade, você não pode. Não há equivalente do operador delete
C++ ou da função free()
em C . Mas isso não significa que você pode simplesmente esquecer todos os objetos depois de terminar de usá-los. Muitos tipos de objetos encapsulam algum outro tipo de recurso do sistema (por exemplo, um arquivo de disco, conexão de banco de dados, soquete de rede, etc.). Deixar esses recursos abertos pode esgotar rapidamente o número total de recursos do sistema, degradando o desempenho e, por fim, levando a falhas no programa.
Embora um método destruidor possa ser definido em qualquer classe C#, o problema com os destruidores (também chamados de finalizadores em C#) é que você não pode saber com certeza quando eles serão chamados. Eles são chamados pelo coletor de lixo (em um thread separado, o que pode causar complicações adicionais) em um tempo indeterminado no futuro. Tentar contornar essas limitações forçando a coleta de lixo com GC.Collect()
não é uma prática recomendada do C#, pois isso bloqueará o encadeamento por um período desconhecido enquanto ele coleta todos os objetos elegíveis para coleta.
Isso não quer dizer que não haja bons usos para finalizadores, mas liberar recursos de forma determinística não é um deles. Em vez disso, quando você está operando em uma conexão de arquivo, rede ou banco de dados, você deseja liberar explicitamente o recurso subjacente assim que terminar com ele.
Vazamentos de recursos são uma preocupação em quase todos os ambientes. No entanto, o C# fornece um mecanismo robusto e simples de usar que, se utilizado, pode tornar os vazamentos uma ocorrência muito mais rara. A estrutura .NET define a interface IDisposable
, que consiste apenas no método Dispose()
. Qualquer objeto que implemente IDisposable
espera ter esse método chamado sempre que o consumidor do objeto terminar de manipulá-lo. Isso resulta em liberação explícita e determinista de recursos.
Se você estiver criando e descartando um objeto dentro do contexto de um único bloco de código, é basicamente indesculpável esquecer de chamar Dispose()
, porque o C# fornece uma instrução using
que garantirá que Dispose()
seja chamado, não importa como o bloco de código é encerrado (seja uma exceção, uma instrução de retorno ou simplesmente o fechamento do bloco). E sim, essa é a mesma instrução using
mencionada anteriormente que é usada para incluir namespaces C# na parte superior do arquivo. Ele tem um segundo propósito, completamente não relacionado, que muitos desenvolvedores C# desconhecem; ou seja, para garantir que Dispose()
seja chamado em um objeto quando o bloco de código for encerrado:
using (FileStream myFile = File.OpenRead("foo.txt")) { myFile.Read(buffer, 0, 100); }
Ao criar um bloco using
no exemplo acima, você sabe com certeza que myFile.Dispose()
será chamado assim que você terminar com o arquivo, quer Read()
lance ou não uma exceção.
Erro comum de programação C# nº 9: evitando exceções
C# continua sua aplicação de segurança de tipo em tempo de execução. Isso permite identificar muitos tipos de erros em C# muito mais rapidamente do que em linguagens como C++, onde conversões de tipo defeituosas podem resultar na atribuição de valores arbitrários aos campos de um objeto. No entanto, mais uma vez, os programadores podem desperdiçar esse ótimo recurso, levando a problemas de C#. Eles caem nessa armadilha porque o C# fornece duas maneiras diferentes de fazer as coisas, uma que pode lançar uma exceção e outra que não. Alguns vão evitar a rota de exceção, imaginando que não ter que escrever um bloco try/catch os salva de alguma codificação.
Por exemplo, aqui estão duas maneiras diferentes de executar uma conversão de tipo explícito em C#:
// METHOD 1: // Throws an exception if account can't be cast to SavingsAccount SavingsAccount savingsAccount = (SavingsAccount)account; // METHOD 2: // Does NOT throw an exception if account can't be cast to // SavingsAccount; will just set savingsAccount to null instead SavingsAccount savingsAccount = account as SavingsAccount;
O erro mais óbvio que poderia ocorrer com o uso do Método 2 seria uma falha na verificação do valor de retorno. Isso provavelmente resultaria em uma eventual NullReferenceException, que poderia surgir muito mais tarde, tornando muito mais difícil rastrear a origem do problema. Em contraste, o Método 1 teria lançado imediatamente uma InvalidCastException
, tornando a origem do problema muito mais óbvia imediatamente.
Além disso, mesmo se você se lembrar de verificar o valor de retorno no Método 2, o que você fará se achar que ele é nulo? O método que você está escrevendo é um local apropriado para relatar um erro? Existe algo mais que você pode tentar se esse elenco falhar? Caso contrário, lançar uma exceção é a coisa correta a fazer, então você pode deixar isso acontecer o mais próximo possível da origem do problema.
Aqui estão alguns exemplos de outros pares comuns de métodos em que um lança uma exceção e o outro não:
int.Parse(); // throws exception if argument can't be parsed int.TryParse(); // returns a bool to denote whether parse succeeded IEnumerable.First(); // throws exception if sequence is empty IEnumerable.FirstOrDefault(); // returns null/default value if sequence is empty
Alguns desenvolvedores de C# são tão “adversos a exceções” que assumem automaticamente que o método que não lança uma exceção é superior. Embora existam alguns casos selecionados em que isso pode ser verdade, não é nada correto como uma generalização.
Como um exemplo específico, em um caso em que você tem uma ação alternativa legítima (por exemplo, padrão) a ser tomada se uma exceção tivesse sido gerada, então essa abordagem de não exceção poderia ser uma escolha legítima. Nesse caso, pode ser melhor escrever algo assim:
if (int.TryParse(myString, out myInt)) { // use myInt } else { // use default value }
ao invés de:
try { myInt = int.Parse(myString); // use myInt } catch (FormatException) { // use default value }
No entanto, é incorreto supor que TryParse
é, portanto, necessariamente o método “melhor”. Às vezes é assim, às vezes não. É por isso que existem duas maneiras de fazê-lo. Use a correta para o contexto em que você se encontra, lembrando que as exceções certamente podem ser suas amigas como desenvolvedor.
Erro comum de programação C# nº 10: permitindo que os avisos do compilador se acumulem
Embora esse problema definitivamente não seja específico do C#, ele é particularmente notório na programação C#, pois abandona os benefícios da verificação de tipo estrita oferecida pelo compilador C#.
Os avisos são gerados por um motivo. Embora todos os erros do compilador C# signifiquem um defeito em seu código, muitos avisos também. O que diferencia os dois é que, no caso de um aviso, o compilador não tem problemas em emitir as instruções que seu código representa. Mesmo assim, ele acha seu código um pouco suspeito e há uma probabilidade razoável de que seu código não reflita com precisão sua intenção.
Um exemplo simples comum para este tutorial de programação em C# é quando você modifica seu algoritmo para eliminar o uso de uma variável que estava usando, mas esquece de remover a declaração da variável. O programa funcionará perfeitamente, mas o compilador sinalizará a declaração de variável inútil. O fato de o programa rodar perfeitamente faz com que os programadores negligenciem a correção da causa do aviso. Além disso, os codificadores aproveitam um recurso do Visual Studio que facilita a ocultação dos avisos na janela "Lista de erros" para que possam se concentrar apenas nos erros. Não demora muito até que haja dezenas de avisos, todos eles alegremente ignorados (ou pior ainda, escondidos).
Mas se você ignorar esse tipo de aviso, mais cedo ou mais tarde, algo assim pode muito bem chegar ao seu código:
class Account { int myId; int Id; // compiler warned you about this, but you didn't listen! // Constructor Account(int id) { this.myId = Id; // OOPS! } }
E na velocidade que o Intellisense nos permite escrever código, esse erro não é tão improvável quanto parece.
Agora você tem um erro grave em seu programa (embora o compilador tenha sinalizado apenas como um aviso, pelos motivos já explicados), e dependendo da complexidade do seu programa, você pode perder muito tempo rastreando este. Se você tivesse prestado atenção a esse aviso em primeiro lugar, teria evitado esse problema com uma simples correção de cinco segundos.
Lembre-se, o compilador C Sharp fornece muitas informações úteis sobre a robustez do seu código... se você estiver ouvindo. Não ignore os avisos. Eles geralmente levam apenas alguns segundos para serem corrigidos, e consertar novos quando eles acontecem pode economizar horas. Treine-se para esperar que a janela “Lista de Erros” do Visual Studio exiba “0 Erros, 0 Avisos”, para que quaisquer avisos o deixem desconfortável o suficiente para resolvê-los imediatamente.
Claro, existem exceções a todas as regras. Assim, pode haver momentos em que seu código parecerá um pouco suspeito para o compilador, mesmo que seja exatamente como você pretendia que fosse. Nesses casos muito raros, use #pragma warning disable [warning id]
apenas para o código que aciona o aviso e apenas para o ID de aviso que ele aciona. Isso suprimirá esse aviso, e apenas esse aviso, para que você ainda possa ficar alerta para novos.
Embrulhar
C# é uma linguagem poderosa e flexível com muitos mecanismos e paradigmas que podem melhorar muito a produtividade. Como acontece com qualquer ferramenta de software ou linguagem, porém, ter uma compreensão ou apreciação limitada de suas capacidades pode às vezes ser mais um impedimento do que um benefício, deixando a pessoa no estado proverbial de “saber o suficiente para ser perigoso”.
Usar um tutorial de C Sharp como este para se familiarizar com as principais nuances do C#, como (mas não limitado a) os problemas levantados neste artigo, ajudará na otimização do C#, evitando algumas de suas armadilhas mais comuns do Língua.
Leitura adicional no Blog da Toptal Engineering:
- Perguntas essenciais da entrevista em C#
- C# vs. C++: O que está no núcleo?