DEV Community

Angelo Belchior
Angelo Belchior

Posted on • Edited on

Por debaixo do capô: csharp e Açúcar Sintático!

Você já ouviu falar em "açúcar sintático" em programação? Não, eu não estou falando de doces ou guloseimas, mas sim de uma técnica que torna a escrita de código mais doce para quem desenvolve, afinal de amarga já basta a vida. Hoje quero mostrar como o csharp explora essa técnica de forma maravilhosamente linda.

Mas o que é Açúcar Sintático?

O açúcar sintático é uma característica das linguagens de programação que oferece uma maneira mais concisa ou mais "doce" de escrever código. Ele não altera a funcionalidade do código, mas simplifica a sintaxe, tornando-a mais legível e menos verbosa.

O termo Açúcar Sintático foi criado por Peter J. Landin em 1964 para descrever a sintaxe de um ALGOL simples - como a linguagem de programação que foi definida semanticamente nos termos da aplicação das expressões do Cálculo lambda, centrada na substituição léxica do λ com "where". (fonte wikipedia)

Açúcar Sintático em csharp

O csharp é cheio de truques por debaixo do capô. A gente utiliza no dia-a-dia e as vezes nem sequer para pra pensar como as coisas funcionam.

Quero começar com um exemplo simples. Propriedades.

As propriedades começaram com essa característica de escrita logo nas primeiras versões do dotnet:

private string _nome;
public string Nome
{
    get { return _nome; }
    set { _nome = value; }
}
Enter fullscreen mode Exit fullscreen mode

Depois evoluíram para isso:

public string Nome { get; set; }
Enter fullscreen mode Exit fullscreen mode

Muito mais simples, certo?

Mas afinal, como que isso funciona por debaixo do capô? Como que o compilador do csharp resolve esse tipo de código?

Ao ser compilado, os dois exemplos acima resultam no mesmo código:

    private string k__BackingField;

    public void set_Nome(string value)
    {
        k__BackingField = value;
    }

    public string get_Nome()
    {
        return k__BackingField;
    }
Enter fullscreen mode Exit fullscreen mode

Imagem do Jackie Chan com o texto Wait What?

Sim, as propriedades nada mais são do que métodos, assim como é feito em Java, só que de um jeito muito mais inteligente. No caso, a variável k__BackingField pode ter outro nome, o compilador se encarrega em definir qual.

Se você duvida, segue um print do Rider:

Image description

Na imagem é mostrado que existem erros no get e no set da propriedade Nome, e a mensagem que o compilador gera para cada um desses erros é "Member with the same signature is already declared".

Interessante, não é? Isso é só o começo.


Vamos para o var. Esse é um caso clássico.

Basicamente o compilador infere o tipo do dado e substitui o var pelo tipo correto:

// Escrito por mim
var nome = "Angelo";
Enter fullscreen mode Exit fullscreen mode
// Gerado pelo compilador
string nome = "Angelo";
Enter fullscreen mode Exit fullscreen mode

E tipos numéricos? Como o compilador sabe se é um int, float, decimal ou double?
Bem, nesse caso você vai precisar deixar explícito, caso contrário, o compilador assume que é um inteiro.

// Escrito por mim
var meuInt = 0;
var meuDouble = 0d;
var meuFloat = 0f;
var meuDecimal = 0m;
Enter fullscreen mode Exit fullscreen mode
// Gerado pelo compilador
int num = 0;
double num2 = 0.0;
float num3 = 0f;
decimal num4 = default(decimal);
Enter fullscreen mode Exit fullscreen mode

Bacana, não? Bom, eu acho! Ainda mais que é possível notar que os nomes das variáveis não necessariamente serão mantidos pelo compilador. Curioso.


E o que acontece quando uma variável é criada, mas não é usada?

// Escrito por mim
int a = 62; // <---- não usada

int b = 45;
Console.WriteLine(b);
Enter fullscreen mode Exit fullscreen mode
// Gerado pelo compilador
Console.WriteLine(45);
Enter fullscreen mode Exit fullscreen mode

Nada. Simples assim. O compilador simplesmente a ignora.


Vamos para o próximo: Tipo Anônimo!
Analise o seguinte código:

