C# Sobrecarga de Operadores
O + sabe somar números (2 + 3) e juntar textos ("Olá, " + "Ana"). Mas, se a Ana criar um tipo Dinheiro para a lojinha, caderno + caneta não compila: o C# não sabe o que é "somar dois Dinheiro". Sobrecarregar um operador é ensinar isso a ele. Você escreve um método especial chamado operator +, e a partir daí o + funciona com o seu tipo como se fosse nativo.
- Como escrever
public static … operator +(…), linha por linha, e por que sem ele dáCS0019. - Operadores com tipos diferentes dos dois lados (
Dinheiro * int) e operadores de um lado só (-x). - Por que
<e>,==e!=vêm sempre em pares (CS0216), e o que oEqualse oGetHashCodetêm a ver com isso (CS0660,CS0661). - Conversões
impliciteexplicit: ensinar o(double)xao seu tipo. - O
+=que vem de graça e ooperator +=de instância do C# 14. - Quando não vale a pena sobrecarregar.
Esta aula usa structs, records, static e sobrecarga de métodos (vários métodos com o mesmo nome e parâmetros diferentes). Os exemplos rodaram com dotnet run no .NET 10 da máquina da Ana, na pasta ~/cs.
A ideia antes do código
Na escola, você aprendeu a somar números. Depois aprendeu a "somar" horas: 1h50 + 0h20 dá 2h10, e não 1h70. O sinal é o mesmo +, mas a regra muda conforme o que está sendo somado. Ninguém acha isso estranho, porque o significado continua óbvio: juntar duas quantidades.
Em C# é igual. O + já tem regras para int, double, decimal e string. Quando você cria um tipo, pode acrescentar uma regra nova para ele. "Sobrecarga" é a mesma palavra da sobrecarga de métodos: um nome (aqui, um símbolo) com várias versões, e o compilador escolhe pela lista de tipos.
1. O primeiro operador: +
Dinheiro caderno = new(15.90m);
Dinheiro caneta = new(2.50m);
Dinheiro total = caderno + caneta;
Console.WriteLine(total);
Console.WriteLine(caneta * 3);
Console.WriteLine(total - caneta);
struct Dinheiro
{
public decimal Valor { get; }
public Dinheiro(decimal valor) => Valor = valor;
public static Dinheiro operator +(Dinheiro a, Dinheiro b) => new(a.Valor + b.Valor);
public static Dinheiro operator -(Dinheiro a, Dinheiro b) => new(a.Valor - b.Valor);
public static Dinheiro operator *(Dinheiro a, int vezes) => new(a.Valor * vezes);
public override string ToString() => $"R$ {Valor:N2}";
}
$ dotnet run dinheiro.cs R$ 18,40 R$ 7,50 R$ 15,90
Leia a linha do + por partes:
- public
- Todo operador é público: quem usa o tipo precisa poder usar o
+. - static
- E é estático: ele não pertence a um
Dinheiroespecífico, e sim ao tipo. Recebe os dois lados como parâmetros. - Dinheiro
- O tipo do resultado: somar dois
Dinheirodá outroDinheiro. - operator +
- O "nome" do método: a palavra
operatore o símbolo que você está ensinando. - (Dinheiro a, Dinheiro b)
- Os dois operandos:
aé o que fica à esquerda do+,bo da direita. - => new(a.Valor + b.Valor);
- O corpo: soma os
decimalde dentro (com o+que o C# já conhece) e embrulha numDinheironovo.
Quando o compilador vê caderno + caneta, ele procura um operator + que aceite (Dinheiro, Dinheiro), acha o seu e troca a expressão por uma chamada a ele. Os operandos não mudam: o resultado é um valor novo, como em 2 + 3, que não estraga o 2.
struct?
Dinheiro é um valor pequeno, que se copia, como um decimal. Tipos assim combinam com struct ou record struct. Os operadores funcionam igual em class; só o == tem uma diferença, que aparece na seção 3.
Tipos diferentes dos dois lados
O operator * acima recebe (Dinheiro a, int vezes): o lado esquerdo é dinheiro e o direito é um inteiro. A ordem dos parâmetros é a ordem da expressão. Por isso caneta * 3 funciona, mas 3 * caneta não:
Dinheiro caneta = new(2.50m);
Console.WriteLine(3 * caneta);
struct Dinheiro
{
public decimal Valor { get; }
public Dinheiro(decimal valor) => Valor = valor;
public static Dinheiro operator *(Dinheiro a, int vezes) => new(a.Valor * vezes);
public override string ToString() => $"R$ {Valor:N2}";
}
$ dotnet run vezes.cs /home/ana/cs/vezes.cs(2,19): error CS0019: O operador "*" não pode ser aplicado a operandos dos tipos "int" e "Dinheiro" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Para aceitar as duas ordens, escreva as duas versões. A segunda pode simplesmente chamar a primeira:
public static Dinheiro operator *(Dinheiro a, int vezes) => new(a.Valor * vezes);
public static Dinheiro operator *(int vezes, Dinheiro a) => a * vezes;
Regra do C#: pelo menos um dos parâmetros tem de ser do tipo onde o operador está declarado. Você não pode, de dentro do Dinheiro, mudar o que int + int faz.
Missão: o Vetor já sabe se multiplicar por um número. Escreva o operator + que soma dois vetores (X com X, Y com Y), para que a + b mostre (4, 6).
Vetor a = new(1, 2);
Vetor b = new(3, 4);
// Console.WriteLine(a + b); // tire o // quando o operador existir
Console.WriteLine(a * 10);
readonly record struct Vetor(int X, int Y)
{
// escreva aqui: public static Vetor operator +(Vetor a, Vetor b) => ...
public static Vetor operator *(Vetor v, int k) => new(v.X * k, v.Y * k);
public override string ToString() => $"({X}, {Y})";
}
Vetor a = new(1, 2);
Vetor b = new(3, 4);
Console.WriteLine(a + b);
Console.WriteLine(a * 10);
readonly record struct Vetor(int X, int Y)
{
public static Vetor operator +(Vetor a, Vetor b) => new(a.X + b.X, a.Y + b.Y);
public static Vetor operator *(Vetor v, int k) => new(v.X * k, v.Y * k);
public override string ToString() => $"({X}, {Y})";
}
2. Operadores de um lado só
Alguns operadores têm um operando: o - de "negativo" (-x), o !, o ++ e o --. São os operadores unários, e o método recebe um parâmetro só. O C# distingue o - de subtrair do - de negar pelo número de parâmetros:
public static Dinheiro operator -(Dinheiro a, Dinheiro b) => new(a.Valor - b.Valor); // a - b
public static Dinheiro operator -(Dinheiro a) => new(-a.Valor); // -a
O exemplo completo está na próxima seção, junto com as comparações.
3. Comparar: ==, < e >
Os operadores de comparação devolvem bool e têm uma regra extra: vêm em pares. Quem define < tem de definir >; quem define <=, >=; quem define ==, !=. Faz sentido: seria muito estranho um tipo que sabe dizer "menor que" e não sabe dizer "maior que".
E o ==? Num record struct (ou num record), o C# já escreve o ==, o !=, o Equals e o GetHashCode para você, comparando todos os campos. Então basta acrescentar o que falta:
Dinheiro carteira = new(50.00m);
Dinheiro lanche = new(12.90m);
Dinheiro livro = new(59.90m);
Console.WriteLine(carteira == new Dinheiro(50.00m));
Console.WriteLine(lanche < carteira);
Console.WriteLine(livro > carteira);
Console.WriteLine(livro <= carteira);
carteira -= lanche;
Console.WriteLine(carteira);
Console.WriteLine(-lanche);
readonly record struct Dinheiro(decimal Valor)
{
public static Dinheiro operator +(Dinheiro a, Dinheiro b) => new(a.Valor + b.Valor);
public static Dinheiro operator -(Dinheiro a, Dinheiro b) => new(a.Valor - b.Valor);
public static Dinheiro operator -(Dinheiro a) => new(-a.Valor);
public static bool operator <(Dinheiro a, Dinheiro b) => a.Valor < b.Valor;
public static bool operator >(Dinheiro a, Dinheiro b) => a.Valor > b.Valor;
public static bool operator <=(Dinheiro a, Dinheiro b) => a.Valor <= b.Valor;
public static bool operator >=(Dinheiro a, Dinheiro b) => a.Valor >= b.Valor;
public override string ToString() => $"R$ {Valor:N2}";
}
$ dotnet run comparar.cs True True True False R$ 37,10 R$ -12,90
| Linha | O que acontece |
|---|---|
carteira == new Dinheiro(50.00m) | O == gerado pelo record struct: os dois têm Valor 50,00, então True. |
lanche < carteira | Chama o seu operator <: 12,90 < 50,00. |
carteira -= lanche; | Você não escreveu -=, e funcionou: o C# transforma em carteira = carteira - lanche; e usa o seu operator - de dois lados. |
-lanche | O operator - de um lado só. |
== em class: o cuidado extra
Numa class comum, o == que vem de fábrica compara referências: só dá true se as duas variáveis apontam para o mesmo objeto (veja Valor × Referência). Um record compara os valores:
var x = new Classe(1);
var y = new Classe(1);
Console.WriteLine(x == y);
var r1 = new Reg(1);
var r2 = new Reg(1);
Console.WriteLine(r1 == r2);
class Classe(int n) { public int N = n; }
record Reg(int N);
$ dotnet run igualdade.cs False True
Se você escrever o == à mão, numa class ou num struct, o compilador lembra que o == e o método Equals precisam concordar (e o GetHashCode também, porque o Dictionary e o HashSet usam os dois). São os avisos CS0660 e CS0661, em "Quando dá errado". O caminho mais simples: se o que você quer é "iguais quando os dados são iguais", use um record ou record struct e deixe o C# escrever tudo.
Missão: a Ana anota quanto tempo estudou por dia. O Tempo já sabe se somar (e o construtor transforma 70 minutos em 1h10). Escreva o operator > (compare o total de minutos: Horas * 60 + Minutos) e tire o // da última linha. Lembre: o > precisa do par <, que já está pronto.
Tempo segunda = new(1, 50);
Tempo terca = new(0, 20);
Tempo quarta = new(2, 45);
Console.WriteLine(segunda + terca);
Console.WriteLine(segunda + terca + quarta);
// Console.WriteLine(segunda > terca);
readonly record struct Tempo
{
public int Horas { get; }
public int Minutos { get; }
public Tempo(int horas, int minutos)
{
Horas = horas + minutos / 60;
Minutos = minutos % 60;
}
public static Tempo operator +(Tempo a, Tempo b) => new(a.Horas + b.Horas, a.Minutos + b.Minutos);
// escreva o operator > aqui
// public static bool operator <(Tempo a, Tempo b) => b > a;
public override string ToString() => $"{Horas}h{Minutos:D2}";
}
Tempo segunda = new(1, 50);
Tempo terca = new(0, 20);
Tempo quarta = new(2, 45);
Console.WriteLine(segunda + terca);
Console.WriteLine(segunda + terca + quarta);
Console.WriteLine(segunda > terca);
readonly record struct Tempo
{
public int Horas { get; }
public int Minutos { get; }
public Tempo(int horas, int minutos)
{
Horas = horas + minutos / 60;
Minutos = minutos % 60;
}
public static Tempo operator +(Tempo a, Tempo b) => new(a.Horas + b.Horas, a.Minutos + b.Minutos);
public static bool operator >(Tempo a, Tempo b) => a.Horas * 60 + a.Minutos > b.Horas * 60 + b.Minutos;
public static bool operator <(Tempo a, Tempo b) => b > a;
public override string ToString() => $"{Horas}h{Minutos:D2}";
}
Com o operador pronto, o programa mostra:
$ dotnet run tempo.cs 2h10 4h55 True
4. Conversões: implicit e explicit
O cast também é um operador. Você pode ensinar o seu tipo a virar outro, e outro a virar o seu:
implicit(implícita): a conversão acontece sozinha, sem cast. Use só quando ela nunca perde informação e nunca falha, comoint→long.explicit(explícita): exige o cast(Tipo)x, para quem lê o código ver que há uma conversão ali.
Celsius c = 25.0;
Console.WriteLine(c);
double d = (double)c;
Console.WriteLine(d);
Fahrenheit f = (Fahrenheit)c;
Console.WriteLine(f);
Console.WriteLine(-c);
readonly record struct Celsius(double Graus)
{
public static implicit operator Celsius(double graus) => new(graus);
public static explicit operator double(Celsius c) => c.Graus;
public static explicit operator Fahrenheit(Celsius c) => new(c.Graus * 9 / 5 + 32);
public static Celsius operator -(Celsius c) => new(-c.Graus);
public override string ToString() => $"{Graus} °C";
}
readonly record struct Fahrenheit(double Graus)
{
public override string ToString() => $"{Graus} °F";
}
$ dotnet run graus.cs 25 °C 25 77 °F -25 °C
- public static implicit operator Celsius(double graus)
- "Um
doublepode virarCelsiussozinho." Por issoCelsius c = 25.0;compila sem cast. Repare: aqui não há tipo de retorno separado; o nome do operador é o tipo de destino. - public static explicit operator double(Celsius c)
- "Um
Celsiusviradouble, mas só com cast":(double)c. - explicit operator Fahrenheit(Celsius c)
- Conversão entre dois tipos seus: 25 °C × 9 / 5 + 32 = 77 °F.
5. += e ++ de instância C# 14
Você viu que carteira -= lanche funcionou de graça: o C# usa o seu operator - e cria um valor novo. Para números e valores pequenos, é o ideal. Mas imagine um objeto grande, como uma lista com milhares de itens: criar uma cópia nova a cada += sai caro. O C# 14 deixa uma class definir o += (e o ++, o --, o -=…) como um método de instância, que altera o próprio objeto:
Contador c = new();
c += 5;
c++;
Console.WriteLine(c.Total);
Dinheiro d = new(10m);
d += new Dinheiro(2m);
Console.WriteLine(d.Valor);
class Contador
{
public int Total { get; private set; }
public void operator +=(int n)
{
Console.WriteLine($"somando {n} no mesmo objeto");
Total += n;
}
public void operator ++() => Total++;
}
struct Dinheiro
{
public decimal Valor { get; }
public Dinheiro(decimal valor) => Valor = valor;
public static Dinheiro operator +(Dinheiro a, Dinheiro b) => new(a.Valor + b.Valor);
}
$ dotnet run contador.cs somando 5 no mesmo objeto 6 12
| Trecho | O que é |
|---|---|
public void operator +=(int n) | Novo no C# 14: sem static, devolve void e recebe só o lado direito. O lado esquerdo é o próprio objeto (this), que é alterado. |
public void operator ++() | O ++ de instância: sem parâmetros, muda o objeto. |
d += new Dinheiro(2m) | O Dinheiro não tem operator +=; o C# usa o operator + estático, como sempre fez. |
Até o C# 13, +=, -= e companhia não podiam ser sobrecarregados: eram sempre traduzidos para x = x + y. Quase todo código que você vai encontrar ainda é assim, e para tipos pequenos (dinheiro, vetores, datas) continua sendo o certo. O operator += de instância é para casos de desempenho, em tipos grandes e mutáveis.
Quais operadores dá para sobrecarregar
| Tipo | Operadores | Observação |
|---|---|---|
| Unários | +x, -x, !, ~, ++, --, true, false | Um parâmetro |
| Aritméticos | +, -, *, /, % | Dois parâmetros |
| Bits | &, |, ^, <<, >>, >>> | Dois parâmetros |
| Comparação | == e !=, < e >, <= e >= | Em pares |
| Conversões | implicit operator T, explicit operator T | O nome é o tipo de destino |
| Atribuição composta C# 14 | +=, -=, *=, /=… e ++/-- de instância | Sem static, devolvem void |
| Não dá | &&, ||, =, ., ?:, ??, is, new, [] | O [] tem o seu próprio recurso: os indexadores |
Quando usar (e quando não)
| Bom uso | Mau uso |
|---|---|
Dinheiro + Dinheiro, Vetor * 2, Tempo + Tempo, Fracao / Fracao: o significado é o da matemática. | carrinho + produto para "adicionar ao carrinho": prefira um método com nome, carrinho.Adicionar(produto). |
a < b quando existe uma ordem natural (valores, datas, versões). | < em Cliente: menor em quê? Nome? Idade? Use um IComparer. |
Conversão implicit que nunca perde nada. | implicit que pode falhar ou arredondar: use explicit ou um método Converter…. |
A regra de ouro: quem lê a + b tem de adivinhar o que acontece sem abrir o código do operador.
Quando dá errado
Usar um operador que o tipo não tem
Dinheiro a = new(10m);
Dinheiro b = new(5m);
Console.WriteLine(a + b);
struct Dinheiro
{
public decimal Valor { get; }
public Dinheiro(decimal valor) => Valor = valor;
public override string ToString() => $"R$ {Valor:N2}";
}
$ dotnet run semop.cs /home/ana/cs/semop.cs(3,19): error CS0019: O operador "+" não pode ser aplicado a operandos dos tipos "Dinheiro" e "Dinheiro" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: "o operador + não pode ser aplicado a dois Dinheiro" (Operator '+' cannot be applied to operands of type…). Não existe um operator + com esses dois tipos, nessa ordem. Como consertar: escreva public static Dinheiro operator +(Dinheiro a, Dinheiro b). Se o erro fala de "int" e "Dinheiro", falta a versão com os parâmetros na outra ordem.
Esquecer o static
Dinheiro a = new(10m);
Console.WriteLine(a + a);
struct Dinheiro
{
public decimal Valor { get; }
public Dinheiro(decimal valor) => Valor = valor;
public Dinheiro operator +(Dinheiro a, Dinheiro b) => new(a.Valor + b.Valor);
}
$ dotnet run semstatic.cs /home/ana/cs/semstatic.cs(7,30): error CS0558: O operador definido pelo usuário "Dinheiro.operator +(Dinheiro, Dinheiro)" deve ser declarado como static e public Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: o operator + de dois lados precisa ser public static. Como consertar: public static Dinheiro operator +(…). (Só os +=, ++ etc. de instância do C# 14 ficam sem static.)
Definir só metade do par
Dinheiro a = new(10m);
Console.WriteLine(a < new Dinheiro(20m));
struct Dinheiro
{
public decimal Valor { get; }
public Dinheiro(decimal valor) => Valor = valor;
public static bool operator <(Dinheiro a, Dinheiro b) => a.Valor < b.Valor;
}
$ dotnet run par.cs /home/ana/cs/par.cs(7,33): error CS0216: O operador "Dinheiro.operator <(Dinheiro, Dinheiro)" requer que um operador correspondente ">" também seja definido Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O que quer dizer: quem define < precisa definir >. Como consertar: acrescente o par. Um truque para não repetir a lógica: operator >(a, b) => b < a;.
== escrito à mão, sem Equals e GetHashCode
Dinheiro a = new(10m);
Console.WriteLine(a == new Dinheiro(10m));
struct Dinheiro
{
public decimal Valor { get; }
public Dinheiro(decimal valor) => Valor = valor;
public static bool operator ==(Dinheiro a, Dinheiro b) => a.Valor == b.Valor;
public static bool operator !=(Dinheiro a, Dinheiro b) => !(a == b);
}
$ dotnet run igual.cs /home/ana/cs/igual.cs(3,8): warning CS0660: "Dinheiro" define o operador = = ou operador !=, mas não substitui Object.Equals(object o) /home/ana/cs/igual.cs(3,8): warning CS0661: "Dinheiro" define o operador = = ou operador !=, mas não substitui Object.GetHashCode() True
O que quer dizer: compilou e rodou (são avisos), mas agora a == b e a.Equals(b) podem dar respostas diferentes, e um HashSet<Dinheiro> pode se perder. (O "= =" separado é da tradução; em inglês: defines operator == or operator != but does not override Object.Equals(object o).) Como consertar: o mais simples é trocar struct por readonly record struct Dinheiro(decimal Valor) e apagar os dois operadores: o record já traz ==, !=, Equals e GetHashCode combinando. Veja A Classe object para escrever o Equals à mão.
Tentar sobrecarregar o que não pode
public static Caixa operator &&(Caixa a, Caixa b) dá error CS1020: Operador binário que pode ser sobrecarregado é esperado. Como consertar: o && e o || não são sobrecarregáveis; sobrecarregue o & e o | (e, para o && funcionar, também true e false), ou, melhor, use um método com nome.
Referência
| Forma | Exemplo |
|---|---|
| Binário | public static T operator +(T a, T b) => …; |
| Tipos misturados | public static T operator *(T a, int k) e (int k, T a) |
| Unário | public static T operator -(T a) => …; |
| Comparação (par) | public static bool operator <(T a, T b) + operator > |
| Igualdade | Use record/record struct; à mão: ==, !=, Equals, GetHashCode |
| Conversão automática | public static implicit operator T(double x) |
| Conversão com cast | public static explicit operator double(T x) |
+= de graça | Vem do operator +: x += y é x = x + y |
+= de instância C# 14 | public void operator +=(int n) { … } |
++ de instância C# 14 | public void operator ++() { … } |
Exercícios
1. Complete a declaração do operador de soma:
2. Que palavra faz a conversão acontecer sem cast?
3. Você definiu operator <=. Qual operador o compilador exige junto?
4. O Dinheiro só tem operator +. O que acontece com carteira += lanche;?
5. Com operator *(Dinheiro a, int vezes), por que 3 * caneta dá CS0019?
6. Qual destes é um bom candidato a sobrecarga de operador?
Missão final: o carrinho da Ana tem 2 cadernos de R$ 8,90 e um estojo de R$ 6,00. Escreva dois operadores no Dinheiro: operator +(Dinheiro, Dinheiro) e operator *(Dinheiro, decimal) (o decimal serve tanto para "2 cadernos" quanto para "10% de desconto", 0.10m). Depois tire os //. A saída deve ser Total: R$ 23,80, Desconto: R$ 2,38 e A pagar: R$ 21,42.
Dinheiro caderno = new(8.90m);
Dinheiro estojo = new(6.00m);
// Dinheiro total = caderno * 2 + estojo;
// Dinheiro desconto = total * 0.10m;
// Console.WriteLine($"Total: {total}");
// Console.WriteLine($"Desconto: {desconto}");
// Console.WriteLine($"A pagar: {total - desconto}");
readonly record struct Dinheiro(decimal Valor)
{
// escreva o operator + e o operator * aqui
public static Dinheiro operator -(Dinheiro a, Dinheiro b) => new(a.Valor - b.Valor);
public override string ToString() => $"R$ {Valor:N2}";
}
Dinheiro caderno = new(8.90m);
Dinheiro estojo = new(6.00m);
Dinheiro total = caderno * 2 + estojo;
Dinheiro desconto = total * 0.10m;
Console.WriteLine($"Total: {total}");
Console.WriteLine($"Desconto: {desconto}");
Console.WriteLine($"A pagar: {total - desconto}");
readonly record struct Dinheiro(decimal Valor)
{
public static Dinheiro operator +(Dinheiro a, Dinheiro b) => new(a.Valor + b.Valor);
public static Dinheiro operator *(Dinheiro a, decimal fator) => new(a.Valor * fator);
public static Dinheiro operator -(Dinheiro a, Dinheiro b) => new(a.Valor - b.Valor);
public override string ToString() => $"R$ {Valor:N2}";
}
Repare que caderno * 2 funcionou com um operator * que pede decimal: o 2 é int, e o C# converte int para decimal sozinho (é uma conversão implícita que já vem na linguagem). A solução, na máquina da Ana:
$ dotnet run carrinho.cs Total: R$ 23,80 Desconto: R$ 2,38 A pagar: R$ 21,42
Resumo
- Sobrecarregar um operador é escrever
public static Resultado operator +(Esquerda a, Direita b). Sem ele,a + bdáCS0019; sempublic static,CS0558. - A ordem dos parâmetros é a ordem da expressão:
Dinheiro * inteint * Dinheirosão dois operadores. - Comparações vêm em pares (
</>,<=/>=,==/!=; senão,CS0216). O==à mão pedeEqualseGetHashCode(CS0660,CS0661); umrecordjá traz tudo. implicit operatorconverte sozinho;explicit operatorexige o cast.x += yvem de graça dooperator +. No C# 14, uma classe pode definirpublic void operator +=(…)eoperator ++()de instância, que alteram o próprio objeto.- Use operadores só quando o significado for óbvio, como na matemática. Para o resto, métodos com nome.
Próxima aula: Indexadores, para o seu tipo aceitar colchetes: agenda["Ana"].