C# 14 - Apresentando Extension Members


     Hoje vou apresentar o novo recurso do C# 14 : Extension Members

O novo recurso Extension Members do C# 14 expande o conceito dos métodos de extensão, permitindo adicionar propriedades, índices, métodos de instância e até membros estáticos associados ao tipo estendido



Vamos iniciar recordando um pouco sobre os métodos de extensão (que foram incluídos no C# 3.0)

Temos a seguir o código que representa a sintaxe usada para criar um método de extensão:

public static class NomeDaClasseDeExtensao
{
     public static TipoRetorno NomeDoMetodo(this TipoAlvo instancia, ParametrosExtras...)
     {
         // corpo do método
     } 
}

Nesta sintaxe :
- Temos que criar uma classe estática onde vamos criar os métodos de extensão,
- Os métodos de extensão tem que ser estáticos.
- Sendo que o primeiro parâmetro do método deve especificar o tipo no qual o método opera e ele deve ser precedido pelo modificador this.

O this serve para informar ao compilador que este primeiro parâmetro representa a instância que está sendo estendida e que o método deve ser tratado como se fosse um método de instância, mesmo sendo estático.

Com exemplo temos um método de extensão ContaPalavras que opera sobre o tipo string

public static class StringExtension
{
     public static int ContaPalavras(this String texto)
    {
        return texto.Split(new char[] { ' ', '.', '?' }, StringSplitOptions.RemoveEmptyEntries).Length;
     }
}

oNeste código criamos a classe estática StringExtensions, e, a seguir o método estático ContaPalavras, definindo o tipo alvo como string precedido do modificador this.

Para usar esse método como se fosse um método de instância do tipo string podemos fazer assim:

string texto = “Membros de extensão no C# 14”;
 int numeroPalavras = texto.ContaPalavras();

Embora seja um recurso poderoso os métodos de extensão possuem limitações como
- Funcionarem somente para métodos, e apenas como açúcar sintático para uma chamada estática
- E além disso temos que usar o namespace da classe de extensão importado via using

Assim os métodos de extensão agem sobe a instância do tipo e ficam em classes externas e não modificam realmente o tipo — são apenas resolvidos pelo compilador. O método é estático, mas se comporta como se fosse de instância, ou seja, os métodos de extensão não ficam dentro do tipo.

Eles sempre ficam em  uma classe externa que precisa ser estática  e estar dentro de algum namespace.

Extension Members

Os Membros de extensão - do C# 14 expande o antigo modelo de métodos de extensão e permite adicionar propriedades, indexadores, eventos, métodos de instância e até membros estáticos associados diretamente ao tipo estendido — tudo isso sem modificar a classe original

Agora com os extension members podemos adicionar propriedades, métodos e membros estáticos
diretamente associados ao tipo, e não apenas ao objeto — e sem precisar de classes externas.

Nota: Embora tenham sidos previstos, os Eventos, indexadores e outros membros, ainda não fazem parte da implementação atual disponível a partir de 15 de novembro

Vejamos a sintaxe dos Membros de extensão expressa neste código:

public static class <ClasseDeExtensao>
{
    // ---- Membros de instância ----
    extension(<TipoAlvo> instancia)
    {
        public <Tipo> Propriedade => ...;
        public <Retorno> Metodo(<params>) { ... }
        public <Tipo> this[int index] => ...;
        public event EventHandler Evento;
    }
    // ---- Membros estáticos ----
    extension(<TipoAlvo>)
    {
        public static <Tipo> PropriedadeEstatica => ...;
        public static <Retorno> MetodoEstatico(<params>) { ... }
    }
}

Observe que eles são organizados em blocos extension(...) dentro de uma classe estática onde podem existir dois tipos de blocos:

1) Bloco de instância

Usado para adicionar propriedades, métodos, indexadores e eventos que se comportam como se fossem membros de instância do tipo alvo.

Aqui o parâmetro (ex.: instancia) representa a instância real do tipo (e por isso não precisamos mais usar o this)

2) Bloco estático associado ao tipo usado para adicionar membros estáticos, como:

- propriedades estáticas
- métodos de fábrica
- constantes
- objetos padrão
- validadores globais

A ausência do parâmetro indica que estamos anexando membros ao TIPO, não ao objeto.

Note que não é mais preciso usar o modificador this nos extension members porque agora não estamos estendendo métodos, e sim declarando blocos de extensão que já definem claramente se os membros são de instância ou estáticos.

Exemplo prático

Vejamos agora um exemplo prático onde dada uma classe Pessoa com Nome do tipo string e Idade do tipo int:

public class Pessoa
{
    public string Nome { get; set; } = "";
    public int Idade { get; set; }
}

Vamos mostrar a implementação dos extensions members criando um método e uma propriedade de extensão e também definindo um membro estático vinculado ao tipo Pessoa. Todos eles definidos fora da classe original, usando blocos de extensão.

Vamos criar a classe PessoaExtensions definindo o seguinte código:

