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.

Você vai aprender
  • Que toda classe herda de object (o mesmo que System.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 Equals padrão e o seu.
  • Por que quem sobrescreve Equals tem de sobrescrever GetHashCode (o aviso CS0659).
  • O GetType(), e os erros comuns: esquecer o override, 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étodoO que fazPode trocar?
ToString()Devolve o objeto como textoSim (virtual)
Equals(object? obj)Diz se o objeto é "igual" a outroSim (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 de object. A classe Produto não escreveu nenhum.
Console.WriteLine(p)
Quando recebe um objeto, o WriteLine chama o ToString() 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 override de virtual e override: o ToString() de object é virtual, e o Produto coloca a versão dele no lugar. Tem de ser public, devolver string e 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 de a (a aula Valor × Referência explica isso a fundo).
a.Equals(b)
O Equals de object compara referências: só dá True se 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 bool e recebe um object?, porque dá para comparar um produto com qualquer coisa, até com null (por isso o ?; veja Referências Anuláveis).
obj is Produto outro
Primeiro teste: o outro é um Produto? Se for, ele passa a se chamar outro, já com o tipo Produto. Se for null ou um texto, o is dá false e 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:

Objetos iguais têm de ter o mesmo hash

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:

  1. Com GetHashCode, os dois cafés têm o mesmo hash e o HashSet guarda só um (2 itens). Sem ele, cada objeto tem um hash diferente, o HashSet nem chega a chamar o Equals e guarda os dois (3 itens).
  2. 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.
  3. O aviso CS0659 só aparece quando o programa é compilado. Rodando de novo sem mudar o arquivo, o dotnet run reaproveita 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

CS0659: "… substitui Object.Equals(object o), mas não substitui Object.GetHashCode()"
$ 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.

CS8765: "A nulidade do tipo de parâmetro 'obj' não corresponde ao membro substituído"
$ 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).

CS0114 no 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().

CS0115: "não encontrado nenhum método adequado para substituição"
$ 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

EscritaO 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().NameO 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 : objectToda 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:

public ToString()

3. O companheiro do Equals

Complete o GetHashCode de uma classe cujo Equals compara Nome e Idade:

public override int GetHashCode() => ;

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 ganha ToString, Equals, GetHashCode e GetType.
  • O ToString() de fábrica mostra só o nome do tipo. Sobrescreva com public override string ToString(); sem o override, vem o aviso CS0114 e o método não é usado.
  • O Equals de fábrica (e o == das classes) pergunta "é o mesmo objeto?". Para comparar o conteúdo, sobrescreva Equals(object? obj) com obj is Produto outro && ….
  • Quem troca o Equals troca o GetHashCode (CS0659), com HashCode.Combine das mesmas propriedades. Sem isso, HashSet e Dictionary nã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.