7 erros perigosos que são fáceis de cometer em C # / .NET

Uma tradução do artigo foi preparada antes do início do curso "C # ASP.NET Core Developer" .




C # é uma ótima linguagem , e o .NET Framework também é muito bom. A digitação forte em C # ajuda a reduzir o número de erros que você pode provocar, em comparação com outros idiomas. Além disso, seu design intuitivo geral também ajuda muito, comparado a algo como JavaScript (onde true é falso ). No entanto, cada idioma tem seu próprio rake que é fácil de seguir, juntamente com idéias erradas sobre o comportamento esperado do idioma e da infraestrutura. Vou tentar descrever alguns desses erros em detalhes.

1. Não entendo a execução atrasada (preguiçosa)


Acredito que desenvolvedores experientes conhecem esse mecanismo .NET, mas pode surpreender colegas com menos conhecimento. Em poucas palavras, os métodos / operadores que retornam IEnumerable<T>e usam yieldpara retornar cada resultado não são executados na linha de código que realmente os chama - eles são executados quando a coleção resultante é acessada de alguma maneira *. Observe que a maioria das expressões LINQ acaba retornando seus resultados com rendimento .

Como exemplo, considere o teste de unidade flagrante abaixo.

[TestMethod]
[ExpectedException(typeof(ArgumentNullException))]
public void Ensure_Null_Exception_Is_Thrown()
{
   var result = RepeatString5Times(null);
}
[TestMethod]
[ExpectedException(typeof(InvalidOperationException))]
public void Ensure_Invalid_Operation_Exception_Is_Thrown()
{
   var result = RepeatString5Times("test");
   var firstItem = result.First();
}
private IEnumerable<string> RepeatString5Times(string toRepeat)
{
   if (toRepeat == null)
       throw new ArgumentNullException(nameof(toRepeat));
   for (int i = 0; i < 5; i++)
   {   
       if (i == 3)
            throw new InvalidOperationException("3 is a horrible number");
       yield return $"{toRepeat} - {i}";
   }
}

Ambos os testes falharão. O primeiro teste falhará, porque o resultado não é usado em nenhum lugar, portanto o corpo do método nunca será executado. O segundo teste falhará por outro motivo um pouco mais trivial. Agora, obtemos o primeiro resultado de chamar nosso método para garantir que o método realmente seja executado. No entanto, o mecanismo de execução atrasada sairá do método assim que puder - nesse caso, usamos apenas o primeiro elemento; portanto, assim que passamos a primeira iteração, o método interrompe sua execução (portanto, i == 3 nunca será verdadeiro).

A execução atrasada é realmente um mecanismo interessante, especialmente porque facilita o encadeamento de consultas LINQ, recuperando dados apenas quando sua consulta está pronta para uso.

2. , Dictionary ,


Isso é especialmente desagradável e tenho certeza de que em algum lugar tenho código que se baseia nessa suposição. Quando você adiciona itens à lista List<T>, eles são armazenados na mesma ordem em que você os adiciona - logicamente. Às vezes, você precisa ter outro objeto associado a um item na lista, e a solução óbvia é usar um dicionário Dictionary<TKey,TValue>que permita especificar um valor relacionado para a chave.

Em seguida, você pode percorrer o dicionário usando o foreach e, na maioria dos casos, ele se comportará conforme o esperado - você acessará os elementos na mesma ordem em que foram adicionados ao dicionário. No entanto, esse comportamento é indefinido - ou seja, é uma feliz coincidência, não algo em que você pode confiar e sempre esperar. Isso é explicado emDocumentação da Microsoft , mas acho que poucas pessoas estudaram esta página com cuidado.

Para ilustrar isso, no exemplo abaixo, a saída será a seguinte:

terceiro
segundo


var dict = new Dictionary<string, object>();       
dict.Add("first", new object());
dict.Add("second", new object());
dict.Remove("first");
dict.Add("third", new object());
foreach (var entry in dict)
{
    Console.WriteLine(entry.Key);
}

Não acredite em mim? Verifique aqui online você mesmo .

3. Não leve em consideração a segurança do fluxo


O multithreading é ótimo; se implementado corretamente, você pode melhorar significativamente o desempenho do seu aplicativo. No entanto, assim que você inserir multithreading, você deve ter muito, muito cuidado com os objetos que modificará, porque poderá começar a encontrar erros aparentemente aleatórios se não for cuidadoso o suficiente.