// Escrito por mim
var pontos = new { x = 1, y = 2};
Console.WriteLine(pontos.x);        
Console.WriteLine(pontos.y);
Enter fullscreen mode Exit fullscreen mode

A variável pontos é um objeto. Isto é um fato. Todo tipo herda de object em csharp. Porém o tipo object não tem as propriedades x nem y. Então qual é a mágica que o compilador faz para que isso funcione?

Respira fundo...

// Gerado pelo compilador
[CompilerGenerated]
[DebuggerDisplay("\\{ x = {x}, y = {y} }", Type = "<Anonymous Type>")]
internal sealed class <>f__AnonymousType0<<x>j__TPar, <y>j__TPar>
{
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly <x>j__TPar <x>i__Field;

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly <y>j__TPar <y>i__Field;

    public <x>j__TPar x
    {
        get
        {
            return <x>i__Field;
        }
    }

    public <y>j__TPar y
    {
        get
        {
            return <y>i__Field;
        }
    }

    [DebuggerHidden]
    public <>f__AnonymousType0(<x>j__TPar x, <y>j__TPar y)
    {
        <x>i__Field = x;
        <y>i__Field = y;
    }

    [DebuggerHidden]
    public override bool Equals(object value)
    {
        <>f__AnonymousType0<<x>j__TPar, <y>j__TPar> anon = value as <>f__AnonymousType0<<x>j__TPar, <y>j__TPar>;
        return this == anon || (anon != null && EqualityComparer<<x>j__TPar>.Default.Equals(<x>i__Field, anon.<x>i__Field) && EqualityComparer<<y>j__TPar>.Default.Equals(<y>i__Field, anon.<y>i__Field));
    }

    [DebuggerHidden]
    public override int GetHashCode()
    {
        return (959378729 * -1521134295 + EqualityComparer<<x>j__TPar>.Default.GetHashCode(<x>i__Field)) * -1521134295 + EqualityComparer<<y>j__TPar>.Default.GetHashCode(<y>i__Field);
    }

    [DebuggerHidden]
    public override string ToString()
    {
        object[] array = new object[2];
        <x>j__TPar val = <x>i__Field;
        array[0] = ((val != null) ? val.ToString() : null);
        <y>j__TPar val2 = <y>i__Field;
        array[1] = ((val2 != null) ? val2.ToString() : null);
        return string.Format(null, "{{ x = {0}, y = {1} }}", array);
    }
}

<>f__AnonymousType0<int, int> anon = new <>f__AnonymousType0<int, int>(1, 2);
Console.WriteLine(anon.x);
Console.WriteLine(anon.y);
Enter fullscreen mode Exit fullscreen mode

Que loucura!!! Olha o tanto de código gerado!!! O compilador do csharp gera uma classe com as propriedades x e y. O nome dessa classe é <>f__AnonymousType0 e tem dois tipos genéricos <x>j__TPar e <y>j__TPar.

Claramente, se você copiar e colar esse código na sua IDE ele não vai compilar, afinal, não é possível criar nomes de classes em csharp que contenham os caracteres <>.

Mas de onde surgiu esse nome? Do IL - Intermediate Language. Em IL é possível ter esses caracteres nos nomes de classes, variáveis, tipos e etc.
Abaixo segue um trecho de código IL da classe anônima gerada:

.class private auto ansi sealed beforefieldinit '<>f__AnonymousType0`2'<'<x>j__TPar', '<y>j__TPar'>
    extends [System.Runtime]System.Object
...
Enter fullscreen mode Exit fullscreen mode

O compilador é muito trabalhador! Ele criou o construtor e os métodos Equals, GetHashCode e ToString. Com isso, temos uma classe muito bem definida e estruturada. De anônima, só o nome mesmo!

Além de trabalhar bastante, ele trabalha com inteligência. Se você criar um outro objeto anônimo com as mesmas propriedades, a classe criada é reaproveitada, sendo assim, as duas variáveis criadas abaixo vão utilizar a mesma classe <>f__AnonymousType0:

var pontos = new { x = 10, y = 20};
var pontos2 = new { x = 30, y = 4o};
Enter fullscreen mode Exit fullscreen mode

Vamos avançando!
Sabe o foreach? E se eu te disser que ele não existe?

