Comparações de igualdade em C#

Dica

Este artigo faz parte da seção Conceitos Básicos para desenvolvedores que já conhecem pelo menos uma linguagem de programação e estão aprendendo C#. Se você é novo em programação, comece com os tutoriais Comece agora.

Vindo de outro idioma? Em Java, == aplicado a objetos e, em JavaScript, === aplicado a objetos testam a identidade, não o conteúdo. As classes C# funcionam da mesma maneira por padrão. Em Python, == chama __eq__ e testa o conteúdo por padrão, semelhante à forma como os registros C# se comparam. As structs de C# também são comparadas por valor quando você chama Equals.

O C# distingue dois tipos de igualdade. A igualdade de valor significa que duas instâncias são iguais quando seus dados correspondem. A igualdade de referência significa que duas variáveis são iguais somente quando apontam para o mesmo objeto na memória. Essa condição também é chamada de identidade. Os tipos de valor geralmente comparam dados e os tipos de referência geralmente comparam a identidade. Os autores de tipos podem alterar esses padrões, mas esse modelo mental impede bugs sutis em que dois objetos idênticos não são considerados iguais ou onde uma mutação por meio de uma variável muda silenciosamente o que outra variável vê.

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

Cada tipo em C# é um tipo de valor ou um tipo de referência. Um tipo de valor mantém seus dados diretamente na variável. Um tipo de referência contém uma referência a um objeto. Quando você atribui uma variável de tipo de referência a outra variável, ambas as variáveis se referem ao mesmo objeto. Para obter mais informações sobre tipos de valor e tipos de referência, consulte a visão geral do sistema type.

O comportamento de igualdade padrão geralmente segue o tipo de tipo:

  • Tipos numéricos internos e enumerações são tipos de valor. Duas int variáveis são iguais quando seus valores numéricos correspondem.
  • Structs são tipos de valor. Uma simples struct usa a igualdade de valor quando você chama Equals.
  • Tuplas são tipos de valor. Duas tuplas são iguais quando todos os valores dos seus elementos coincidem.
  • Classes são tipos de referência. Uma classe simples usa a igualdade de referência e == testa Equals se duas variáveis apontam para o mesmo objeto.

Uma classe usa a 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

Um struct mostra a equivalência de valor por meio de Equals. Duas instâncias de struct são iguais quando seus campos correspondem:

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

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

Estruturas simples não têm o operador == predefinido. Gravar p1 == p2 em uma estrutura simples só será compilado se a estrutura declarar seu próprio operator ==. Se você precisar de comparações de operador para um struct, defina == e != como um par e mantenha-os consistentes com Equals e GetHashCode.

Tuplas também são tipos de valor. Duas tuplas são iguais quando o valor de cada elemento é igual. Os nomes de elementos em uma tupla nomeada são uma praticidade de tempo de compilação e não são considerados durante a comparação. Somente 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 de tuplas e desconstrução, consulte Tuplas e desconstrução.

Usar Object.ReferenceEquals para testar a identidade diretamente

ReferenceEquals sempre testa a identidade, independentemente de como um tipo substitui Equals ou sobrecarrega ==. Use-o como um diagnóstico de identidade quando precisar 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

Um uso comum é dentro de uma substituição Equals para evitar a comparação completa: quando os dois argumentos são a mesma referência, eles são sempre iguais sem precisar verificar cada campo individualmente.

Note

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

Note

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

Os tipos podem definir diferentes semânticas de igualdade

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

Note

Cadeias de caracteres são classes, mas == e Equals comparam o conteúdo da cadeia de caracteres, não a identidade.

  • ==: o operador de igualdade. A maioria dos tipos usa esse operador como a verificação de igualdade primária. Seu comportamento depende de o tipo ter um operador == interno ou definido pelo usuário.
  • !=: o operador de desigualdade. Quando um tipo define um operador definido pelo == usuário, ele também deve definir !=.
  • Equals: um método virtual herdado por todos os tipos. Você pode substituí-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, seus códigos de hash também devem ser iguais.
  • ReferenceEquals: um método estático que sempre testa a identidade.

Usar registros para igualdade de valor

Use o modificador record para atribuir igualdade de valor a um tipo com foco em dados quando o tipo puder ser um registro. O compilador gera os membros Equals, GetHashCode e ==/!= que comparam o valor de cada propriedade declarada.

A record class ainda é 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 isso person1 e person2 são objetos diferentes na memória, enquanto == e Equals retornam True porque a igualdade gerada pelo compilador compara valores de propriedade.

A mesma geração do compilador também se aplica aos 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 registro geram todo o conjunto de igualdade para seu próprio tipo. Ambos os tipos record class e record struct sobrescrevem Equals e GetHashCode. Eles também geram os operadores == e !=, além de um método tipado Equals para o tipo de registro. Ao contrário de um(a) simples struct, record struct portanto oferece suporte a == e != automaticamente. Para obter mais informações sobre tipos de registro e sua semântica de igualdade, consulte Registros.

Registros com membros do tipo referência

A igualdade de registros usa a semântica de igualdade dos próprios membros. Cada propriedade ou campo é comparado usando seu próprio Equals método. Para a maioria dos valores escalares, como int, stringou DateTime, essa abordagem compara os valores dos membros do registro. A sutileza surge com coleções mutáveis comuns, como List<T> ou T[]: esses tipos são comparados por referência, portanto, duas instâncias de registro que contêm objetos de lista diferentes com o mesmo conteúdonão são consideradas iguais pela igualdade de registro sintetizada.

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 seu conteúdo corresponda, Equals retorna false.

Quando você precisa de igualdade de registros para refletir o conteúdo da coleção, você tem algumas opções:

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

Importante

A implementação manual da igualdade é rara hoje em C#. Os registros lidam com o cenário comum de igualdade de valor automaticamente. Se você precisar implementar a igualdade manualmente - por exemplo, porque seu tipo deve derivar de uma classe base que não seja de registro - consulte Implementar igualdade por conta própria quando um tipo não pode ser um registro na referência de idioma.

Consulte também