C# Métodos de Extensão

A Ana calcula descontos o tempo todo. Seria ótimo escrever preco.ComDesconto(10), como se o decimal tivesse nascido com esse método. Mas o decimal é do .NET, e ela não pode mexer nele. Um método de extensão resolve isso: é um método estático, escrito por você, que o compilador deixa chamar com o ponto, como se fosse do próprio tipo. O LINQ inteiro é feito de métodos de extensão, e o C# 14 trouxe um jeito novo de escrevê-los, com propriedades.

Você vai aprender
  • Criar um método de extensão com static class e this.
  • Que preco.ComDesconto(10) e ExtensoesDePreco.ComDesconto(preco, 10) são a mesma chamada.
  • Extensões para string e para List<Produto>, inclusive uma que aceita null.
  • O using que faz a extensão aparecer, e quem vence quando o tipo já tem um método com o mesmo nome.
  • Os blocos extension do C# 14: propriedades e membros estáticos de extensão.
  • Por que o Where do LINQ aparece em qualquer lista.
  • Os erros reais: CS1061, CS1106, CS9283 e CS0122.

Todos os programas rodaram com o .NET 10 (C# 14) na máquina da Ana, na pasta ~/cs, com dotnet run arquivo.cs.


A ideia: o adesivo na máquina

A balança da lojinha veio de fábrica com os botões "pesar" e "zerar". A Ana não pode abrir a balança e soldar um botão novo. Mas ela pode colar ao lado um adesivo com uma tabela: "para o preço do quilo de bala, multiplique o peso por 32". Quem usa a balança enxerga o adesivo junto dos botões, como se fizesse parte dela.

O método de extensão é o adesivo: ele fica fora do tipo (numa classe sua), não muda nada dentro dele e só usa o que o tipo já mostra para todo mundo. Mas, na hora de usar, aparece com o ponto, junto dos métodos do próprio tipo.


1. O primeiro método de extensão

decimal preco = 50m;

Console.WriteLine(preco.ComDesconto(10));
Console.WriteLine(129.90m.ComDesconto(20));
Console.WriteLine(ExtensoesDePreco.ComDesconto(preco, 10));

static class ExtensoesDePreco
{
    public static decimal ComDesconto(this decimal preco, int porcento)
    {
        return preco - preco * porcento / 100;
    }
}
$ dotnet run extensao.cs
45
103,92
45

Três ingredientes transformam um método estático comum num método de extensão:

static class ExtensoesDePreco
A classe que guarda as extensões precisa ser static (veja static) e não pode ser genérica nem ficar dentro de outra classe. O nome é livre; a convenção é Extensoes… ou …Extensions.
public static
O método também é estático.
this decimal preco
O this antes do primeiro parâmetro é o que faz a mágica: diz "este método estende o tipo decimal". Na chamada preco.ComDesconto(10), o valor antes do ponto vira o preco.
int porcento
Os outros parâmetros são normais e vão entre os parênteses: o 10 de ComDesconto(10).

A terceira linha da saída mostra o segredo: preco.ComDesconto(10) é só um jeito mais bonito de escrever ExtensoesDePreco.ComDesconto(preco, 10). O compilador troca uma pela outra; nada é acrescentado ao decimal de verdade. A segunda linha mostra que até um número escrito direto no código ganha o método: 129.90m.ComDesconto(20) dá 103,92.

Missão: o ComFrete é um método estático comum, chamado com o nome da classe. Transforme-o em método de extensão (acrescente o this) e troque a chamada por preco.ComFrete(). A saída continua Com frete: 28.

decimal preco = 20m;
Console.WriteLine($"Com frete: {ExtensoesDePreco.ComFrete(preco)}");

static class ExtensoesDePreco
{
    public static decimal ComFrete(decimal preco) => preco + 8m;
}
decimal preco = 20m;
Console.WriteLine($"Com frete: {preco.ComFrete()}");

static class ExtensoesDePreco
{
    public static decimal ComFrete(this decimal preco) => preco + 8m;
}

2. Extensões para string (e para null)

Qualquer tipo pode ganhar extensões. Para a vitrine da lojinha, a Ana quer resumir nomes compridos e pôr a primeira letra em maiúscula:

string nome = "caderno universitário de 200 folhas";

Console.WriteLine(nome.Resumir(15));
Console.WriteLine("lápis".Resumir(15));
Console.WriteLine(nome.Capitalizar());

string? vazio = null;
Console.WriteLine(vazio.EstaVazio());
Console.WriteLine("".EstaVazio());
Console.WriteLine(nome.EstaVazio());

static class ExtensoesDeTexto
{
    public static string Resumir(this string texto, int tamanho) =>
        texto.Length <= tamanho ? texto : texto[..tamanho] + "…";

    public static string Capitalizar(this string texto) =>
        texto.Length == 0 ? texto : char.ToUpper(texto[0]) + texto[1..];

    public static bool EstaVazio(this string? texto) =>
        texto is null || texto.Length == 0;
}
$ dotnet run texto.cs
caderno univers…
lápis
Caderno universitário de 200 folhas
True
True
False
  • Resumir: se o texto passa do tamanho, corta (texto[..tamanho], veja Índices e Intervalos) e põe reticências. Uma classe pode ter quantas extensões quiser.
  • Capitalizar: troca só a primeira letra por maiúscula e junta o resto (texto[1..]).
  • EstaVazio estende string? e funciona até com null. Um método de verdade chamado num null daria NullReferenceException; a extensão, não, porque é só a chamada ExtensoesDeTexto.EstaVazio(null), e o método trata o null. O .NET já tem a mesma ideia pronta: string.IsNullOrEmpty(texto).

3. Extensões para os seus tipos e para listas

Também dá para estender List<T> de um tipo específico. O carrinho de compras ganha Total() e MaisCaro():

List<Produto> carrinho =
[
    new("Caderno", 18.90m),
    new("Lápis", 2.50m),
    new("Mochila", 129.90m),
];

Console.WriteLine($"Total: {carrinho.Total():C}");
Console.WriteLine($"Mais caro: {carrinho.MaisCaro().Nome}");

static class ExtensoesDoCarrinho
{
    public static decimal Total(this List<Produto> lista)
    {
        decimal soma = 0m;
        foreach (Produto p in lista) soma += p.Preco;
        return soma;
    }

    public static Produto MaisCaro(this List<Produto> lista)
    {
        Produto maior = lista[0];
        foreach (Produto p in lista)
        {
            if (p.Preco > maior.Preco) maior = p;
        }
        return maior;
    }
}

record Produto(string Nome, decimal Preco);
$ dotnet run lista-ext.cs
Total: R$ 151,30
Mais caro: Mochila

Se o tipo é seu (como o record Produto), você poderia pôr o método direto nele. A extensão faz sentido quando o tipo não é seu (decimal, string, List<T>), ou quando você quer manter o tipo pequeno e juntar as "ferramentas" num lugar à parte.


4. O using e quem vence

A extensão só aparece com o using do namespace dela

Em projetos de verdade, as extensões ficam num namespace. Elas só aparecem no código que importa esse namespace com using:

using Loja.Extensoes;

decimal preco = 50m;
Console.WriteLine(preco.ComDesconto(10));

namespace Loja.Extensoes
{
    static class ExtensoesDePreco
    {
        public static decimal ComDesconto(this decimal preco, int porcento) =>
            preco - preco * porcento / 100;
    }
}
$ dotnet run namespace-ok.cs
45

Sem a primeira linha, o compilador não acha o método (veja o erro CS1061 em Quando dá errado). É por isso que, em projetos antigos, todo arquivo que usa LINQ começa com using System.Linq;. Hoje esse using já vem embutido (os implicit usings do .NET 6+, veja Namespaces e using).

Um método do próprio tipo sempre vence

Se o tipo já tem um método com o mesmo nome e os mesmos parâmetros, a extensão é ignorada, sem aviso:

Produto p = new("Caderno", 18.90m);
Console.WriteLine(p.Descricao());

static class ExtensoesDeProduto
{
    public static string Descricao(this Produto p) => "da extensão";
}

record Produto(string Nome, decimal Preco)
{
    public string Descricao() => $"{Nome} por {Preco:C}";
}
$ dotnet run vence.cs
Caderno por R$ 18,90     « o método do record venceu

O compilador procura primeiro nos métodos do próprio tipo, e só se não achar procura as extensões. Isso protege o seu código: uma extensão nunca muda o comportamento de um método que já existe. Mas também quer dizer que, se uma versão nova do .NET acrescentar ao tipo um método com o nome da sua extensão, a sua deixa de ser chamada.


5. Os blocos extension do C# 14 C# 14

Até o C# 13, só existiam métodos de extensão. O C# 14 (.NET 10) trouxe os blocos extension, que permitem também propriedades de extensão e membros estáticos de extensão (chamados pelo nome do tipo, como decimal.Frete):

decimal preco = 129.90m;

Console.WriteLine(preco.ComDesconto(10));
Console.WriteLine(preco.EhCaro);
Console.WriteLine(2.50m.EhCaro);
Console.WriteLine(preco.EmReais);
Console.WriteLine(decimal.Frete);
Console.WriteLine(decimal.ComFrete(20m));

static class ExtensoesDePreco
{
    extension(decimal preco)
    {
        public decimal ComDesconto(int porcento) => preco - preco * porcento / 100;
        public bool EhCaro => preco > 100m;
        public string EmReais => preco.ToString("C");
    }

    extension(decimal)
    {
        public static decimal Frete => 8m;
        public static decimal ComFrete(decimal valor) => valor + 8m;
    }
}
$ dotnet run bloco14.cs
116,91
True
False
R$ 129,90
8
28
extension(decimal preco)
Abre um bloco de extensões para o tipo decimal. O preco é o receptor: o valor antes do ponto. Ele vale para todos os membros do bloco, e por isso nenhum deles repete o this decimal preco.
public decimal ComDesconto(int porcento)
Um método de extensão, escrito como um método comum (sem static e sem this). Funciona igual ao da seção 1.
public bool EhCaro => …
Uma propriedade de extensão (veja Propriedades): chamada sem parênteses, preco.EhCaro. Antes do C# 14, isso era impossível; era preciso escrever um método EhCaro().
extension(decimal)
Um bloco sem nome de receptor: os membros dele são static e se chamam pelo nome do tipo, decimal.Frete e decimal.ComFrete(20m), como se fossem do próprio decimal.

Os blocos continuam dentro de uma static class de nível superior e não genérica, como os métodos de extensão clássicos. As duas formas podem conviver na mesma classe, e um método escrito num bloco é chamado do mesmo jeito que um escrito com this: quem usa não percebe a diferença.

Você quer…Forma clássica (this)Bloco extension (C# 14)
Um método: preco.ComDesconto(10)SimSim
Uma propriedade: preco.EhCaroNãoSim
Um membro estático: decimal.FreteNãoSim
Funcionar em versões antigas do C#Sim (desde o C# 3)Não: só C# 14 / .NET 10
Qual usar

Para métodos, as duas formas dão o mesmo resultado, e a clássica (com this) é a que você vai encontrar em quase todo o código que existe hoje, inclusive no próprio LINQ. Use os blocos quando quiser propriedades ou membros estáticos, ou quando várias extensões do mesmo tipo ficarem mais organizadas num bloco só.


6. O LINQ é feito de extensões

Agora você pode entender de onde vem o Where que aparece em qualquer lista, array ou coleção: ele é um método de extensão do tipo IEnumerable<T>, escrito na classe estática System.Linq.Enumerable. As duas linhas abaixo são a mesma chamada:

List<decimal> precos = [18.90m, 2.50m, 129.90m];
var baratos1 = precos.Where(p => p < 20m);
var baratos2 = Enumerable.Where(precos, p => p < 20m);
Console.WriteLine(string.Join(" | ", baratos1));
Console.WriteLine(string.Join(" | ", baratos2));
$ dotnet run linqext.cs
18,90 | 2,50
18,90 | 2,50

Como estende a interface IEnumerable<T>, e não uma classe, o mesmo Where serve para List, arrays, HashSet e qualquer coisa que dê para percorrer com foreach. Esse é o assunto da próxima aula, O que é LINQ.


Quando dá errado

CS1061: falta o using do namespace

Este código tem um erro de propósito.

decimal preco = 50m;
Console.WriteLine(preco.ComDesconto(10));

namespace Loja.Extensoes
{
    static class ExtensoesDePreco
    {
        public static decimal ComDesconto(this decimal preco, int porcento) =>
            preco - preco * porcento / 100;
    }
}
$ dotnet run namespace-erro.cs
/home/ana/cs/namespace-erro.cs(2,25): error CS1061: ‘decimal’ não contém uma definição para "ComDesconto" e não foi possível encontrar nenhum método de extensão "ComDesconto" que aceite um primeiro argumento do tipo ‘decimal’ (você está se esquecendo de usar uma diretiva ou uma referência de assembly?)

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que quer dizer: o decimal não tem ComDesconto, e nenhuma extensão visível tem. A própria mensagem dá a pista: "você está se esquecendo de usar uma diretiva" (a diretiva using).

Como consertar: acrescente using Loja.Extensoes; no topo do arquivo.

CS1061: esqueceu o this

Este código tem um erro de propósito.

Console.WriteLine(50m.ComDesconto(10));

static class ExtensoesDePreco
{
    public static decimal ComDesconto(decimal preco, int porcento) =>
        preco - preco * porcento / 100;
}
$ dotnet run semthis.cs
/home/ana/cs/semthis.cs(1,23): error CS1061: ‘decimal’ não contém uma definição para "ComDesconto" e não foi possível encontrar nenhum método de extensão "ComDesconto" que aceite um primeiro argumento do tipo ‘decimal’ (você está se esquecendo de usar uma diretiva ou uma referência de assembly?)

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que quer dizer: a mesma mensagem, por outro motivo: sem o this, o método é estático comum, não uma extensão.

Como consertar: ComDesconto(this decimal preco, int porcento), ou chame como método estático: ExtensoesDePreco.ComDesconto(50m, 10).

CS1106: a classe não é static

Este código tem um erro de propósito.

Console.WriteLine(50m.ComDesconto(10));

class ExtensoesDePreco
{
    public static decimal ComDesconto(this decimal preco, int porcento) =>
        preco - preco * porcento / 100;
}
$ dotnet run naoestatica.cs
/home/ana/cs/naoestatica.cs(3,7): error CS1106: O método de extensão deve ser definido em uma classe estática não genérica

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Como consertar: static class ExtensoesDePreco. Ela também não pode ser genérica (class Ext<T>) nem ficar dentro de outra classe.

CS9283: bloco extension fora de uma classe estática

Este código tem um erro de propósito.

Console.WriteLine(50m.EhCaro);

class ExtensoesDePreco
{
    extension(decimal preco)
    {
        public bool EhCaro => preco > 100m;
    }
}
$ dotnet run bloco-erro.cs
/home/ana/cs/bloco-erro.cs(5,5): error CS9283: As extensões devem ser declaradas em uma classe estática de nível superior, não genérica

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

Como consertar: a mesma regra dos métodos clássicos, com um código de erro novo: static class ExtensoesDePreco, no nível de cima do arquivo.

CS0122: a extensão não enxerga o que é privado

Este código tem um erro de propósito.

Produto p = new("Caderno", 18.90m);
Console.WriteLine(p.ComDesconto(10));

static class ExtensoesDeProduto
{
    public static decimal ComDesconto(this Produto p, int porcento) => p.precoBase * (100 - porcento) / 100;
}

class Produto(string nome, decimal preco)
{
    public string Nome { get; } = nome;
    private decimal precoBase = preco;
}
$ dotnet run privado.cs
/home/ana/cs/privado.cs(6,74): error CS0122: "Produto.precoBase" é inacessível devido ao seu nível de proteção

Ocorreu uma falha no build. Corrija os erros de build e execute novamente.

O que quer dizer: a extensão parece fazer parte do tipo, mas é um método de outra classe. Ela só enxerga o que é público (veja public, private e internal). É o "adesivo" do lado de fora da balança.

Como consertar: use o que o tipo mostra publicamente (uma propriedade public decimal Preco { get; }), ou, se o tipo é seu, escreva o método dentro dele.


Referência

CódigoO que faz
static class Ext { public static R M(this T x, …) { … } }Método de extensão clássico para o tipo T.
valor.M(…)Chama a extensão; o mesmo que Ext.M(valor, …).
this string? textoExtensão que aceita null (o método precisa tratar).
extension(T x) { public R M(…) => …; }Bloco do C# 14: método de extensão.
extension(T x) { public R P => …; }Bloco do C# 14: propriedade de extensão, valor.P.
extension(T) { public static R S => …; }Bloco do C# 14: membro estático, T.S.
using Namespace.DasExtensoes;Torna as extensões daquele namespace visíveis.

Exercícios

1. O ingrediente que falta

Complete o primeiro parâmetro para que Dobro seja uma extensão de int:

public static int Dobro() => n * 2;

2. Chamar a extensão

Com a extensão Resumir(this string texto, int tamanho), chame-a na variável nome para resumir em 10 caracteres:

3. A palavra do C# 14

Complete a abertura do bloco do C# 14 que estende string com o receptor chamado texto:

{ public bool Vazio => texto.Length == 0; }

4. preco.ComDesconto(10) é o mesmo que…

5. O tipo Produto tem um método Descricao(), e você cria uma extensão Descricao(this Produto p). Qual roda em p.Descricao()?

6. O que só os blocos extension do C# 14 permitem?

Missão final: no bloco extension, crie a propriedade EhBarato (verdadeira quando o preço é menor que 5m) e troque, no Console.WriteLine, p.EhCaro ? "caro" por p.EhBarato ? "barato". A saída deve ser R$ 2,50: barato, R$ 18,90: normal e R$ 1,75: barato.

decimal[] precos = [2.50m, 18.90m, 1.75m];

foreach (decimal p in precos)
{
    Console.WriteLine($"{p:C}: {(p.EhCaro ? "caro" : "normal")}");
}

static class ExtensoesDePreco
{
    extension(decimal preco)
    {
        public bool EhCaro => preco > 100m;
        // crie aqui a propriedade EhBarato (preço menor que 5m)
    }
}
decimal[] precos = [2.50m, 18.90m, 1.75m];

foreach (decimal p in precos)
{
    Console.WriteLine($"{p:C}: {(p.EhBarato ? "barato" : "normal")}");
}

static class ExtensoesDePreco
{
    extension(decimal preco)
    {
        public bool EhCaro => preco > 100m;
        public bool EhBarato => preco < 5m;
    }
}

Resumo

  • Um método de extensão "acrescenta" um método a um tipo que não é seu: static class + public static + this no primeiro parâmetro.
  • preco.ComDesconto(10) é só outro jeito de escrever ExtensoesDePreco.ComDesconto(preco, 10); nada muda dentro do tipo.
  • A extensão só enxerga o que é público (CS0122) e só aparece com o using do namespace dela (CS1061).
  • Um método do próprio tipo sempre vence uma extensão com o mesmo nome.
  • No C# 14, os blocos extension(decimal preco) { … } permitem também propriedades e membros estáticos de extensão.
  • O LINQ (Where, Select, OrderBy…) é um conjunto de métodos de extensão de IEnumerable<T>.

Próxima aula: O que é LINQ.