Comparações de igualdade em C#

Sugestão

Este artigo faz parte da secção Fundamentos para programadores que já conhecem pelo menos uma linguagem de programação e estão a aprender C#. Se és novo na programação, começa primeiro pelos tutoriais para começar .

Vem de outra língua? Em Java, == nos objetos e no JavaScript === nos objetos testam a identidade, não o conteúdo. As classes de C# funcionam da mesma forma por defeito. Em Python, == chama __eq__ e testa o conteúdo por defeito, de forma semelhante à maneira como os records de C# são comparados. As estruturas C# também comparam por valor quando chamas Equals.

C# distingue dois tipos de igualdade. Igualdade de valor significa que duas instâncias são iguais quando os seus dados coincidem. Igualdade de referência significa que duas variáveis são iguais apenas quando apontam para o mesmo objeto na memória. Esta condição também é chamada identidade. Os tipos de valor normalmente comparam dados, e os tipos de referência comparam normalmente identidade. Os autores de tipos podem alterar esses valores predefinidos, mas esse modelo mental evita bugs subtis em que dois objetos que parecem idênticos não são considerados iguais, ou em que uma mutação por intermédio de uma variável altera silenciosamente aquilo a que outra variável acede.

Tipos de valor, tipos de referência e predefinições de igualdade

Cada tipo em C# é ou um tipo de valor ou um tipo de referência. Um tipo de valor mantém os seus dados diretamente na variável. Um tipo de referência contém uma referência a um objeto. Quando atribui uma variável do tipo referência a outra variável, ambas as variáveis referem-se ao mesmo objeto. Para mais informações sobre tipos de valor e tipos de referência, veja Visão Geral do Sistema de Tipos.

O comportamento predefinido de igualdade geralmente segue a categoria de tipo:

  • Tipos numéricos incorporados e enums são tipos de valor. Duas int variáveis são iguais quando os seus valores numéricos coincidem.
  • Structs são tipos de valor. Um struct simples usa igualdade de valores quando chama Equals.
  • Tuplas são tipos de valor. Duas tuplas são iguais quando todos os seus valores de elementos coincidem.
  • As classes são tipos de referência. Uma classe simples usa igualdade de referência, por isso == testa Equals se duas variáveis apontam para o mesmo objeto.

Uma classe usa igualdade de referência. Dois objetos separados com os mesmos dados não são iguais, mas duas variáveis que se referem ao mesmo objeto são iguais:

var order1 = new Order(42, "Shoes");
var order2 = new Order(42, "Shoes");

Console.WriteLine(order1 == order2);               // => False
Console.WriteLine(order1.Equals(order2));          // => False
Console.WriteLine(ReferenceEquals(order1, order2)); // => False

Order order3 = order1;
Console.WriteLine(order1 == order3);               // => True

A struct indica igualdade de valores através de Equals. Duas instâncias de struct são iguais quando os seus campos coincidem:

var pt1 = new Point(3, 4);
var pt2 = new Point(3, 4);

Console.WriteLine(pt1.Equals(pt2)); // => True

Estruturas simples não têm um operador predefinido == . Escrever p1 == p2 numa estrutura simples compila apenas se a estrutura declarar a sua própria operator ==. Se precisar de comparações de operadores para uma struct, defina == e != como par e mantenha-os consistentes com Equals e GetHashCode.

As tuplas também são tipos de valor. Duas tuplas são iguais quando todos os valores dos elementos coincidem. Os nomes dos elementos numa tupla nomeada são uma conveniência em tempo de compilação e não são considerados durante a comparação. Só as posições e valores importam:

var t1 = (Name: "Grace", Role: "Engineer");
var t2 = (Name: "Grace", Role: "Engineer");

Console.WriteLine(t1 == t2); // => True

Para obter mais informações sobre a sintaxe das tuplas e a desconstrução, consulte Tuplas e desconstrução.

Uso Object.ReferenceEquals para testar a identidade diretamente

ReferenceEquals verifica sempre a identidade, independentemente de como um tipo substitui Equals ou sobrecarrega ==. Use-o como diagnóstico de identidade quando precisar de confirmar se duas variáveis apontam exatamente para o mesmo objeto:

var doc1 = new Document("Report");
var doc2 = new Document("Report");
var doc3 = doc1;

Console.WriteLine(ReferenceEquals(doc1, doc2)); // => False
Console.WriteLine(ReferenceEquals(doc1, doc3)); // => True

Uma utilização comum é num override de Equals para evitar a comparação completa: quando ambos os argumentos são a mesma referência, são sempre iguais sem ser necessário verificar cada campo individualmente.

Note

Quando variáveis são tipadas como uma interface, == verifica-se se as variáveis de interface se referem ao mesmo objeto. Uma chamada para Equals ainda executa a implementação do objeto subjacente.

Note

