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.
- Criar um método de extensão com
static classethis. - Que
preco.ComDesconto(10)eExtensoesDePreco.ComDesconto(preco, 10)são a mesma chamada. - Extensões para
stringe paraList<Produto>, inclusive uma que aceitanull. - O
usingque faz a extensão aparecer, e quem vence quando o tipo já tem um método com o mesmo nome. - Os blocos
extensiondo C# 14: propriedades e membros estáticos de extensão. - Por que o
Wheredo LINQ aparece em qualquer lista. - Os erros reais:
CS1061,CS1106,CS9283eCS0122.
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
thisantes do primeiro parâmetro é o que faz a mágica: diz "este método estende o tipodecimal". Na chamadapreco.ComDesconto(10), o valor antes do ponto vira opreco. - int porcento
- Os outros parâmetros são normais e vão entre os parênteses: o
10deComDesconto(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..]).EstaVazioestendestring?e funciona até comnull. Um método de verdade chamado numnulldariaNullReferenceException; a extensão, não, porque é só a chamadaExtensoesDeTexto.EstaVazio(null), e o método trata onull. 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. Oprecoé o receptor: o valor antes do ponto. Ele vale para todos os membros do bloco, e por isso nenhum deles repete othis decimal preco. - public decimal ComDesconto(int porcento)
- Um método de extensão, escrito como um método comum (sem
statice semthis). 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étodoEhCaro(). - extension(decimal)
- Um bloco sem nome de receptor: os membros dele são
statice se chamam pelo nome do tipo,decimal.Freteedecimal.ComFrete(20m), como se fossem do própriodecimal.
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) | Sim | Sim |
Uma propriedade: preco.EhCaro | Não | Sim |
Um membro estático: decimal.Frete | Não | Sim |
| Funcionar em versões antigas do C# | Sim (desde o C# 3) | Não: só C# 14 / .NET 10 |
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
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.
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).
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.
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.
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ódigo | O 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? texto | Extensã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:
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:
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+thisno primeiro parâmetro. preco.ComDesconto(10)é só outro jeito de escreverExtensoesDePreco.ComDesconto(preco, 10); nada muda dentro do tipo.- A extensão só enxerga o que é público (
CS0122) e só aparece com ousingdo 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 deIEnumerable<T>.
Próxima aula: O que é LINQ.