Abaixo temos um código simples, onde é criada uma lista de string e depois eu varro ela, imprimindo cada item.

// Escrito por mim
var items = new List<string>();
items.Add("A");        
items.Add("B");
items.Add("C");

foreach(var item in items)
{
    Console.WriteLine(item);
}
Enter fullscreen mode Exit fullscreen mode

Porém, analisando o código abaixo, nota-se que não existe uma instrução foreach! Basicamente o que se tem é o uso do Iterator Pattern!

// Gerado pelo compilador
List<string> list = new List<string>();
list.Add("A");
list.Add("B");
list.Add("C");
List<string>.Enumerator enumerator = list.GetEnumerator();
try
{
    while (enumerator.MoveNext())
    {
        string current = enumerator.Current;
        Console.WriteLine(current);
    }
}
finally
{
    ((IDisposable)enumerator).Dispose();
}
Enter fullscreen mode Exit fullscreen mode

Falando em IDisposable, como será que o compilador lida com a instrução using?

// Escrito por mim
using (var disposable = new Disposable())
{
    disposable. Execute();
}
Enter fullscreen mode Exit fullscreen mode
// Gerado pelo compilador
Disposable disposable = new Disposable();
try
{
    disposable.Execute();
}
finally
{
    if (disposable != null)
    {
        ((IDisposable)disposable).Dispose();
    }
}
Enter fullscreen mode Exit fullscreen mode

Ele utiliza um singelo try/finally, invocando o método Dispose da interface no finally.


Bora falar sobre Interpolação de Strings!

Interpolação de Strings é uma das coisas mais legais que temos no csharp. Essa feature facilita muito a escrita de textos que contenham variáveis dentro:

// Escrito por mim
var nome = "Angelo";
var frase = $"Meu nome é {nome}";
Console.WriteLine(frase);  
Enter fullscreen mode Exit fullscreen mode

No código abaixo podemos notar o quão inteligente é o compilador do csharp. Nesse exemplo simples, o compilador utiliza o método Concat da string.

// Gerado pelo compilador
string text = "Angelo";
string value = string.Concat("Meu nome é ", text);
Console.WriteLine(value);
Enter fullscreen mode Exit fullscreen mode

Porém, caso tenhamos mais de uma variável na concatenação, a escrita de código muda completamente, focando na performance e em não disperdiçar memória.

Vejamos esse exemplo.

// Escrito por mim
var nome = "Angelo";
var idade = 39;
var cidade = "São Paulo";
var time = "Corinthians";
var frase = $"Meu nome é {nome}, tenho {idade} anos, moro na cidade de {cidade} e torço para o time {time}";
Console.WriteLine(frase); 
Enter fullscreen mode Exit fullscreen mode

E o resultado:

// Gerado pelo compilador
string value = "Angelo";
int value2 = 39;
string value3 = "São Paulo";
string value4 = "Corinthians";
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(65, 4);
defaultInterpolatedStringHandler.AppendLiteral("Meu nome é ");
defaultInterpolatedStringHandler.AppendFormatted(value);
defaultInterpolatedStringHandler.AppendLiteral(", tenho ");
defaultInterpolatedStringHandler.AppendFormatted(value2);
defaultInterpolatedStringHandler.AppendLiteral(" anos, moro na cidade de ");
defaultInterpolatedStringHandler.AppendFormatted(value3);
defaultInterpolatedStringHandler.AppendLiteral(" e torço para o time ");
defaultInterpolatedStringHandler.AppendFormatted(value4);
string value5 = defaultInterpolatedStringHandler.ToStringAndClear();
Console.WriteLine(value5);
Enter fullscreen mode Exit fullscreen mode

Note que coisa interessantíssima temos aqui. O compilador utilizou a struct DefaultInterpolatedStringHandler que fica no namespace System.Runtime.CompilerServices para fazer a concatenação da string. Esse processo é muito mais eficiente do que um string.Concat ou string.Format.

Nesse post o Sergey Teplyakov, que é um engenheiro da Microsoft, dá detalhes de como o DefaultInterpolatedStringHandler funciona por debaixo do capô e o quão performático ele é em relação ao outros métodos. Vale muito a pena ler!


Vamos em frente, e agora eu quero falar sobre Extension Methods.