ReferenceEquals retorna false sempre ao comparar tipos de valores, mesmo que ambos os argumentos contenham os mesmos valores. Este comportamento ocorre porque cada argumento do tipo valor é independentemente encaixado num objeto heap separado quando passado para ReferenceEquals.

Os tipos podem definir diferentes semânticas de igualdade

Os tipos podem definir semânticas de igualdade que diferem do comportamento padrão. A razão mais comum é implementar a igualdade de valor. Se criar um tipo que represente dados, como uma conta bancária, um produto em inventário ou um utilizador num sistema, considere instâncias com os mesmos valores como iguais. Escolha os tipos de registo para implementar a igualdade de valor, e o compilador gera todos os membros necessários para a igualdade.

Note

Strings são classes, mas == e Equals comparam o conteúdo das strings, não a identidade.

  • ==: o operador de igualdade. A maioria dos tipos utiliza este operador como verificação primária de igualdade. O seu comportamento depende do facto de o tipo ter um operador == predefinido ou definido pelo utilizador.
  • !=: o operador de desigualdade. Quando um tipo define um operador definido == pelo utilizador, também deve definir !=.
  • Equals: um método virtual herdado por todos os tipos. Podes sobrepô-lo para alterar a semântica de igualdade para um tipo.
  • GetHashCode: um método virtual usado por coleções baseadas em hash. Quando dois valores são iguais, os seus códigos de hash também devem ser iguais.
  • ReferenceEquals: um método estático que testa sempre a identidade.

Utilizar registos para igualdade de valores

Utilize o modificador record para atribuir igualdade por valor a um tipo centrado nos dados, se o tipo puder ser um registo. O compilador gera Equals, GetHashCode, e ==/!= membros que comparam todos os valores de propriedade declarados.

A record class continua a ser um tipo de referência, mas compara valores em vez de identidade:

var person1 = new Person("Ada", "Lovelace");
var person2 = new Person("Ada", "Lovelace");

Console.WriteLine(person1 == person2);               // => True
Console.WriteLine(person1.Equals(person2));          // => True
Console.WriteLine(ReferenceEquals(person1, person2)); // => False

ReferenceEquals confirma que person1 e person2 são objetos diferentes na memória, enquanto == e Equals retornam True porque a igualdade gerada pelo compilador compara os valores das propriedades.

A mesma geração do compilador aplica-se a tipos record struct:

var dim1 = new Dimension(1920, 1080);
var dim2 = new Dimension(1920, 1080);

Console.WriteLine(dim1 == dim2);      // => True
Console.WriteLine(dim1.Equals(dim2)); // => True

Os tipos de registo geram automaticamente todo o conjunto de operações de igualdade para o respetivo tipo. Ambos os tipos record class e record struct substituem Equals e GetHashCode. Também geram os operadores == e !=, bem como um método tipado Equals para o tipo de registo. Ao contrário de um simples struct, uma record struct suporta, por isso, == e != automaticamente. Para mais informações sobre os tipos de registo e a sua semântica de igualdade, consulte Registos.

Registos com membros do tipo referência

A igualdade de registo utiliza a própria semântica de igualdade dos membros. Cada propriedade ou campo é comparado utilizando o seu próprio Equals método. Para a maioria dos valores escalares, como int, string, ou DateTime, esta abordagem compara os valores dos membros do registo. A subtileza surge com coleções mutáveis comuns como List<T> ou: T[]estes tipos comparam-se por referência, pelo que duas instâncias de registo que contêm objetos de lista diferentes com o mesmo conteúdonão são consideradas iguais pela igualdade de registos sintetizados.

var playlist1 = new Playlist("Chill", new List<string> { "Song A", "Song B" });
var playlist2 = new Playlist("Chill", new List<string> { "Song A", "Song B" });

Console.WriteLine(playlist1.Equals(playlist2));                        // => False (different List instances)
Console.WriteLine(playlist1.Tracks.SequenceEqual(playlist2.Tracks));   // => True

playlist1 e playlist2 são instâncias separadas List<string> . Mesmo que o seu conteúdo coincida, Equals devolve false.

Quando precisar de igualdade de registos para refletir o conteúdo da coleção, tem algumas opções:

  • Implemente IEquatable<T> no registo e substitua Equals para usar Enumerable.SequenceEqual para os membros da coleção.
  • Utilize um tipo de coleção com igualdade de valor — por exemplo, um IEqualityComparer<T> personalizado ou um tipo cujo próprio Equals compara elementos.
  • Design em torno da identidade: se o registo representa uma entidade em vez de um valor puro, a igualdade de referência para os seus membros da coleção pode ser intencional.

Importante

A implementação manual da igualdade é rara hoje em C#. Os registos tratam automaticamente o cenário comum da igualdade de valor. Se precisar de implementar a igualdade manualmente — por exemplo, porque o tipo tem de derivar de uma classe base que não é um tipo record — consulte Implementar a igualdade manualmente quando um tipo não pode ser um tipo record na referência da linguagem.

Consulte também