DEV Community

Angelo Belchior
Angelo Belchior

Posted on • Edited on

C#: Bug ou Feature?

Barata de terno e gravada saindo de dentro de um monitor de computador

Eu sempre estou estudando.

Dedico muito tempo para ir cada vez mais a fundo em temas que envolvem programação e para conhecer as entranhas das tecnologias que eu utilizo no meu dia a dia.

Geralmente eu crio cenários ou situações imaginárias e as tento resolver utilizando aquilo que eu aprendi durante essa longa jornada de mais de 20 anos colocando bugs sistemas em produção. Isso me força sempre a revisitar conceitos e olhar como outras tecnologias poderiam resolver essas mesmas situações.

Esse é o meio que eu utilizo para me manter relevante dentro da minha profissão: Aprendizado contínuo!

Para fixar aquilo que eu (supostamente) aprendi, eu tento, dentro da minha cabeça com meus amigos imaginários, explicar como eu resolvi determinado problema e como a tecnologia que eu usei me ajudou a fazer isso. Na maioria das vezes eu crio anotações que agora estou organizando de tal forma que no futuro eu as use em posts, vídeos e ou palestras.

Nessa jornada eu percebi que as vezes eu acabo assumindo que muitas coisas são obvias e, quando eu vou transmitir esse conhecimento, acabo não me dando conta disso.
Eu tenho exercitado meu cérebro cada vez mais para tentar identificar esse tipo de situação, justamente para que eu consiga reformular a minha ideia e repassar aquilo que eu sei da melhor maneira possível.
O que parece ser óbvio para mim, pode não parecer para você. Fato!


Antes de continuar, #VemCodar com a gente!!

Tá afim de criar APIs robustas com .NET?

Feito de dev para dev, direto das trincheiras, por quem coloca software em produção há mais de 20 anos, o curso "APIs robustas com ASP.NET Core 8, Entity Framework, Docker e Redis" traz de maneira direta, sem enrolação, tudo que você precisa saber para construir suas APIs de tal forma que elas sejam seguras, performáticas, escaláveis e, acima de tudo, dentro de um ambiente de desenvolvimento totalmente configurado utilizando Docker e Docker Compose.

Não fique de fora! Dê um Up na sua carreira!

O treinamento acontecerá nos dias 27/02, 28/02 e 29/02, online, das 19.30hs às 22.30hs

Acesse: https://vemcodar.com.br/


Esses dias me peguei analisando um trecho de código csharp onde eu estava testando as novidades da última versão e num determinado momento me deparei num cenário onde algo funcionava de um jeito que era óbvio pra mim, mas que nitidamente uma pessoa que está começando na programação ou que não conhece muito bem como as coisas funcionam por debaixo do capô ia ficar com cara de Jackie Chan:

Imagem do Jackie Chan com o texto Wait What?

Vou remontar o cenário de uma forma mais simples e objetiva aqui, sendo assim, avalie o código abaixo:

var maria = new Pessoa(1, "Maria", 28);  
var mariaPodeDirigir = maria.PodeDirigir();  
Console.WriteLine($"Maria pode dirigir? {mariaPodeDirigir}");  
return;  

public record Pessoa(int Id, string Nome, uint Idade);  

public static class ExtensionMethods  
{  
    public static bool PodeDirigir(this Pessoa? pessoa)  
        => pessoa?.Idade >= 18;  
}
Enter fullscreen mode Exit fullscreen mode

Esse código é bem simples e acredito que seja bem descritivo. (Eu supondo obviedades novamente...)

Temos um record chamado Pessoa que contém as propriedades Id, Nome e Idade. Em seguida temos uma classe estática chamada ExtensionMethods contendo um método de extensão chamado PodeDirigir que valida se a idade de uma determinada pessoa é maior ou igual a 18.

Métodos de extensão permitem que você "adicione" métodos a tipos existentes sem criar um novo tipo derivado, recompilar ou modificar de outra forma o tipo original.
Nesse caso, toda Pessoa terá um método chamado PodeDirigir. Aquele this antes do parâmetro pessoa me indica isso.

