C# A Classe object
Você já reparou que todo objeto em C# tem um método ToString(), mesmo os das classes que você escreveu e nas quais nunca pôs esse método? É que, no C#, toda classe tem uma mãe, mesmo quando você não escreve : Mãe. Lá no alto da árvore de família de todos os tipos está a classe object. Nesta aula você vai conhecer o que ela dá de herança para todo mundo e aprender a sobrescrever os três métodos mais importantes: ToString, Equals e GetHashCode.
- Que toda classe herda de
object(o mesmo queSystem.Object). - O que o
ToString()mostra se você não fizer nada, e como trocar. - A diferença entre "o mesmo objeto" e "objetos com o mesmo conteúdo": o
Equalspadrão e o seu. - Por que quem sobrescreve
Equalstem de sobrescreverGetHashCode(o avisoCS0659). - O
GetType(), e os erros comuns: esquecer ooverride, errar o nome, esquecer o?.
Todos os programas desta aula rodaram com dotnet run arquivo.cs no .NET 10 da máquina da Ana, na pasta ~/cs.
A ideia: o kit de boas-vindas
Numa empresa, todo funcionário novo recebe o mesmo kit no primeiro dia: crachá, caneca e uma pasta. Não importa o cargo: o estagiário e a diretora recebem o kit. Depois, cada um personaliza o seu: põe uma foto no crachá, troca a caneca.
A classe object é esse kit. Todo tipo do C# recebe dela, de graça, alguns métodos:
| Método | O que faz | Pode trocar? |
|---|---|---|
ToString() | Devolve o objeto como texto | Sim (virtual) |
Equals(object? obj) | Diz se o objeto é "igual" a outro | Sim (virtual) |
GetHashCode() | Devolve um número que resume o objeto (para HashSet e Dictionary) | Sim (virtual) |
GetType() | Diz de que tipo o objeto é | Não |
Escrever class Produto é o mesmo que escrever class Produto : object. E object é só um apelido do C# para a classe System.Object do .NET, como string é apelido de System.String.
O ToString() que vem de fábrica
Produto p = new Produto("Café", 18.90m);
Console.WriteLine(p.ToString());
Console.WriteLine(p);
Console.WriteLine($"Comprei: {p}");
Console.WriteLine(p.GetType());
class Produto
{
public string Nome { get; }
public decimal Preco { get; }
public Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
}
- p.ToString()
- Chama o
ToString()herdado deobject. A classeProdutonão escreveu nenhum. - Console.WriteLine(p)
- Quando recebe um objeto, o
WriteLinechama oToString()dele por você. - $"Comprei: {p}"
- A interpolação também chama o
ToString(). - p.GetType()
- Devolve o tipo do objeto. Mostrado com
WriteLine, aparece o nome completo do tipo.
$ dotnet run padrao.cs Produto Produto Comprei: Produto Produto
O ToString() de fábrica só sabe o nome do tipo. Ele não conhece as suas propriedades. (Se a classe estivesse dentro de um namespace Loja, apareceria Loja.Produto.) Para mostrar algo útil, troque o método.
Sobrescrever o ToString()
Produto p = new Produto("Café", 18.90m);
Console.WriteLine(p);
Console.WriteLine($"Comprei: {p}");
class Produto
{
public string Nome { get; }
public decimal Preco { get; }
public Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
public override string ToString()
{
return $"{Nome} ({Preco:C})";
}
}
- public override string ToString()
- O mesmo
overridede virtual e override: oToString()deobjectévirtual, e oProdutocoloca a versão dele no lugar. Tem de serpublic, devolverstringe não receber nada. - return $"{Nome} ({Preco:C})";
- O texto que representa um produto: o nome e o preço em reais.
$ dotnet run tostring.cs Café (R$ 18,90) Comprei: Café (R$ 18,90)
Agora todo lugar que transforma um produto em texto usa a versão nova: o WriteLine, a interpolação, a concatenação com + e até a janela do depurador (veja Depuração). Sobrescrever o ToString() é um dos hábitos mais úteis em C#: deixa a saída e a depuração muito mais fáceis.
Missão: rode e veja Aluno duas vezes. Sobrescreva o ToString() da classe Aluno para devolver $"{Nome} (nota {Nota})". A saída deve ser Bia (nota 9,5) e Caio (nota 7).
Aluno[] turma = [new Aluno("Bia", 9.5), new Aluno("Caio", 7)];
foreach (Aluno a in turma)
{
Console.WriteLine(a);
}
class Aluno
{
public string Nome { get; }
public double Nota { get; }
public Aluno(string nome, double nota)
{
Nome = nome;
Nota = nota;
}
// escreva aqui o ToString
}
Aluno[] turma = [new Aluno("Bia", 9.5), new Aluno("Caio", 7)];
foreach (Aluno a in turma)
{
Console.WriteLine(a);
}
class Aluno
{
public string Nome { get; }
public double Nota { get; }
public Aluno(string nome, double nota)
{
Nome = nome;
Nota = nota;
}
public override string ToString() => $"{Nome} (nota {Nota})";
}
Dois erros comuns no ToString
Esquecer o override (public string ToString()): compila, com um aviso, e o WriteLine continua usando o de fábrica!
$ dotnet run semoverride.cs /home/ana/cs/semoverride.cs(16,19): warning CS0114: "Produto.ToString()" oculta o membro herdado "object.ToString()". Para que o membro atual substitua essa implementação, adicione a palavra-chave override. Caso contrário, adicione a palavra-chave new. Produto Comprei: Produto
Sem override, o método da Ana é um método novo, que só tem o mesmo nome. O WriteLine recebe um objeto e chama o ToString() de object, que não foi trocado. Leia sempre os avisos: este explica exatamente o que fazer.
Errar uma letra (public override string Tostring(), com o s minúsculo): o C# diferencia maiúsculas de minúsculas, e não existe nenhum Tostring na mãe para substituir.
$ dotnet run minuscula.cs /home/ana/cs/minuscula.cs(16,28): error CS0115: "Produto.Tostring()": não encontrado nenhum método adequado para substituição Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
O Equals que vem de fábrica: o mesmo objeto?
Dois pacotes de café da mesma marca, com o mesmo preço, são "iguais"? Depende da pergunta. São o mesmo pacote? Não: são dois pacotes na prateleira. Têm o mesmo conteúdo? Sim. Para uma classe, o C# responde, de fábrica, a primeira pergunta:
Produto a = new Produto("Café", 18.90m);
Produto b = new Produto("Café", 18.90m);
Produto c = a;
Console.WriteLine(a.Equals(b));
Console.WriteLine(a == b);
Console.WriteLine(a.Equals(c));
Console.WriteLine(a == c);
class Produto
{
public string Nome { get; }
public decimal Preco { get; }
public Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
}
$ dotnet run igual.cs False « a e b: dois objetos, mesmo conteúdo False True « a e c: o mesmo objeto True
- Produto a = new …; Produto b = new …;
- Dois
new= dois objetos na memória, cada um no seu lugar. - Produto c = a;
- Nenhum
new:cé outro nome para o mesmo objeto dea(a aula Valor × Referência explica isso a fundo). - a.Equals(b)
- O
Equalsdeobjectcompara referências: só dáTruese as duas variáveis apontam para o mesmo objeto. - a == b
- Para classes, o
==também compara referências (a menos que a classe redefina o operador; veja Sobrecarga de Operadores).
Muitas vezes é isso mesmo que você quer: duas contas bancárias com o mesmo saldo não são a mesma conta. Mas para coisas que são só "dados" (um produto do catálogo, uma data, um endereço), faz mais sentido comparar o conteúdo.
Sobrescrever o Equals: o mesmo conteúdo
Produto a = new Produto("Café", 18.90m);
Produto b = new Produto("Café", 18.90m);
Produto d = new Produto("Chá", 7.50m);
Console.WriteLine(a.Equals(b));
Console.WriteLine(a.Equals(d));
Console.WriteLine(a.Equals("Café"));
Console.WriteLine(a == b);
class Produto
{
public string Nome { get; }
public decimal Preco { get; }
public Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
public override bool Equals(object? obj)
{
return obj is Produto outro
&& Nome == outro.Nome
&& Preco == outro.Preco;
}
public override int GetHashCode()
{
return HashCode.Combine(Nome, Preco);
}
}
- public override bool Equals(object? obj)
- A assinatura tem de ser exatamente esta: devolve
boole recebe umobject?, porque dá para comparar um produto com qualquer coisa, até comnull(por isso o?; veja Referências Anuláveis). - obj is Produto outro
- Primeiro teste: o outro é um
Produto? Se for, ele passa a se chamaroutro, já com o tipoProduto. Se fornullou um texto, oisdáfalsee o resto nem é avaliado. - && Nome == outro.Nome && Preco == outro.Preco
- Depois, compara o que importa, propriedade por propriedade. Você decide o que torna dois produtos iguais.
- public override int GetHashCode()
- O companheiro obrigatório do
Equals. Ele tem a próxima seção inteira.
$ dotnet run equals.cs True « mesmo nome e mesmo preço False « o chá é outro produto False « um texto nunca é um Produto False « o == continua comparando referências
Repare na última linha: sobrescrever o Equals não muda o ==. São duas coisas separadas. Se você quer comparação por conteúdo em tudo (Equals, ==, ToString e GetHashCode) sem escrever nada disso, a ferramenta certa é o record.
GetHashCode: o resumo numérico
O GetHashCode() devolve um int que "resume" o objeto. O HashSet e o Dictionary (veja HashSet e Dictionary) usam esse número para achar as coisas rápido, como uma biblioteca que separa os livros em estantes pela primeira letra do título: para procurar Dom Casmurro, você vai direto à estante do "D" e só compara os livros de lá.
A regra de ouro:
Se a.Equals(b) é True, então a.GetHashCode() tem de ser igual a b.GetHashCode(). (O contrário não é exigido: objetos diferentes podem, por azar, ter o mesmo hash.) Se a regra for quebrada, o HashSet procura o objeto na "estante" errada e não o encontra.
O jeito mais fácil de cumprir a regra é passar as mesmas propriedades do Equals para o HashCode.Combine, que mistura os hashes delas num só: HashCode.Combine(Nome, Preco).
O compilador avisa
Se a Ana tirar o GetHashCode do exemplo acima:
$ dotnet run semhash.cs /home/ana/cs/semhash.cs(10,7): warning CS0659: "Produto" substitui Object.Equals(object o), mas não substitui Object.GetHashCode() True False False False
O programa roda, mas o aviso é sério. Veja o estrago num HashSet, que guarda cada coisa uma vez só:
Produto a = new Produto("Café", 18.90m);
Produto b = new Produto("Café", 18.90m);
Console.WriteLine(a.GetHashCode());
Console.WriteLine(b.GetHashCode());
Console.WriteLine(a.GetHashCode() == b.GetHashCode());
HashSet<Produto> carrinho = [a, b, new Produto("Chá", 7.50m)];
Console.WriteLine(carrinho.Count);
// … a classe Produto com Equals e GetHashCode, como em equals.cs
$ dotnet run hash.cs « com GetHashCode -1247727026 -1247727026 True 2 « o café repetido entrou uma vez só $ dotnet run hash.cs « de novo, sem mudar nada -141737905 -141737905 True 2 $ dotnet run hashsem.cs « sem GetHashCode /home/ana/cs/hashsem.cs(10,7): warning CS0659: "Produto" substitui Object.Equals(object o), mas não substitui Object.GetHashCode() 58225482 54267293 False 3 « o HashSet não percebeu que os cafés são iguais
Três lições nessa saída:
- Com
GetHashCode, os dois cafés têm o mesmo hash e oHashSetguarda só um (2itens). Sem ele, cada objeto tem um hash diferente, oHashSetnem chega a chamar oEqualse guarda os dois (3itens). - O número muda a cada execução. Os hashes de texto do .NET são sorteados toda vez que o programa começa (é uma proteção contra ataques). O seu número vai ser outro. Por isso, nunca grave um hash em arquivo nem dependa do valor dele: só compare hashes dentro da mesma execução.
- O aviso
CS0659só aparece quando o programa é compilado. Rodando de novo sem mudar o arquivo, odotnet runreaproveita a compilação anterior e não mostra o aviso outra vez. Não é porque o problema sumiu!
GetType() e o tipo object
Como object é a mãe de tudo, uma variável object aceita qualquer valor: um número, um texto, um produto. O GetType() conta o que há lá dentro:
object o = 42; Console.WriteLine(o.GetType().Name); Console.WriteLine(o);
$ dotnet run tipo.cs Int32 « o nome .NET do int 42
O .Name dá só o nome, sem o namespace (Int32 em vez de System.Int32). Guardar um int numa variável object tem um custo escondido, chamado boxing, assunto da aula Valor × Referência. Na maior parte do código, prefira o tipo certo (int, Produto) a object: o compilador só ajuda quando sabe o tipo.
Quando dá errado
$ dotnet run semhash.cs /home/ana/cs/semhash.cs(10,7): warning CS0659: "Produto" substitui Object.Equals(object o), mas não substitui Object.GetHashCode()
O que aconteceu: a classe trocou o Equals e ficou com o GetHashCode de fábrica. Objetos iguais terão hashes diferentes, e HashSet e Dictionary vão se confundir.
Como resolver: public override int GetHashCode() => HashCode.Combine(…); com as mesmas propriedades usadas no Equals.
$ dotnet run semnull.cs /home/ana/cs/semnull.cs(21,26): warning CS8765: A nulidade do tipo de parâmetro 'obj' não corresponde ao membro substituído (possivelmente devido a atributos de nulidade).
O que aconteceu: a Ana escreveu Equals(object obj), sem o ?. Mas o Equals de object aceita null, e o seu também tem de aceitar.
Como resolver: escreva Equals(object? obj). O is Produto outro já trata o null (dá false).
ToString: o método foi escrito, mas não é usado
$ dotnet run semoverride.cs /home/ana/cs/semoverride.cs(16,19): warning CS0114: "Produto.ToString()" oculta o membro herdado "object.ToString()". … Produto
O que aconteceu: faltou o override.
Como resolver: public override string ToString().
$ dotnet run minuscula.cs /home/ana/cs/minuscula.cs(16,28): error CS0115: "Produto.Tostring()": não encontrado nenhum método adequado para substituição
O que aconteceu: o override está num método que a mãe não tem: nome com letra errada (Tostring, equals), parâmetros diferentes (Equals(Produto p)) ou tipo de retorno diferente.
Como resolver: copie a assinatura exata: string ToString(), bool Equals(object? obj), int GetHashCode().
Referência
| Escrita | O que faz |
|---|---|
public override string ToString() | Como o objeto vira texto (WriteLine, $"…", +) |
public override bool Equals(object? obj) | Quando dois objetos são iguais (de fábrica: o mesmo objeto) |
obj is Produto outro && … | O começo típico de um Equals |
public override int GetHashCode() => HashCode.Combine(A, B); | Hash com as mesmas propriedades do Equals |
obj.GetType(), obj.GetType().Name | O tipo do objeto (não pode ser sobrescrito) |
object.ReferenceEquals(a, b) | "É o mesmo objeto?", mesmo que a classe tenha trocado o Equals |
a == b (classes) | Compara referências, a menos que o operador seja redefinido |
class X = class X : object | Toda classe herda de object (System.Object) |
Exercícios
1. A mãe de todos
De que classe toda classe do C# herda, mesmo sem escrever nada?
2. A assinatura do ToString
Complete a primeira linha do ToString de uma classe:
3. O companheiro do Equals
Complete o GetHashCode de uma classe cujo Equals compara Nome e Idade:
4. Produto não sobrescreve nada. O que mostra Console.WriteLine(new Produto("Café", 18.90m));?
5. a e b são dois new Produto("Café", 18.90m), e a classe não sobrescreve nada. Quanto dá a.Equals(b)?
6. O que é verdade sobre o GetHashCode()?
Missão final: o carrinho da Ana tem o mesmo café duas vezes, mas o programa acha que são produtos diferentes. Na classe Produto, sobrescreva o Equals(object? obj) (iguais = mesmo Nome e mesmo Preco) e o GetHashCode() (com HashCode.Combine). A saída deve ter a e b são iguais? True e Itens diferentes: 2, sem nenhum aviso. Dica: escreva primeiro só o Equals e rode, para ver o aviso e o resultado do HashSet.
Produto a = new Produto("Café", 18.90m);
Produto b = new Produto("Café", 18.90m);
Produto c = new Produto("Chá", 7.50m);
Console.WriteLine($"a e b são iguais? {a.Equals(b)}");
HashSet<Produto> itens = [a, b, c];
Console.WriteLine($"Itens diferentes: {itens.Count}");
class Produto
{
public string Nome { get; }
public decimal Preco { get; }
public Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
// escreva aqui o Equals e o GetHashCode
}
Produto a = new Produto("Café", 18.90m);
Produto b = new Produto("Café", 18.90m);
Produto c = new Produto("Chá", 7.50m);
Console.WriteLine($"a e b são iguais? {a.Equals(b)}");
HashSet<Produto> itens = [a, b, c];
Console.WriteLine($"Itens diferentes: {itens.Count}");
class Produto
{
public string Nome { get; }
public decimal Preco { get; }
public Produto(string nome, decimal preco)
{
Nome = nome;
Preco = preco;
}
public override bool Equals(object? obj)
{
return obj is Produto outro && Nome == outro.Nome && Preco == outro.Preco;
}
public override int GetHashCode() => HashCode.Combine(Nome, Preco);
}
$ dotnet run carrinho.cs « a solução a e b são iguais? True Itens diferentes: 2
Resumo
- Toda classe herda de
object(System.Object) e ganhaToString,Equals,GetHashCodeeGetType. - O
ToString()de fábrica mostra só o nome do tipo. Sobrescreva compublic override string ToString(); sem ooverride, vem o avisoCS0114e o método não é usado. - O
Equalsde fábrica (e o==das classes) pergunta "é o mesmo objeto?". Para comparar o conteúdo, sobrescrevaEquals(object? obj)comobj is Produto outro && …. - Quem troca o
Equalstroca oGetHashCode(CS0659), comHashCode.Combinedas mesmas propriedades. Sem isso,HashSeteDictionarynão acham os iguais. - O valor do hash muda a cada execução: nunca dependa dele. Para ter tudo isso pronto, use um record. Próxima aula: Valor × Referência.