Simplificando, muitas classes base na biblioteca .NET não são seguras para threads.- Isso significa que a Microsoft não garante que você possa usar essa classe em paralelo usando vários threads. Isso não seria um grande problema se você pudesse encontrar imediatamente problemas associados a isso, mas a natureza do multithreading implica que quaisquer problemas que surjam são muito instáveis ​​e imprevisíveis - provavelmente, não há duas execuções produzindo o mesmo resultado.

Por exemplo, considere este bloco de código que usa simples, mas não é seguro para threads List<T>.

var items = new List<int>();
var tasks = new List<Task>();
for (int i = 0; i < 5; i++)
{
   tasks.Add(Task.Run(() => {
       for (int k = 0; k < 10000; k++)
       {
           items.Add(i);
       }
   }));
}
Task.WaitAll(tasks.ToArray());
Console.WriteLine(items.Count);

Assim, adicionamos números de 0 a 4 à lista 10.000 vezes cada, o que significa que a lista deve conter 50.000 elementos. Eu devo? Bem, há uma pequena chance de que, no final, seja - mas abaixo estão os resultados de 5 dos meus diferentes lançamentos:

28191
23536
44346
40007
40476

Você pode verificar on-line aqui .

De fato, isso ocorre porque o método Add não é atômico, o que implica que o encadeamento pode interromper o método, o que pode redimensionar a matriz enquanto outro encadeamento está em processo de adição ou adicionar um elemento com o mesmo índice como o outro segmento. A exceção IndexOutOfRange veio a mim algumas vezes, provavelmente porque o tamanho da matriz mudou durante a adição a ela. Então, o que fazemos aqui? Podemos usar a palavra-chave lock para garantir que apenas um encadeamento possa adicionar um item (Adicionar) à lista de uma vez, mas isso pode afetar significativamente o desempenho. A Microsoft, sendo pessoas legais, fornece ótimas coleções queEles são seguros para threads e altamente otimizados em termos de desempenho. Já publiquei um artigo descrevendo como você pode usá-los .

4. Abuse de carregamento lento (adiado) no LINQ


O carregamento lento é um ótimo recurso para o LINQ to SQL e o LINQ to Entities (Entity Framework), que permite carregar linhas de tabela relacionadas conforme necessário. Em um dos meus outros projetos, tenho uma tabela "Módulos" e uma tabela "Resultados" com um relacionamento um para muitos (um módulo pode ter muitos resultados).



Quando quero obter um módulo específico, certamente não quero que o Entity Framework retorne todos os resultados que a tabela Módulos possui! Portanto, ele é inteligente o suficiente para executar uma consulta para obter resultados somente quando eu precisar. Assim, o código abaixo executará 2 consultas - uma para obter o módulo e a outra para obter os resultados (para cada módulo),

using (var db = new DBEntities())
{
   var modules = db.Modules;
   foreach (var module in modules)
   {
       var moduleType = module.Results;
      //   
   }
}

No entanto, e se eu tiver centenas de módulos? Isso significa que uma consulta SQL separada para recebimento de registros de resultados será executada para cada módulo! Obviamente, isso sobrecarregará o servidor e diminuirá significativamente seu aplicativo. No Entity Framework, a resposta é muito simples - você pode especificar que inclua um conjunto específico de resultados em sua consulta. Veja o código modificado abaixo, onde apenas uma consulta SQL será executada, que incluirá cada módulo e cada resultado desse módulo (combinados em uma consulta, que o Entity Framework exibe de maneira inteligente em seu modelo),

using (var db = new DBEntities())
{
   var modules = db.Modules.Include(b => b.Results);
   foreach (var module in modules)
   {
       var moduleType = module.Results;
      //   
   }
}

5. Não entenda como o LINQ to SQL / Entity Frameworks traduz consultas


Desde que abordamos o tópico LINQ, acho que vale a pena mencionar a diferença com que seu código será executado se estiver dentro de uma consulta LINQ. Explicando em alto nível, todo o seu código dentro de uma consulta LINQ é traduzido para SQL usando expressões - isso parece óbvio, mas é muito, muito fácil esquecer o contexto em que você está e, finalmente, introduzir problemas em sua base de código. Abaixo, compilei uma lista para descrever alguns obstáculos típicos que você pode encontrar.

A maioria das chamadas de método não funcionará.

