Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
Sugestão
Considere usar registros primeiro. Os registros implementam automaticamente a igualdade de valor com código mínimo, tornando-os a abordagem recomendada para a maioria dos tipos focados em dados. Se você precisar de lógica de igualdade de valor personalizada ou não puder usar registros, continue com as etapas de implementação manual abaixo.
Ao definir uma classe ou estrutura, você decide se faz sentido criar uma definição personalizada de igualdade de valor (ou equivalência) para o tipo. Normalmente, você implementa a igualdade de valor quando espera adicionar objetos do tipo a uma coleção ou quando sua finalidade principal é armazenar um conjunto de campos ou propriedades. Você pode basear sua definição de igualdade de valor em uma comparação de todos os campos e propriedades no tipo, ou pode basear a definição em um subconjunto.
Em ambos os casos, e em ambas as classes e structs, sua implementação deve seguir as cinco garantias de equivalência (para as seguintes regras, assuma que x, y e z não são nulas):
A propriedade reflexiva:
x.Equals(x)retornatrue.A propriedade simétrica:
x.Equals(y)retorna o mesmo valor quey.Equals(x).A propriedade transitiva: if
(x.Equals(y) && y.Equals(z))retornatrue, entãox.Equals(z)retornatrue.Invocações sucessivas de
x.Equals(y)retornam o mesmo valor, desde que os objetos referenciados por x e y não sejam modificados.Qualquer valor não nulo não é igual a nulo. No entanto,
x.Equals(y)lança uma exceção quandoxé null. Isso quebra as regras 1 ou 2, dependendo do argumento paraEquals.
Qualquer struct que definas já tem uma implementação padrão de igualdade de valor que herda da System.ValueType substituição do método Object.Equals(Object). Esta implementação usa reflexão para examinar todos os campos e propriedades no tipo. Embora essa implementação produza resultados corretos, ela é relativamente lenta em comparação com uma implementação personalizada que você escreve especificamente para o tipo.
Os detalhes de implementação para igualdade de valor são diferentes para classes e estruturas. No entanto, ambas as classes e estruturas requerem os mesmos passos básicos para implementar a igualdade:
Substitua o método virtualObject.Equals(Object). Isso fornece um comportamento de igualdade polimórfica, permitindo que seus objetos sejam comparados corretamente quando tratados como
objectreferências. Garante um comportamento adequado nas coleções e na utilização do polimorfismo. Na maioria dos casos, a sua implementação debool Equals( object obj )deve apenas chamar o métodoEqualsespecífico ao tipo que é a implementação da interface System.IEquatable<T>. (Consulte o passo 2.)Implemente a System.IEquatable<T> interface fornecendo um método do tipo específico
Equals. Isso proporciona uma verificação de igualdade segura sem boxe, resultando em melhor desempenho. Ele também evita a transmissão desnecessária e permite a verificação de tipo em tempo de compilação. É aqui que é realizada a comparação de equivalência real. Por exemplo, você pode decidir definir igualdade comparando apenas um ou dois campos em seu tipo. Não lance exceções deEquals. Para classes relacionadas por herança:Este método deve examinar apenas os campos que são declarados na classe. Ele deve chamar
base.Equalspara examinar campos que estão na classe base. (Não liguebase.Equalsse o tipo herda diretamente do Object, porque a implementação de Object de Object.Equals(Object) executa uma verificação de igualdade por referência.)Duas variáveis só devem ser consideradas iguais se os tipos de tempo de execução das variáveis que estão sendo comparadas forem os mesmos. Além disso, certifique-se de que a
IEquatableEqualsimplementação do método para o tipo de tempo de execução é usada se os tipos de tempo de execução e tempo de compilação de uma variável são diferentes. Uma estratégia para garantir que os tipos de tempo de execução sejam sempre comparados corretamente é implementarIEquatableapenas emsealedclasses. Para obter mais informações, consulte o exemplo de classe mais adiante neste artigo.
Opcional, mas recomendado: sobrecarregue os operadores == e !=. Isso fornece sintaxe consistente e intuitiva para comparações de igualdade, correspondendo às expectativas do usuário de tipos internos. Garante isso
obj1 == obj2eobj1.Equals(obj2)comporta-se da mesma maneira.Substitua Object.GetHashCode para que dois objetos que têm igualdade de valor produzam o mesmo código hash. Isso é necessário para o comportamento correto em coleções baseadas em hash como
Dictionary<TKey,TValue>eHashSet<T>. Os objetos que são iguais devem ter códigos hash iguais, ou essas coleções não funcionarão corretamente.Opcional: Para suportar definições para "maior que" ou "menor que", implemente a IComparable<T> interface para seu tipo e também sobrecarregue os <operadores = e >= . Isso permite operações de classificação e fornece uma relação de ordenação completa para o seu tipo, útil ao adicionar objetos a coleções ordenadas ou ao classificar matrizes ou listas.
Exemplo de registo
O exemplo a seguir mostra como os registros implementam automaticamente a igualdade de valor com código mínimo. O primeiro registro TwoDPoint é um tipo de registro simples que implementa automaticamente a igualdade de valor. O segundo registro ThreeDPoint demonstra que os registros podem ser derivados de outros registros e ainda mantêm um comportamento adequado de igualdade de valores:
namespace ValueEqualityRecord;
public record TwoDPoint(int X, int Y);
public record ThreeDPoint(int X, int Y, int Z) : TwoDPoint(X, Y);
class Program
{
static void Main(string[] args)
{
// Create some points
TwoDPoint pointA = new TwoDPoint(3, 4);
TwoDPoint pointB = new TwoDPoint(3, 4);
TwoDPoint pointC = new TwoDPoint(5, 6);
ThreeDPoint point3D_A = new ThreeDPoint(3, 4, 5);
ThreeDPoint point3D_B = new ThreeDPoint(3, 4, 5);
ThreeDPoint point3D_C = new ThreeDPoint(3, 4, 7);
Console.WriteLine("=== Value Equality with Records ===");
// Value equality works automatically
Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}"); // True
Console.WriteLine($"pointA == pointB = {pointA == pointB}"); // True
Console.WriteLine($"pointA.Equals(pointC) = {pointA.Equals(pointC)}"); // False
Console.WriteLine($"pointA == pointC = {pointA == pointC}"); // False
Console.WriteLine("\n=== Hash Codes ===");
// Equal objects have equal hash codes automatically
Console.WriteLine($"pointA.GetHashCode() = {pointA.GetHashCode()}");
Console.WriteLine($"pointB.GetHashCode() = {pointB.GetHashCode()}");
Console.WriteLine($"pointC.GetHashCode() = {pointC.GetHashCode()}");
Console.WriteLine("\n=== Inheritance with Records ===");
// Inheritance works correctly with value equality
Console.WriteLine($"point3D_A.Equals(point3D_B) = {point3D_A.Equals(point3D_B)}"); // True
Console.WriteLine($"point3D_A == point3D_B = {point3D_A == point3D_B}"); // True
Console.WriteLine($"point3D_A.Equals(point3D_C) = {point3D_A.Equals(point3D_C)}"); // False
// Different types are not equal (unlike problematic class example)
Console.WriteLine($"pointA.Equals(point3D_A) = {pointA.Equals(point3D_A)}"); // False
Console.WriteLine("\n=== Collections ===");
// Works seamlessly with collections
var pointSet = new HashSet<TwoDPoint> { pointA, pointB, pointC };
Console.WriteLine($"Set contains {pointSet.Count} unique points"); // 2 unique points
var pointDict = new Dictionary<TwoDPoint, string>
{
{ pointA, "First point" },
{ pointC, "Different point" }
};
// Demonstrate that equivalent points work as the same key
var duplicatePoint = new TwoDPoint(3, 4);
Console.WriteLine($"Dictionary contains key for {duplicatePoint}: {pointDict.ContainsKey(duplicatePoint)}"); // True
Console.WriteLine($"Dictionary contains {pointDict.Count} entries"); // 2 entries
Console.WriteLine("\n=== String Representation ===");
// Automatic ToString implementation
Console.WriteLine($"pointA.ToString() = {pointA}");
Console.WriteLine($"point3D_A.ToString() = {point3D_A}");
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Expected Output:
=== Value Equality with Records ===
pointA.Equals(pointB) = True
pointA == pointB = True
pointA.Equals(pointC) = False
pointA == pointC = False
=== Hash Codes ===
pointA.GetHashCode() = -1400834708
pointB.GetHashCode() = -1400834708
pointC.GetHashCode() = -148136000
=== Inheritance with Records ===
point3D_A.Equals(point3D_B) = True
point3D_A == point3D_B = True
point3D_A.Equals(point3D_C) = False
pointA.Equals(point3D_A) = False
=== Collections ===
Set contains 2 unique points
Dictionary contains key for TwoDPoint { X = 3, Y = 4 }: True
Dictionary contains 2 entries
=== String Representation ===
pointA.ToString() = TwoDPoint { X = 3, Y = 4 }
point3D_A.ToString() = ThreeDPoint { X = 3, Y = 4, Z = 5 }
*/
Os registos oferecem várias vantagens para a igualdade de valor:
-
Implementação automática: Os registros implementam System.IEquatable<T> e substituem Object.Equalsautomaticamente , Object.GetHashCodee os
==/!=operadores. -
Comportamento correto de herança: os registros são implementados
IEquatable<T>usando métodos virtuais que verificam o tipo de tempo de execução de ambos os operandos, garantindo o comportamento correto em hierarquias de herança e cenários polimórficos. - Imutabilidade por padrão: os registros incentivam o design imutável, que funciona bem com a semântica de igualdade de valores.
- Sintaxe concisa: os parâmetros posicionais fornecem uma maneira compacta de definir tipos de dados.
- Melhor desempenho: A implementação de igualdade gerada pelo compilador é otimizada e não usa reflexão como a implementação struct padrão.
Use registros quando seu objetivo principal for armazenar dados e precisar de semântica de igualdade de valor.
Registros com membros que usam igualdade de referência
Quando os registros contêm membros que usam a igualdade de referência, o comportamento automático de igualdade de valores dos registros não funciona conforme o esperado. Isso se aplica a coleções como System.Collections.Generic.List<T>, matrizes e outros tipos de referência que não implementam a igualdade baseada em valor (com a notável exceção de , que implementa a igualdade de System.Stringvalor).
Importante
Embora os registros forneçam excelente igualdade de valor para tipos de dados básicos, eles não resolvem automaticamente a igualdade de valor para membros que usam igualdade de referência. Se um registro contiver um System.Collections.Generic.List<T>, System.Array, ou outros tipos de referência que não implementam a igualdade de valor, duas instâncias de registro com conteúdo idêntico nesses membros ainda não serão iguais porque os membros usam igualdade de referência.
public record PersonWithHobbies(string Name, List<string> Hobbies);
var person1 = new PersonWithHobbies("Alice", new List<string> { "Reading", "Swimming" });
var person2 = new PersonWithHobbies("Alice", new List<string> { "Reading", "Swimming" });
Console.WriteLine(person1.Equals(person2)); // False - different List instances!
Isso ocorre porque os registros usam o método de cada membro, e os Object.Equals tipos de coleção normalmente usam igualdade de referência em vez de comparar seu conteúdo.
O seguinte mostra o problema:
// Records with reference-equality members don't work as expected
public record PersonWithHobbies(string Name, List<string> Hobbies);
Veja como isso se comporta quando você executa o código:
Console.WriteLine("=== Records with Collections - The Problem ===");
// Problem: Records with mutable collections use reference equality for the collection
var person1 = new PersonWithHobbies("Alice", [ "Reading", "Swimming" ]);
var person2 = new PersonWithHobbies("Alice", [ "Reading", "Swimming" ]);
Console.WriteLine($"person1: {person1}");
Console.WriteLine($"person2: {person2}");
Console.WriteLine($"person1.Equals(person2): {person1.Equals(person2)}"); // False! Different List instances
Console.WriteLine($"Lists have same content: {person1.Hobbies.SequenceEqual(person2.Hobbies)}"); // True
Console.WriteLine();
Soluções para registros com membros de igualdade de referência
Implementação personalizadaSystem.IEquatable<T>: substitua a igualdade gerada pelo compilador por uma versão codificada manualmente que forneça comparação baseada em conteúdo para membros de igualdade de referência. Para coleções, implemente a comparação elemento a elemento usando Enumerable.SequenceEqual métodos ou métodos semelhantes.
Use tipos de valor sempre que possível: considere se seus dados podem ser representados com tipos de valor ou estruturas imutáveis que naturalmente suportam igualdade de valor, como System.Numerics.Vector<T> ou Plane.
Usar tipos com igualdade baseada em valor: para coleções, considere o uso de tipos que implementam a igualdade baseada em valor ou implementam tipos de coleção personalizados que substituem Object.Equals para fornecer comparação baseada em conteúdo, como System.Collections.Immutable.ImmutableArray<T> ou System.Collections.Immutable.ImmutableList<T>.
Projetar com igualdade de referência em mente: aceite que alguns membros usarão a igualdade de referência e projete sua lógica de aplicativo de acordo, garantindo que você reutilize as mesmas instâncias quando a igualdade for importante.
Aqui está um exemplo de implementação de igualdade personalizada para registros com coleções:
// A potential solution using IEquatable<T> with custom equality
public record PersonWithHobbiesFixed(string Name, List<string> Hobbies) : IEquatable<PersonWithHobbiesFixed>
{
public virtual bool Equals(PersonWithHobbiesFixed? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
// Use SequenceEqual for List comparison
return Name == other.Name && Hobbies.SequenceEqual(other.Hobbies);
}
public override int GetHashCode()
{
// Create hash based on content, not reference
var hashCode = new HashCode();
hashCode.Add(Name);
foreach (var hobby in Hobbies)
{
hashCode.Add(hobby);
}
return hashCode.ToHashCode();
}
}
Esta implementação personalizada funciona corretamente:
Console.WriteLine("=== Solution 1: Custom IEquatable Implementation ===");
var personFixed1 = new PersonWithHobbiesFixed("Bob", [ "Cooking", "Hiking" ]);
var personFixed2 = new PersonWithHobbiesFixed("Bob", [ "Cooking", "Hiking" ]);
Console.WriteLine($"personFixed1: {personFixed1}");
Console.WriteLine($"personFixed2: {personFixed2}");
Console.WriteLine($"personFixed1.Equals(personFixed2): {personFixed1.Equals(personFixed2)}"); // True! Custom equality
Console.WriteLine();
O mesmo problema afeta matrizes e outros tipos de coleção:
// These also use reference equality - the issue persists
public record PersonWithHobbiesArray(string Name, string[] Hobbies);
public record PersonWithHobbiesImmutable(string Name, IReadOnlyList<string> Hobbies);
As matrizes também usam igualdade de referência, produzindo os mesmos resultados inesperados:
Console.WriteLine("=== Arrays Also Use Reference Equality ===");
var personArray1 = new PersonWithHobbiesArray("Charlie", ["Gaming", "Music" ]);
var personArray2 = new PersonWithHobbiesArray("Charlie", ["Gaming", "Music" ]);
Console.WriteLine($"personArray1: {personArray1}");
Console.WriteLine($"personArray2: {personArray2}");
Console.WriteLine($"personArray1.Equals(personArray2): {personArray1.Equals(personArray2)}"); // False! Arrays use reference equality too
Console.WriteLine($"Arrays have same content: {personArray1.Hobbies.SequenceEqual(personArray2.Hobbies)}"); // True
Console.WriteLine();
Mesmo as coleções somente leitura exibem esse comportamento de igualdade de referência:
Console.WriteLine("=== Same Issue with IReadOnlyList ===");
var personImmutable1 = new PersonWithHobbiesImmutable("Diana", [ "Art", "Travel" ]);
var personImmutable2 = new PersonWithHobbiesImmutable("Diana", [ "Art", "Travel" ]);
Console.WriteLine($"personImmutable1: {personImmutable1}");
Console.WriteLine($"personImmutable2: {personImmutable2}");
Console.WriteLine($"personImmutable1.Equals(personImmutable2): {personImmutable1.Equals(personImmutable2)}"); // False! Reference equality
Console.WriteLine($"Content is the same: {personImmutable1.Hobbies.SequenceEqual(personImmutable2.Hobbies)}"); // True
Console.WriteLine();
O principal insight é que os registros resolvem o problema da igualdade estrutural , mas não alteram o comportamento de igualdade semântica dos tipos que eles contêm.
Exemplo de classe
O exemplo a seguir mostra como implementar a igualdade de valor em uma classe (tipo de referência). Essa abordagem manual é necessária quando você não pode usar registros ou precisa de uma lógica de igualdade personalizada:
namespace ValueEqualityClass;
class TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }
public TwoDPoint(int x, int y)
{
if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.X = x;
this.Y = y;
}
public override bool Equals(object obj) => this.Equals(obj as TwoDPoint);
public bool Equals(TwoDPoint p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// If run-time types are not exactly the same, return false.
if (this.GetType() != p.GetType())
{
return false;
}
// Return true if the fields match.
// Note that the base class is not invoked because it is
// System.Object, which defines Equals as reference equality.
return (X == p.X) && (Y == p.Y);
}
public override int GetHashCode() => (X, Y).GetHashCode();
public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs)
{
if (lhs is null)
{
if (rhs is null)
{
return true;
}
// Only the left side is null.
return false;
}
// Equals handles case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}
// For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint.
class ThreeDPoint : TwoDPoint, IEquatable<ThreeDPoint>
{
public int Z { get; private set; }
public ThreeDPoint(int x, int y, int z)
: base(x, y)
{
if ((z < 1) || (z > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.Z = z;
}
public override bool Equals(object obj) => this.Equals(obj as ThreeDPoint);
public bool Equals(ThreeDPoint p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// Check properties that this class declares.
if (Z == p.Z)
{
// Let base class check its own fields
// and do the run-time type comparison.
return base.Equals((TwoDPoint)p);
}
else
{
return false;
}
}
public override int GetHashCode() => (X, Y, Z).GetHashCode();
public static bool operator ==(ThreeDPoint lhs, ThreeDPoint rhs)
{
if (lhs is null)
{
if (rhs is null)
{
// null == null = true.
return true;
}
// Only the left side is null.
return false;
}
// Equals handles the case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(ThreeDPoint lhs, ThreeDPoint rhs) => !(lhs == rhs);
}
class Program
{
static void Main(string[] args)
{
ThreeDPoint pointA = new ThreeDPoint(3, 4, 5);
ThreeDPoint pointB = new ThreeDPoint(3, 4, 5);
ThreeDPoint pointC = null;
int i = 5;
Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}");
Console.WriteLine($"pointA == pointB = {pointA == pointB}");
Console.WriteLine($"null comparison = {pointA.Equals(pointC)}");
Console.WriteLine($"Compare to some other type = {pointA.Equals(i)}");
TwoDPoint pointD = null;
TwoDPoint pointE = null;
Console.WriteLine($"Two null TwoDPoints are equal: {pointD == pointE}");
pointE = new TwoDPoint(3, 4);
Console.WriteLine($"(pointE == pointA) = {pointE == pointA}");
Console.WriteLine($"(pointA == pointE) = {pointA == pointE}");
Console.WriteLine($"(pointA != pointE) = {pointA != pointE}");
System.Collections.ArrayList list = new System.Collections.ArrayList();
list.Add(new ThreeDPoint(3, 4, 5));
Console.WriteLine($"pointE.Equals(list[0]): {pointE.Equals(list[0])}");
// Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
pointA.Equals(pointB) = True
pointA == pointB = True
null comparison = False
Compare to some other type = False
Two null TwoDPoints are equal: True
(pointE == pointA) = False
(pointA == pointE) = False
(pointA != pointE) = True
pointE.Equals(list[0]): False
*/
Em classes (tipos de referência), a implementação padrão de ambos os Object.Equals(Object) métodos executa uma comparação de igualdade de referência, não uma verificação de igualdade de valor. Quando um implementador substitui o método virtual, o objetivo é dar-lhe semântica de igualdade de valor.
Os == operadores e != podem ser usados com classes, mesmo que a classe não os sobrecarregue. No entanto, o comportamento padrão é executar uma verificação de igualdade de referência. Em uma classe, se você sobrecarregar o método Equals, convém sobrecarregar os operadores == e !=, mas não é necessário.
Importante
O código de exemplo anterior pode não lidar com todos os cenários de herança da maneira esperada. Considere o seguinte código:
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
Console.WriteLine(p1.Equals(p2)); // output: True
Este código informa que p1 é p2 igual apesar da diferença nos z valores. A diferença é ignorada porque o compilador escolhe a TwoDPoint implementação de IEquatable com base no tipo em tempo de compilação. Esta é uma questão fundamental com a igualdade polimórfica nas hierarquias de herança.
Igualdade polimórfica
Ao implementar a igualdade de valor em hierarquias de herança com classes, a abordagem padrão mostrada no exemplo de classe pode levar a um comportamento incorreto quando os objetos são usados polimorficamente. O problema ocorre porque System.IEquatable<T> as implementações são escolhidas com base no tipo de tempo de compilação, não no tipo de tempo de execução.
O problema com implementações padrão
Considere este cenário problemático:
TwoDPoint p1 = new ThreeDPoint(1, 2, 3); // Declared as TwoDPoint
TwoDPoint p2 = new ThreeDPoint(1, 2, 4); // Declared as TwoDPoint
Console.WriteLine(p1.Equals(p2)); // True - but should be False!
A comparação retorna True porque o compilador seleciona com base no tipo declarado, ignorando as Z diferenças de TwoDPoint.Equals(TwoDPoint) coordenadas.
A chave para corrigir a igualdade polimórfica é garantir que todas as comparações de igualdade usem o método virtual Object.Equals , que pode verificar os tipos de tempo de execução e lidar com a herança corretamente. Isso pode ser conseguido usando a implementação de interface explícita para System.IEquatable<T> que delega ao método virtual:
A classe base demonstra os principais padrões:
// Safe polymorphic equality implementation using explicit interface implementation
class TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }
public TwoDPoint(int x, int y)
{
if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.X = x;
this.Y = y;
}
public override bool Equals(object? obj) => Equals(obj as TwoDPoint);
// Explicit interface implementation prevents compile-time type issues
bool IEquatable<TwoDPoint>.Equals(TwoDPoint? p) => Equals((object?)p);
protected virtual bool Equals(TwoDPoint? p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// If run-time types are not exactly the same, return false.
if (this.GetType() != p.GetType())
{
return false;
}
// Return true if the fields match.
// Note that the base class is not invoked because it is
// System.Object, which defines Equals as reference equality.
return (X == p.X) && (Y == p.Y);
}
public override int GetHashCode() => (X, Y).GetHashCode();
public static bool operator ==(TwoDPoint? lhs, TwoDPoint? rhs)
{
if (lhs is null)
{
if (rhs is null)
{
return true;
}
// Only the left side is null.
return false;
}
// Equals handles case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(TwoDPoint? lhs, TwoDPoint? rhs) => !(lhs == rhs);
}
A classe derivada estende corretamente a lógica de igualdade:
// For the sake of simplicity, assume a ThreeDPoint IS a TwoDPoint.
class ThreeDPoint : TwoDPoint, IEquatable<ThreeDPoint>
{
public int Z { get; private set; }
public ThreeDPoint(int x, int y, int z)
: base(x, y)
{
if ((z < 1) || (z > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
this.Z = z;
}
public override bool Equals(object? obj) => Equals(obj as ThreeDPoint);
// Explicit interface implementation prevents compile-time type issues
bool IEquatable<ThreeDPoint>.Equals(ThreeDPoint? p) => Equals((object?)p);
protected override bool Equals(TwoDPoint? p)
{
if (p is null)
{
return false;
}
// Optimization for a common success case.
if (Object.ReferenceEquals(this, p))
{
return true;
}
// Runtime type check happens in the base method
if (p is ThreeDPoint threeD)
{
// Check properties that this class declares.
if (Z != threeD.Z)
{
return false;
}
return base.Equals(p);
}
return false;
}
public override int GetHashCode() => (X, Y, Z).GetHashCode();
public static bool operator ==(ThreeDPoint? lhs, ThreeDPoint? rhs)
{
if (lhs is null)
{
if (rhs is null)
{
// null == null = true.
return true;
}
// Only the left side is null.
return false;
}
// Equals handles the case of null on right side.
return lhs.Equals(rhs);
}
public static bool operator !=(ThreeDPoint? lhs, ThreeDPoint? rhs) => !(lhs == rhs);
}
Veja como essa implementação lida com os cenários polimórficos problemáticos:
Console.WriteLine("=== Safe Polymorphic Equality ===");
// Test polymorphic scenarios that were problematic before
TwoDPoint p1 = new ThreeDPoint(1, 2, 3);
TwoDPoint p2 = new ThreeDPoint(1, 2, 4);
TwoDPoint p3 = new ThreeDPoint(1, 2, 3);
TwoDPoint p4 = new TwoDPoint(1, 2);
Console.WriteLine("Testing polymorphic equality (declared as TwoDPoint):");
Console.WriteLine($"p1 = ThreeDPoint(1, 2, 3) as TwoDPoint");
Console.WriteLine($"p2 = ThreeDPoint(1, 2, 4) as TwoDPoint");
Console.WriteLine($"p3 = ThreeDPoint(1, 2, 3) as TwoDPoint");
Console.WriteLine($"p4 = TwoDPoint(1, 2)");
Console.WriteLine();
Console.WriteLine($"p1.Equals(p2) = {p1.Equals(p2)}"); // False - different Z values
Console.WriteLine($"p1.Equals(p3) = {p1.Equals(p3)}"); // True - same values
Console.WriteLine($"p1.Equals(p4) = {p1.Equals(p4)}"); // False - different types
Console.WriteLine($"p4.Equals(p1) = {p4.Equals(p1)}"); // False - different types
Console.WriteLine();
A implementação também lida corretamente com comparações diretas de tipo:
// Test direct type comparisons
var point3D_A = new ThreeDPoint(3, 4, 5);
var point3D_B = new ThreeDPoint(3, 4, 5);
var point3D_C = new ThreeDPoint(3, 4, 7);
var point2D_A = new TwoDPoint(3, 4);
Console.WriteLine("Testing direct type comparisons:");
Console.WriteLine($"point3D_A.Equals(point3D_B) = {point3D_A.Equals(point3D_B)}"); // True
Console.WriteLine($"point3D_A.Equals(point3D_C) = {point3D_A.Equals(point3D_C)}"); // False
Console.WriteLine($"point3D_A.Equals(point2D_A) = {point3D_A.Equals(point2D_A)}"); // False
Console.WriteLine($"point2D_A.Equals(point3D_A) = {point2D_A.Equals(point3D_A)}"); // False
Console.WriteLine();
A implementação da igualdade também funciona adequadamente com coleções:
// Test with collections
Console.WriteLine("Testing with collections:");
var hashSet = new HashSet<TwoDPoint> { p1, p2, p3, p4 };
Console.WriteLine($"HashSet contains {hashSet.Count} unique points"); // Should be 3: one ThreeDPoint(1,2,3), one ThreeDPoint(1,2,4), one TwoDPoint(1,2)
var dictionary = new Dictionary<TwoDPoint, string>
{
{ p1, "First 3D point" },
{ p2, "Second 3D point" },
{ p4, "2D point" }
};
Console.WriteLine($"Dictionary contains {dictionary.Count} entries");
Console.WriteLine($"Dictionary lookup for equivalent point: {dictionary.ContainsKey(new ThreeDPoint(1, 2, 3))}"); // True
O código anterior demonstra elementos-chave para implementar a igualdade baseada em valor:
-
Substituição virtual
Equals(object?): A lógica de igualdade principal acontece no método virtualObject.Equals, que é chamado independentemente do tipo de tempo de compilação. -
Verificação de tipo de tempo de execução: Usar
this.GetType() != p.GetType()garante que objetos de tipos diferentes nunca sejam considerados iguais. - Implementação de interface explícita: A System.IEquatable<T> implementação delega ao método virtual, evitando problemas de seleção de tipo em tempo de compilação.
-
Método auxiliar virtual protegido: O
protected virtual Equals(TwoDPoint? p)método permite que as classes derivadas substituam a lógica de igualdade, mantendo a segurança do tipo.
Use este padrão quando:
- Você tem hierarquias de herança onde a igualdade de valores é importante
- Os objetos podem ser usados polimorficamente (declarados como tipo base, instanciados como tipo derivado)
- Você precisa de tipos de referência com semântica de igualdade de valor
A abordagem preferida é usar record tipos para implementar a igualdade baseada em valor. Essa abordagem requer uma implementação mais complexa do que a abordagem padrão e requer testes completos de cenários polimórficos para garantir a correção.
Exemplo de estrutura
O exemplo a seguir mostra como implementar a igualdade de valor em uma struct (tipo de valor). Embora as structs tenham igualdade de valor padrão, uma implementação personalizada pode melhorar o desempenho:
namespace ValueEqualityStruct
{
struct TwoDPoint : IEquatable<TwoDPoint>
{
public int X { get; private set; }
public int Y { get; private set; }
public TwoDPoint(int x, int y)
: this()
{
if (x is (< 1 or > 2000) || y is (< 1 or > 2000))
{
throw new ArgumentException("Point must be in range 1 - 2000");
}
X = x;
Y = y;
}
public override bool Equals(object? obj) => obj is TwoDPoint other && this.Equals(other);
public bool Equals(TwoDPoint p) => X == p.X && Y == p.Y;
public override int GetHashCode() => (X, Y).GetHashCode();
public static bool operator ==(TwoDPoint lhs, TwoDPoint rhs) => lhs.Equals(rhs);
public static bool operator !=(TwoDPoint lhs, TwoDPoint rhs) => !(lhs == rhs);
}
class Program
{
static void Main(string[] args)
{
TwoDPoint pointA = new TwoDPoint(3, 4);
TwoDPoint pointB = new TwoDPoint(3, 4);
int i = 5;
// True:
Console.WriteLine($"pointA.Equals(pointB) = {pointA.Equals(pointB)}");
// True:
Console.WriteLine($"pointA == pointB = {pointA == pointB}");
// True:
Console.WriteLine($"object.Equals(pointA, pointB) = {object.Equals(pointA, pointB)}");
// False:
Console.WriteLine($"pointA.Equals(null) = {pointA.Equals(null)}");
// False:
Console.WriteLine($"(pointA == null) = {pointA == null}");
// True:
Console.WriteLine($"(pointA != null) = {pointA != null}");
// False:
Console.WriteLine($"pointA.Equals(i) = {pointA.Equals(i)}");
// CS0019:
// Console.WriteLine($"pointA == i = {pointA == i}");
// Compare unboxed to boxed.
System.Collections.ArrayList list = new System.Collections.ArrayList();
list.Add(new TwoDPoint(3, 4));
// True:
Console.WriteLine($"pointA.Equals(list[0]): {pointA.Equals(list[0])}");
// Compare nullable to nullable and to non-nullable.
TwoDPoint? pointC = null;
TwoDPoint? pointD = null;
// False:
Console.WriteLine($"pointA == (pointC = null) = {pointA == pointC}");
// True:
Console.WriteLine($"pointC == pointD = {pointC == pointD}");
TwoDPoint temp = new TwoDPoint(3, 4);
pointC = temp;
// True:
Console.WriteLine($"pointA == (pointC = 3,4) = {pointA == pointC}");
pointD = temp;
// True:
Console.WriteLine($"pointD == (pointC = 3,4) = {pointD == pointC}");
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
pointA.Equals(pointB) = True
pointA == pointB = True
Object.Equals(pointA, pointB) = True
pointA.Equals(null) = False
(pointA == null) = False
(pointA != null) = True
pointA.Equals(i) = False
pointE.Equals(list[0]): True
pointA == (pointC = null) = False
pointC == pointD = True
pointA == (pointC = 3,4) = True
pointD == (pointC = 3,4) = True
*/
}
Para structs, a implementação padrão de Object.Equals(Object) (que é a versão substituída em System.ValueType) executa uma verificação de igualdade de valores usando Reflection para comparar os valores de todos os campos no tipo. Embora essa implementação produza resultados corretos, ela é relativamente lenta em comparação com uma implementação personalizada que você escreve especificamente para o tipo.
Quando você substitui o método virtual Equals em um struct, o objetivo é fornecer um meio mais eficiente de executar a verificação de igualdade de valor e, opcionalmente, basear a comparação em algum subconjunto dos campos ou propriedades do struct.
Os == operadores e != não podem operar em uma estrutura, a menos que a estrutura os sobrecarregue explicitamente.