E caso queira saber mais sobre Extension Methods clique aqui.

Ao executar o código acima, o retorno é:

Maria pode dirigir? True
Enter fullscreen mode Exit fullscreen mode

Até aqui ok, certo?

Vamos avançar para um outro cenário.
Avalie o código abaixo:

var maria = new Pessoa(1, "Maria", 28);  
maria = null;  /* SETANDO COMO NULO PRA VER O CIRCO PEGAR FOGO */
Console.WriteLine($"Maria tem {maria.Idade} anos de idade");  
return;  

public record Pessoa(int Id, string Nome, uint Idade);  

public static class ExtensionMethods  
{  
    public static bool PodeDirigir(this Pessoa? pessoa)  
        => pessoa?.Idade >= 18;  
}
Enter fullscreen mode Exit fullscreen mode

Ao executar esse trecho de código temos o seguinte retorno:

System.NullReferenceException: Object reference not set to an instance of an object.
   at Program.<Main>$(String[] args) in /Volumes/SSD/Git/BugOuFeature/BugOuFeature/Program.cs:line 3
Enter fullscreen mode Exit fullscreen mode

Object reference not set to an instance of an object!

NullReferenceException Claro!!

Após ser criada uma pessoa eu simplesmente seto a variável para null e em seguida eu tento acessar a propriedade Idade.

É óbvio que daria erro! Isso acontece desde a versão 0.0.1-pre-alpha do csharp!

Ok, seguimos para um outro cenário muito parecido com esse!

var maria = new Pessoa(1, "Maria", 28);  
maria = null; /* JA VI O CIRCO PEGAR FOGO, AGORA QUERO SABER O QUE VAI ACONTECER... */
var mariaPodeDirigir = maria.PodeDirigir();  
Console.WriteLine($"Maria pode dirigir? {mariaPodeDirigir}");  
Console.WriteLine($"Maria tem {maria.Idade} anos de idade"); 
return;  

public record Pessoa(int Id, string Nome, uint Idade);  

public static class ExtensionMethods  
{  
    public static bool PodeDirigir(this Pessoa? pessoa)  
        => pessoa?.Idade >= 18;  
}
Enter fullscreen mode Exit fullscreen mode

Dado o exemplo anterior onde eu seto a variável maria para nulo e em seguida tento ler o valor da sua propriedade Idade e recebo um erro, qual seria o resultado do código acima?

A) Maria pode dirigir? False
B) Object reference not set to an instance of an object!
C) Palmeiras não tem Mundial
D) Todas as alternativas anteriores

Se você respondeu a letra A, acertou.
Se você respondeu a letra B, acertou também.
Se você respondeu a letra C, mais do que acertou, merece um prêmio!

Agora, se você respondeu a letra D, saberia explicar o que aconteceu?

Saca só que coisa interessante. Esse é o resultado da execução acima:

Maria pode dirigir? False
Unhandled exception. System.NullReferenceException: Object reference not set to an instance of an object...
Enter fullscreen mode Exit fullscreen mode

Primeiro imprimiu o texto Maria pode dirigir? False e em seguida lançou o erro Object reference not set to an instance of an object.

Um objeto nulo conseguiu invocar um método de extensão, mas não conseguiu invocar uma propriedade.

Para mim parece óbvio o motivo. Mas no fundo não é.
Não faz sentido semanticamente, parece um bug do compilador, mas na verdade é uma "feature".

E o segredo encontra-se no método de extensão. Será que ele adiciona mesmo um método a um tipo existente? Qual mágica ele faz por debaixo do capô?

Não se preocupe e como de costume pegue um café que eu explico tudinho...


Métodos de Extensão não existem...

Pra começar, quero trazer um post que eu escrevi sobre Açúcar Sintático: Por debaixo do capô: csharp e Açúcar Sintático!.
Nele eu explico o que é como funciona esse tal de Syntactic Sugar ou para nós tupiniquins, Açúcar Sintático!

