Partilhar via


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):

  1. A propriedade reflexiva: x.Equals(x) retorna true.

  2. A propriedade simétrica: x.Equals(y) retorna o mesmo valor que y.Equals(x).

  3. A propriedade transitiva: if (x.Equals(y) && y.Equals(z)) retorna true, então x.Equals(z) retorna true.

  4. 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.

  5. Qualquer valor não nulo não é igual a nulo. No entanto, x.Equals(y) lança uma exceção quando x é null. Isso quebra as regras 1 ou 2, dependendo do argumento para Equals.

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:

  1. Substitua o método virtualObject.Equals(Object). Na maioria dos casos, sua implementação de deve apenas chamar o método específico Equals do bool Equals( object obj ) tipo que é a implementação da System.IEquatable<T> interface. (Consulte o passo 2.)

  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 de Equals. 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 ligue base.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 é implementar IEquatable apenas em sealed classes. Para obter mais informações, consulte o exemplo de classe mais adiante neste artigo.

  3. Opcional, mas recomendado: sobrecarregue os == operadores e != .

  4. Substitua Object.GetHashCode para que dois objetos que têm igualdade de valor produzam o mesmo código hash.

  5. 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.

Consulte também