Então, imagine que você tenha a consulta abaixo para separar o nome de todos os módulos com dois pontos e capturar a segunda parte.

var modules = from m in db.Modules
              select m.Name.Split(':')[1];

Você receberá uma exceção na maioria dos provedores LINQ - não há conversão SQL para o método Split, alguns métodos podem ser suportados, por exemplo, adicionar dias a uma data, mas tudo depende do seu provedor.

Aqueles que trabalham podem produzir resultados inesperados ...

Pegue a expressão LINQ abaixo (não tenho idéia do por que você faria isso na prática, mas imagine que essa seja uma solicitação razoável).

int modules = db.Modules.Sum(a => a.ID);

Se você tiver alguma linha na tabela de módulos, ela fornecerá a soma dos identificadores. Parece certo! Mas e se você executá-lo usando o LINQ to Objects? Podemos fazer isso convertendo a coleção de módulos em uma lista antes de executar nosso método Sum.

int modules = db.Modules.ToList().Sum(a => a.ID);

Choque, horror - fará exatamente o mesmo! No entanto, e se você não tivesse linhas na tabela de módulos? O LINQ to Objects retorna 0 e a versão do Entity Framework / LINQ to SQL lança uma InvalidOperationException , que diz que não pode converter "int?" em "int" ... tal. Isso ocorre porque quando você executa SUM no SQL para um conjunto vazio, NULL é retornado em vez de 0 - portanto, tenta retornar um int nulo. Aqui estão algumas dicas para corrigir isso se você encontrar esse problema .

Saiba quando você só precisa usar o bom e velho SQL.

Se você estiver executando uma solicitação extremamente complexa, sua solicitação traduzida pode acabar parecendo algo cuspido, comido várias vezes. Infelizmente, não tenho exemplos para demonstrar, mas, a julgar pela opinião predominante, gosto muito de usar visualizações aninhadas, o que torna a manutenção de código um pesadelo.

Além disso, se você encontrar algum gargalo de desempenho, será difícil corrigi-lo porque você não tem controle direto sobre o SQL gerado. Faça isso no SQL ou delegá-lo ao administrador do banco de dados, se você ou sua empresa tiver um!

6. Arredondamento errado


Agora, sobre algo um pouco mais simples do que os parágrafos anteriores, mas sempre me esqueci e acabei com erros desagradáveis ​​(e, se estiver relacionado às finanças, um diretor zangado de barbatanas / genes).

O .NET Framework inclui um excelente método estático na classe Math chamada Round , que pega um valor numérico e o arredonda para a casa decimal especificada. Funciona perfeitamente na maioria das vezes, mas o que fazer quando você tenta arredondar 2,25 para a primeira casa decimal? Suponho que você provavelmente espere arredondar para 2,3 - é com isso que estamos acostumados, certo? Bem, na prática, acontece que o .NET usa arredondamentos bancáriosque arredonda o exemplo dado para 2.2! Isso se deve ao fato de os banqueiros serem arredondados para o número par mais próximo se o número estiver no "ponto médio". Felizmente, isso pode ser facilmente substituído no método Math.Round.

Math.Round(2.25,1, MidpointRounding.AwayFromZero)

7. Classe horrível 'DBNull'


Isso pode causar lembranças desagradáveis ​​para alguns - o ORM esconde essa sujeira de nós, mas se você mergulhar no mundo do ADO.NET (SqlDataReader e similares), você encontrará o DBNull.Value.

Não tenho 100% de certeza da razão pela qualOs valores NULL do banco de dados são processados ​​da seguinte maneira (comente abaixo se você souber!), Mas a Microsoft decidiu apresentá-los com um tipo especial DBNull (com um campo estático Value). Eu posso dar uma das vantagens disso - você não receberá NullReferenceException desagradável ao acessar um campo de banco de dados que é NULL. No entanto, você não deve apenas dar suporte à maneira secundária de verificar valores NULL (o que é fácil de esquecer, o que pode levar a erros graves), mas você perde qualquer um dos ótimos recursos do C # que o ajudam a trabalhar com nulos. O que poderia ser tão simples quanto

reader.GetString(0) ?? "NULL";

o que eventualmente se torna ...

reader.GetString(0) != DBNull.Value ? reader.GetString(0) : "NULL";

Ugh.

Nota


Estes são apenas alguns dos "truques" não triviais que encontrei no .NET - se você souber mais, gostaria de ouvir você abaixo.



ASP.NET Core:



All Articles