Essa funcionalidade, como o próprio nome diz permite adicionar novos métodos em tipos já existentes.

Um bom exemplo são os métodos do LINQ para as listas. Basta incluir o namespace System.Linq que nossas listas terão suportes para métodos como Where, FirstOrDefault, GroupBy, Select, etc.

Criar Extension Methods é bem simples, segue um exemplo.

// Escrito por mim
public static class MarkDownExtensionMethods
{
    public static string ToItalic(this string value)
    {
        return $"_{value}_";
    }
}

var md = "Esse é um texto em itálico".ToItalic();
Console.WriteLine(md);
Enter fullscreen mode Exit fullscreen mode

Basicamente, no método ToItalic temos um parâmetro string chamado value. Porém, o que transforma esse método em uma extensão são o fatos da classe e o método ToItalic serem estáticos, além do uso do this no parâmetro value. Isso quer dizer que toda string terá o método ToItalic.

E como é que o compilador resolve isso? Será que temos aqui uma mágica mais rebuscada? Não! Basicamente é utilizado o método MarkDownExtensionMethods.ToItalic passando uma string como parâmetro. Simples, simples, simples. Eu confesso que não acreditei que era algo tão simplório quanto isto:

// Gerado pelo compilador
[Extension]
public static class MarkDownExtensionMethods
{
    [System.Runtime.CompilerServices.NullableContext(1)]
    [Extension]
    public static string ToItalic(string value)
    {
        return string.Concat("_", value, "_");
    }
}

string value = MarkDownExtensionMethods.ToItalic("Esse é um texto em itálico");
Console.WriteLine(value);
Enter fullscreen mode Exit fullscreen mode

E pra fechar, eu escolhi demonstrar os Records. Record é algo sensacional, muito prático e resolve muitos problemas. Record pode ser uma classe, ou uma struct. A grande sacada aqui é que é um objeto imutável, isto é, após sua criação não é possível alterar suas propriedades.

// Escrito por mim

public record Pessoa(string Nome, int Idade);

//Cria um objeto imutável
var angelo = new Pessoa("Angelo", 39);
Console.WriteLine(angelo);

// Cria um novo objeto imutável a partir de um outro e muda apenas a Idade
var angeloComNovaIdade = angelo with { Idade = 35 };
Console.WriteLine(angeloComNovaIdade);
Enter fullscreen mode Exit fullscreen mode

Agora, respira... vai com calma...

// Gerado pelo compilador
[System.Runtime.CompilerServices.NullableContext(1)]
[System.Runtime.CompilerServices.Nullable(0)]
public class Pessoa : IEquatable<Pessoa>
{
    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly string <Nome>k__BackingField;

    [CompilerGenerated]
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly int <Idade>k__BackingField;

    [CompilerGenerated]
    protected virtual Type EqualityContract
    {
        [CompilerGenerated]
        get
        {
            return typeof(Pessoa);
        }
    }

    public string Nome
    {
        [CompilerGenerated]
        get
        {
            return <Nome>k__BackingField;
        }
        [CompilerGenerated]
        init
        {
            <Nome>k__BackingField = value;
        }
    }

    public int Idade
    {
        [CompilerGenerated]
        get
        {
            return <Idade>k__BackingField;
        }
        [CompilerGenerated]
        init
        {
            <Idade>k__BackingField = value;
        }
    }

    public Pessoa(string Nome, int Idade)
    {
        <Nome>k__BackingField = Nome;
        <Idade>k__BackingField = Idade;
        base..ctor();
    }

    [CompilerGenerated]
    public override string ToString()
    {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.Append("Pessoa");
        stringBuilder.Append(" { ");
        if (PrintMembers(stringBuilder))
        {
            stringBuilder.Append(' ');
        }
        stringBuilder.Append('}');
        return stringBuilder.ToString();
    }