Eu gosto muito de usar o site SharpLab. Nele eu consigo ver as entranhas do código csharp, consigo entender como o compilador resolve os famigerados Açúcares Sintáticos.

O compilador do csharp - Roslyn - transforma o código escrito em IL e esse código é executado pelo .Net Runtime via JIT ou compilado em AOT, aliás escrevi um post muito bom sobre esse tema, modéstia à parte: .NET 8, JIT e AOT.

Usando o SharpLab é possível ver o seu código csharp compilado em IL.
Além disso é possível fazer engenharia reversa no IL transformando-o em csharp (ou em vb.net). E isso para nós é muito útil, afinal, dessa forma podemos ver todo o trabalho que o compilador faz para que a gente escreva o mínimo de código possível. Essa é a função dos Açúcares Sintáticos!

Sendo assim, vamos usar o SharpLab para saber como o nosso código de exemplo é compilado.
Eu não vou explicar todo código gerado pelo SharpLab trecho a trecho, já fiz isso no post citado logo acima, quero apenas focar no método de extensão, afinal, como disse anteriormente, é ele quem faz a mágica acontecer, ou melhor, é ele quem confunde tudo....

public static class ExtensionMethods
{
    public static bool PodeDirigir(Pessoa pessoa)
    {
        if ((object)pessoa == null)
        {
            return false;
        }
        return pessoa.Idade >= 18;
    }
}
Enter fullscreen mode Exit fullscreen mode

Já podemos notar logo de cara que o operador de nulo ? se transforma num bom e velho if.
Outro ponto é que o indicador this da variável pessoa simplesmente some também. Ele serve justamente para indicar que o tipo Pessoa vai ter um método chamado PodeDirigir, certo?

Mas não é isso que acontece de fato.
Olha como o compilador do csharp resolve a invocação desse método de extensão:

Pessoa pessoa = new Pessoa(1, "Maria", 28u);
pessoa = null;
bool value = ExtensionMethods.PodeDirigir(pessoa); // <======
Console.WriteLine($"Maria pode dirigir? {value}");
Console.WriteLine($"Maria tem {pessoa.Idade} anos de idade");
// .....
Enter fullscreen mode Exit fullscreen mode

Os métodos de extensão não adicionam um novo método ao tipo!

O que temos é o método PodeDirigir sendo invocado normalmente recebendo o objeto pessoa como parâmetro. Nesse cenário, tanto faz o objeto pessoa estar ou não nulo.
Dentro desse método podemos notar que existe uma verificação e que se o parâmetro for null, o retorno do método é false e é por isso que recebemos a mensagem Maria pode dirigir? False.
Em seguida temos o trecho de código que vai dar erro, afinal estamos tentando acessar a propriedade Idade de um objeto nulo.

Lendo o código gerado pelo compilador do csharp fica claro o motivo pelo qual não recebemos erro de nulo ao chamar o método de extensão, afinal, ele não existe!

Mas esse entendimento só fica claro a partir do momento em que aprendemos como as coisas funcionam por debaixo do capô. A gente leva um tempo até achar que isso é "óbvio", aliás, para quem está começando com csharp esse tipo de situação é bem estranho, soa como um bug e faz total sentido soar assim, afinal, a linguagem apresenta dois tipos de comportamentos diferentes para cenários iguais.

O Açúcar Sintático simplifica a escrita de código e traz esse comportamento totalmente incompatível com a semântica da linguagem: Não deveria ser possível acessar um método a partir de um objeto nulo! Mas métodos de extensão permitem esse comportamento, afinal eles não adicionam um novo método ao tipo.

Quem não conhece a fundo vai achar que é um bug. Quem já se aventurou pelas entranhas do csharp vai dizer que é uma feature.

Eu acho estranho, mas já me acostumei...

O que você acha disso? Coloca aí nos comentários.

Era isso. Muito obrigado! Até a próxima!

Top comments (0)