Trilhas
/
C#
C#
/
Programa
/
Igualdade
Ig

Igualdade em C#

5 exercícios

Sobre Igualdade

O exercício de programação ilustra uma série de propriedades da igualdade em C#:

Object.Equals()

Tópicos abordados pelo exercício de programação

  • Tipos simples (strings e primitivos) normalmente são testados quanto à igualdade com == e !=. Isso é considerado mais idiomático do que usar o método Equals(), que também está disponível com esses tipos. Quem programa em Java deve ficar atento, ao lidar com strings, ao fato de que == compara por valor em C#, mas por referência em Java, ao voltar à sua antiga linguagem.
  • Tipos de referência (instâncias de classes) são comparados usando o método Equals() herdado de object. Se o seu objetivo com o teste de igualdade é garantir que dois objetos são exatamente a mesma instância, confiar na implementação de object será suficiente. Caso contrário, você precisa sobrescrever object.Equals().
  • Se você sabe que todas as instâncias da sua classe são criadas em um único lugar, digamos personagens de algum jogo ou simulação, então a igualdade por referência é suficiente. No entanto, é provável que várias instâncias da mesma entidade do mundo real sejam criadas (por exemplo, a partir de um banco de dados, por entrada do usuário ou por meio de uma requisição web). Nesse caso, os valores que identificam unicamente a entidade precisam ser testados quanto à igualdade. Por isso, Equals() precisa ser sobrescrito.
  • Um Equals() sobrescrito conterá testes de igualdade em membros de tipos simples usando == e em tipos de referência com chamadas recursivas a Equals().
class StatusBar
{
    private readonly int width = 200, height = 20;

    public override bool Equals(object other)
    {
        // ... null and type checks and performance optimisations
        return width == (other as StatusBar).width && height == (other as StatusBar).height;
    }
}
class Window
{
    private readonly string title = "Main";
    private readonly StatusBar statusBar = new StatusBar();

    public override bool Equals(object other)
    {
        // ... null and type checks and performance optimisations
        return title == (other as Window).title && statusBar.Equals((other as Window).statusBar);
    }
}

  • O método estático object.ReferenceEquals() é usado para comparar dois objetos e detectar se são uma única e mesma instância. Isso traz clareza e é uma necessidade quando Equals() e == foram sobrescritos/sobrecarregados.
var winA = new Window(); // above code shows that all windows are equal
var winB = new Window();
ReferenceEquals(winA, winB);
// => false
var winC = winA;
ReferenceEquals(winA, winC);
// => true

Tópicos secundários

  • Além de public override bool Equals(object obj), as IDEs normalmente geram a sobrecarga protected bool Equals(FacialFeatures other) para uso quando há herança envolvida. Uma classe derivada pode chamar o Equals() da classe base e depois adicionar seu próprio teste.
  • Não use == a menos que você tenha sobrecarregado o operador ==, bem como o método Equals() na sua classe (veja o exercício operator-overloading), ou a menos que você se importe apenas que as referências sejam iguais.
  • Os testes de igualdade em structs são tratados no exercício structs.
  • A igualdade de tuples é tratada no exercício tuples.
  • Muitos desenvolvedores contam com suas IDEs para fornecer a implementação dos métodos de igualdade, já que elas cuidam de todas as minúcias da igualdade. Por exemplo, o RIDER da JetBrains (v 2020.1) gera os seguintes métodos de igualdade para uma classe:
protected bool Equals(T other)
{
    return field1 == other.field1 && field2.Equals(other.field2);
}

public override bool Equals(object obj)
{
    if (ReferenceEquals(null, obj)) return false;
    if (ReferenceEquals(this, obj)) return true;
    if (obj.GetType() != this.GetType()) return false;
    return Equals((T) obj);
}
  • Tenha cuidado se você decidir melhorar o código gerado pela sua IDE. Esse código em geral é à prova de falhas contra nulos e objetos com o tipo errado, e razoavelmente bem otimizado.
  • Os testes de igualdade de delegates não são discutidos especificamente no exercício.
  • Não há testes de igualdade embutidos para arrays nem para a maioria das coleções. O LINQ (discutido em exercícios posteriores) fornece SequenceEquals(), mas, na ausência do LINQ, é uma questão de iterar pelas duas coleções e comparar os itens individualmente.
  • Para uma discussão sobre como usar == e != com suas próprias classes, veja o exercício operator-overloading.