public static class PessoaExtensions
{
    // -----------------------------
    // 1) Membros de instância
    // -----------------------------
    extension(Pessoa p)
    {
        // Método de instância (extension member)
        public string Describe() => $"{p.Nome}, {p.Idade} anos";
        // Propriedade de instância (extension member)
        public bool IsAdult => p.Idade >= 18;
    }
    // -----------------------------
    // 2) Membros estáticos associados ao tipo
    // -----------------------------
    extension(Pessoa)
    {
        public static Pessoa Default
            => new Pessoa { Nome = "Desconhecido", Idade = 0 };
    }
}

Criamos dois blocos extension onde no primeiro bloco declaramos que estamos criando **membros de instância** associados ao tipo Pessoa.

A variável p representa a instância real de Pessoa sobre a qual esses membros irão atuar.

O primeiro membro que implementamos foi a propriedade de instância IsAdult que retorna true se a idade da Pessoa for maior ou igual a 18:

public bool IsAdult => p.Idade >= 18;

A seguir no mesmo bloco, criamo o método de instância Describe que retorna uma string descrevendo a instancia da Pessoa com nome e idade:

public string Describe() => $"{p.Nome}, {p.Idade} anos";

No segundo bloco extension não definimos uma variável de instância. Isso indica ao compilador que este bloco define **membros estáticos associados ao tipo** Pessoa, e não a uma instância específica.

Dentro deste bloco vamos criamos o membro estático Default que retorna uma instância padrão de Pessoa com Nome 'Desconhecido' e idade zero para um tipo Pessoa:

 public static Pessoa Default => new Pessoa { Nome = "Desconhecido", Idade = 0 };

Com isso, concluímos a implementação dos extension members:

• IsAdult → propriedade de instância
• Describe → método de instância
• Default → membro estático associado ao tipo Pessoa

E tudo isso sem alterar a classe original Pessoa, ou seja, o membro estático não fica na classe de extensão; ele aparece como se fizesse parte do tipo original.

É por isso que hoje podemos escrever:  var p = Pessoa.Default; mesmo que o código de extensão esteja em outro arquivo.

Agora que já implementamos os extension members para a classe Pessoa, vamos ver na prática como utilizá-los dentro da classe Program: 

using CSharp_ExtensionMembers;
Console.WriteLine("=== Demonstração Extension Members (C# 14 / .NET 10) ===");
Console.WriteLine("\nCriando objeto p :  Maria com 20 anos");
var p = new Pessoa { Nome = "Maria", Idade = 20 };
// ----------------------
// MÉTODO DE EXTENSÃO
// ----------------------
Console.WriteLine("\nMétodo de Extensão: Describe()");
Console.WriteLine(p.Describe());
Console.WriteLine("\nPressione algo para continuar");
Console.ReadKey();
// ----------------------
// PROPRIEDADE DE EXTENSÃO
// ----------------------
Console.WriteLine("\nPropriedade de Extensão: IsAdult");
Console.WriteLine($"É adulta? {p.IsAdult}");
Console.WriteLine("\nPressione algo para continuar\n");
Console.ReadKey();
// ---------------------------------
// MEMBRO ESTÁTICO ASSOCIADO AO TIPO
// ----------------------------------
Console.WriteLine("Membro Estático Associado ao Tipo: Default");
var padrao = Pessoa.Default;  // <-- Direto no tipo Pessoa!
Console.WriteLine($"Default: {padrao.Describe()}");
Console.WriteLine("\n--- Fim da demonstração ---");
Console.ReadKey();

Primeiro, usamos o método de instância que criamos, o Describe(). Mesmo ele estando fora da classe Pessoa, eu posso chamá-lo diretamente da instância:

Console.WriteLine(p.Describe());

O compilador trata esse método como se ele fizesse parte da classe original.

Em seguida, vamos usar a propriedade de extensão IsAdult. Ela também aparece naturalmente no objeto:

Console.WriteLine(p.IsAdult);

Agora o membro estático associado ao tipo, definido no bloco extension(Pessoa). Diferente dos anteriores, esse membro pertence ao tipo, e não a uma instância. Por isso o acesso é direto no nome da classe:

var padrao = Pessoa.Default;

Esse Default não existe dentro da classe original Pessoa, mas graças aos extension members, ele aparece como se fizesse parte do tipo.

Isso possibilita criar valores padrão, métodos fábrica e utilidades associadas ao tipo de maneira organizada, sem alterar a classe original.

Por fim, como Default retorna uma nova Pessoa, eu posso usá-la normalmente:

Console.WriteLine(padrao.Describe());

Dessa forma, vimos o uso dos três tipos de extensão que criamos:

• método de instância
• propriedade de instância
• membro estático associado ao tipo

Todos funcionando de forma natural, como se fossem da própria classe Pessoa, embora tenham sido definidos externamente através dos extension members.

Essa é exatamente a grande vantagem desse recurso.

Pegue o projeto no github : https://github.com/macoratti/CSharp_ExtensionMembers

E estamos conversados...  

"Quem é fiel no mínimo, também é fiel no muito; quem é injusto no mínimo, também é injusto no muito."
Lucas 16:10

Referências:


José Carlos Macoratti