Como definir a igualdade de valor para uma classe ou struct (Guia de Programação em C#)
Os registros implementam automaticamente a igualdade de valores. Considere definir um record
em vez de um class
quando seu tipo modela dados e deve implementar a igualdade de valor.
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)
retorno têm 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 você definir já tem uma implementação padrão de igualdade de valor que herda da System.ValueType substituição do Object.Equals(Object) método. 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). Na maioria dos casos, sua implementação de deve apenas chamar o método específico
Equals
dobool Equals( object obj )
tipo que é a implementação da System.IEquatable<T> interface. (Consulte o passo 2.)Implemente a System.IEquatable<T> interface fornecendo um método específico
Equals
do tipo. É 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.Equals
para examinar campos que estão na classe base. (Não liguebase.Equals
se o tipo herda diretamente do Object, porque a Object implementação do executa uma verificação de igualdade de Object.Equals(Object) 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
IEquatable
Equals
implementaçã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 é implementarIEquatable
apenas emsealed
classes. Para obter mais informações, consulte o exemplo de classe mais adiante neste artigo.
Opcional, mas recomendado: sobrecarregue os == operadores e != .
Substitua Object.GetHashCode para que dois objetos que têm igualdade de valor produzam o mesmo código hash.
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 >= .
Nota
Você pode usar registros para obter semântica de igualdade de valor sem qualquer código clichê desnecessário.
Exemplo de classe
O exemplo a seguir mostra como implementar a igualdade de valor em uma classe (tipo de referência).
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) = {0}", pointA.Equals(pointB));
Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
Console.WriteLine("null comparison = {0}", pointA.Equals(pointC));
Console.WriteLine("Compare to some other type = {0}", pointA.Equals(i));
TwoDPoint pointD = null;
TwoDPoint pointE = null;
Console.WriteLine("Two null TwoDPoints are equal: {0}", pointD == pointE);
pointE = new TwoDPoint(3, 4);
Console.WriteLine("(pointE == pointA) = {0}", pointE == pointA);
Console.WriteLine("(pointA == pointE) = {0}", pointA == pointE);
Console.WriteLine("(pointA != pointE) = {0}", pointA != pointE);
System.Collections.ArrayList list = new System.Collections.ArrayList();
list.Add(new ThreeDPoint(3, 4, 5));
Console.WriteLine("pointE.Equals(list[0]): {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 Equals
método, você deve 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 com base no tipo de tempo de IEquatable
compilação.
A igualdade de valor interna de record
tipos lida corretamente com cenários como este. Se TwoDPoint
e ThreeDPoint
fossem record
tipos, o resultado de p1.Equals(p2)
seria False
. Para obter mais informações, consulte Igualdade nas record
hierarquias de herança de tipo.
Exemplo de estrutura
O exemplo a seguir mostra como implementar a igualdade de valor em uma struct (tipo de valor):
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) = {0}", pointA.Equals(pointB));
// True:
Console.WriteLine("pointA == pointB = {0}", pointA == pointB);
// True:
Console.WriteLine("object.Equals(pointA, pointB) = {0}", object.Equals(pointA, pointB));
// False:
Console.WriteLine("pointA.Equals(null) = {0}", pointA.Equals(null));
// False:
Console.WriteLine("(pointA == null) = {0}", pointA == null);
// True:
Console.WriteLine("(pointA != null) = {0}", pointA != null);
// False:
Console.WriteLine("pointA.Equals(i) = {0}", pointA.Equals(i));
// CS0019:
// Console.WriteLine("pointA == i = {0}", 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]): {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) = {0}", pointA == pointC);
// True:
Console.WriteLine("pointC == pointD = {0}", pointC == pointD);
TwoDPoint temp = new TwoDPoint(3, 4);
pointC = temp;
// True:
Console.WriteLine("pointA == (pointC = 3,4) = {0}", pointA == pointC);
pointD = temp;
// True:
Console.WriteLine("pointD == (pointC = 3,4) = {0}", 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 (que é a versão substituída em System.ValueType) executa uma verificação de igualdade de Object.Equals(Object) valor usando reflection para comparar os valores de cada campo no tipo. Quando um implementador 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.