    [CompilerGenerated]
    protected virtual bool PrintMembers(StringBuilder builder)
    {
        RuntimeHelpers.EnsureSufficientExecutionStack();
        builder.Append("Nome = ");
        builder.Append((object)Nome);
        builder.Append(", Idade = ");
        builder.Append(Idade.ToString());
        return true;
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    [CompilerGenerated]
    public static bool operator !=(Pessoa left, Pessoa right)
    {
        return !(left == right);
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    [CompilerGenerated]
    public static bool operator ==(Pessoa left, Pessoa right)
    {
        return (object)left == right || ((object)left != null && left.Equals(right));
    }

    [CompilerGenerated]
    public override int GetHashCode()
    {
        return (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<string>.Default.GetHashCode(<Nome>k__BackingField)) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(<Idade>k__BackingField);
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    [CompilerGenerated]
    public override bool Equals(object obj)
    {
        return Equals(obj as Pessoa);
    }

    [System.Runtime.CompilerServices.NullableContext(2)]
    [CompilerGenerated]
    public virtual bool Equals(Pessoa other)
    {
        return (object)this == other || ((object)other != null && EqualityContract == other.EqualityContract && EqualityComparer<string>.Default.Equals(<Nome>k__BackingField, other.<Nome>k__BackingField) && EqualityComparer<int>.Default.Equals(<Idade>k__BackingField, other.<Idade>k__BackingField));
    }

    [CompilerGenerated]
    public virtual Pessoa <Clone>$()
    {
        return new Pessoa(this);
    }

    [CompilerGenerated]
    protected Pessoa(Pessoa original)
    {
        <Nome>k__BackingField = original.<Nome>k__BackingField;
        <Idade>k__BackingField = original.<Idade>k__BackingField;
    }

    [CompilerGenerated]
    public void Deconstruct(out string Nome, out int Idade)
    {
        Nome = this.Nome;
        Idade = this.Idade;
    }
}

Pessoa pessoa = new Pessoa("Angelo", 39);
Console.WriteLine(pessoa);
Pessoa pessoa2 = pessoa.<Clone>$();
pessoa2.Idade = 35;
Pessoa value = pessoa2;
Console.WriteLine(value);
Enter fullscreen mode Exit fullscreen mode

Novamente aqui temos o compilador trabalhando a todo vapor. Ele criou uma classe Pessoa com as propriedades Nome e Idade somente leitura (sem os set). No construtor ele recebe esses valores e preenche os campos. Isso é como a gente fazia antes para ter uma classe imutável.

Porém ele não pára por ai. Ele reescreve os operadores de comparação != e == (sim, é possível fazer isso!!) e implementa a interface IEquatable<Pessoa>. Isso vai garantir que dois objetos Pessoa, com os mesmos valores, sejam tratados como iguais.

Além disso temos um método chamado <Clone>$, que vai servir para, vejam só, clonar o objeto. Ele vai ser chamado quando for necessário criar um novo objeto a partir de um objeto (do mesmo tipo) já existente - que é o caso do var angeloComNovaIdade = angelo with { Idade = 35 };.

O compilador ainda faz o favor de criar o método Deconstruct. E pra que serve esse método? Para podermos fazer algo como:

// Escrito por mim

var angelo = new Pessoa("Angelo", 39);
var (nome, idade) = angelo;

Console.WriteLine(nome);
Console.WriteLine(idade);
Enter fullscreen mode Exit fullscreen mode
// Gerado pelo compilador
string Nome;
int Idade;
new Pessoa("Angelo", 39).Deconstruct(out Nome, out Idade);
string value = Nome;
int value2 = Idade;
Console.WriteLine(value);
Console.WriteLine(value2);
Enter fullscreen mode Exit fullscreen mode

Já pensou ter que fazer tudo isso na mão só pra ter um objeto imutável? Acho que agora dá pra entender os benefícios do Acúcar Sintático :)


Quer explorar mais como o compilador do csharp resolve determinados códigos e saber como eu obtive o retorno do compilador? Use o https://sharplab.io/.

Nesse site é possível escrever um código csharp e ver o IL gerado, e a partir desse IL o site consegue recriar o csharp, dessa vez com todos os códigos adicionados no momento da compilação. Recomendo fortemente que você teste o uso do async/await. Com certeza vai se surpreender! Aliás, quero escrever escrevi um post só falando sobre isso.

E aí, tem algum açúcar sintático que faltou aqui? Faltaram vários!!! Posta ai nos comentários!

Era isso. :)

Top comments (1)

Collapse
 
ilustreandre profile image
André

Muito bom, confesso que não imaginava que o compilador trabalhava tanto!