Object.GetHashCode()

  • object.GetHashCode() retorna um código hash na forma de um inteiro de 32 bits. O código hash é usado por classes de dicionário e de conjunto, como Dictionary<T> e HashSet<T>, para armazenar e recuperar objetos de forma performática. No caso dos dicionários, o hashing está relacionado às chaves.
  • Há uma expectativa entre quem programa em C# de que, se você sobrescrever Equals(), também sobrescreverá GetHashCode(). Existe uma relação entre Equals() e GetHashCode() que precisa se manter verdadeira para o comportamento correto das classes de dicionário e de conjunto hash e de quaisquer outras que usem um código hash. Espera-se que você implemente o método de modo que não crie armadilhas para quem for dar manutenção e talvez adicione uma coleção baseada em código hash mais tarde.
  • A relação entre código hash e igualdade é que, se dois objetos são iguais (Equal() retorna true), então o GetHashCode() dos dois objetos precisa retornar o mesmo valor. Isso não vale na direção inversa. Não é simétrico. Imagine uma função de busca que primeiro vai a um "bucket" com base no código hash e depois seleciona o item específico usando o teste de igualdade.
  • A maneira mais fácil de criar um hashcode é chamar HashCode.Combine() passando os valores usados no teste de igualdade (ou um subconjunto deles). Tenha em mente que, quanto mais informações você fornecer a Combine(), provavelmente mais performática será a implementação do hash.
public class Assessment
{
    private int rating;
    private Person boss;

    public override int GetHashCode()
    {
        return HashCode.Combine(rating, boss);
    }
}
  • Os valores usados no teste de igualdade precisam ser estáveis enquanto a coleção com hash estiver em uso. Se você adicionar um objeto à coleção com um conjunto de valores e depois alterar esses valores, o código hash não apontará mais para o "bucket" correto. Na prática, isso significa que o objeto deve ser imutável. Outras abordagens correm o risco de criar armadilhas para quem for dar manutenção. A imutabilidade é discutida em outros exercícios.
  • É possível que você consiga projetar um hashcode melhor do que o produzido pelas rotinas da biblioteca, mas isso acontece porque você tem um entendimento detalhado das características dos dados ou porque é uma coleção muito simples, em que os valores podem ser usados diretamente sem hashing. Pode não valer o esforço extra.

Melhorias de desempenho

Para melhorar um pouco o desempenho, especialmente quando os objetos pertencem a coleções, você pode adicionar um membro sobrecarregado public bool Equals(T other).

Isso economizará uma certa quantidade de verificação de tipo para tipos de referência e eliminará uma etapa de boxing para tipos de valor, já que eles não precisarão ser convertidos em um objeto (boxed) como argumento para public override bool Equals(object other).

Se você adicionar a interface IEquatable<T> à sua classe, isso exigirá que a sobrecarga seja implementada. A menos que seu código contenha rotinas que recebam objetos do tipo IEquatable<T> (e provavelmente façam algo interessante relacionado à igualdade, independentemente da classe que a implementa), não há realmente nenhum outro motivo convincente para incluir a interface.

IEqualityComparer<T>

Se você tem uma classe que pode ser identificada unicamente de duas maneiras diferentes, digamos uma classe Person que tem um SSID e um endereço de email único, então o .NET fornece um meio de permitir que duas coleções diferentes usem testes de código hash e de igualdade diferentes. Cada uma pode ter uma implementação diferente de IEqualityComparer<T>, com seu próprio método Equals() e GetHashCode(). Você pode ter um dicionário com chave por SSID e outro com chave por endereço de email.

Onde IEqualityComparer<T> está em jogo, você normalmente ainda sobrescreveria Equals() e GetHashCode() na sua classe de item, para evitar problemas fora das classes de coleção.

Uma consideração ao usar IEqualityComparer<T> é que métodos privados etc. não estarão disponíveis para o teste de igualdade.

Se apenas uma coleção com hash estiver em jogo, pode ser melhor evitar IEqualityComparer<T> e encapsular a igualdade e o código hash no próprio objeto. Não é ideal, em primeiro lugar, que uma dependência-chave como a que existe entre o objeto e a coleção com hash não possa ser imposta pelo compilador, mas, com a lógica de hashing e comparação em outro lugar, seria ainda mais fácil para quem for dar manutenção alterar os membros de uma classe sem considerar as consequências para a coleção.

Nota sobre igualdade de ponto flutuante

Um primitivo que pode desafiar quem programa sem estar atento é o teste da igualdade de valores de ponto flutuante. Isso é discutido no documento about.md do conceito floating-point-numbers.

Igualdade e herança

Este artigo mostra algumas das decisões que precisam ser tomadas em relação à igualdade e à herança. Elas só dirão respeito a você se você estiver manipulando instâncias de uma classe e de uma classe derivada e precisar testar a igualdade entre as duas.

Editar via GitHub O link abre em uma nova janela ou aba

Aprenda Igualdade

A prática está bloqueada

Desbloqueie mais 4 exercícios para praticar Igualdade