Percursos
/
C#
C#
/
Programa
/
Igualdade
Ig

Igualdade em C#

5 exercícios

Sobre Igualdade

O exercício de programação ilustra várias propriedades da igualdade em C#:

Object.Equals()

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

  • Os tipos simples (strings e primitivos) são normalmente testados quanto à igualdade com == e !=. Isto é considerado mais idiomático do que usar o método Equals(), que também está disponível com estes tipos. Os programadores de Java devem ter atenção, ao lidar com strings, ao facto de == comparar por valor em C# mas por referência em Java quando voltarem à sua linguagem anterior.
  • Os tipos de referência (instâncias de classes) são comparados através do método Equals() herdado de object. Se o teu objetivo com o teste de igualdade é garantir que dois objetos são exatamente a mesma instância, basta confiar na implementação de object. Caso contrário, tens de substituir object.Equals().
  • Se sabes que todas as instâncias da tua classe são criadas num único sítio, por exemplo, personagens de um jogo ou de uma simulação, então a igualdade de referência é suficiente. No entanto, é provável que sejam criadas várias instâncias da mesma entidade do mundo real (por exemplo, a partir de uma base de dados, por introdução do utilizador, através de um pedido web). Neste caso, os valores que identificam unicamente a entidade têm de ser testados quanto à igualdade. Por isso, Equals() tem de ser substituído.
  • Um Equals() substituído conterá testes de igualdade sobre membros de tipos simples com == e sobre 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 detetar se são uma e a mesma instância. Isto traz clareza e é uma necessidade quando Equals() e == foram substituídos/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 auxiliares

  • Além de public override bool Equals(object obj), as IDEs geram normalmente a sobrecarga protected bool Equals(FacialFeatures other) para usar quando está envolvida herança. Uma classe derivada pode chamar o Equals() da classe base e depois acrescentar o seu próprio teste.
  • Não uses == a não ser que tenhas sobrecarregado o operador ==, bem como o método Equals() na tua classe (ver o exercício operator-overloading) ou só te interesse que as referências sejam iguais.
  • Os testes de igualdade em structs são abordados no exercício structs.
  • A igualdade de tuples é abordada no exercício tuples.
  • Muitos programadores confiam nas suas IDEs para fornecer a implementação dos métodos de igualdade, uma vez que estas tratam de todos os pormenores 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);
}
  • Tem cuidado se decidires melhorar o código gerado pela tua IDE. Esse código é geralmente à prova de nulos e de objetos com tipos errados e bastante bem otimizado.
  • Os testes de igualdade de delegates não são abordados especificamente no exercício.
  • Não existem testes de igualdade incorporados para arrays nem para a maioria das coleções. O LINQ (abordado em exercícios posteriores) fornece SequenceEquals(), mas na ausência de LINQ é uma questão de iterar por ambas as coleções e comparar os itens individualmente.
  • Para uma discussão sobre como usar == e != com as tuas próprias classes, vê o exercício operator-overloading.

Object.GetHashCode()

  • object.GetHashCode() devolve um código de hash sob a forma de um número inteiro de 32 bits. O código de hash é usado por classes de dicionário e de conjunto, como Dictionary<T> e HashSet<T>, para armazenar e obter objetos de forma eficiente. No caso dos dicionários, o hashing está relacionado com as chaves.
  • Existe uma expectativa entre os programadores de C# de que, se substituíres Equals(), também substituirás GetHashCode(). Há uma relação entre Equals() e GetHashCode() que tem de se verificar para o comportamento correto das classes de dicionário e de conjunto de hash e de quaisquer outras que usem um código de hash. Espera-se que implementes o método de modo a não deixar armadilhas para os responsáveis pela manutenção que possam acrescentar mais tarde uma coleção baseada em códigos de hash.
  • A relação entre o código de hash e a igualdade é que, se dois objetos forem iguais (Equal() devolve true), então GetHashCode() para os dois objetos tem de devolver o mesmo valor. Isto não se aplica na direção inversa. Não é simétrico. Imagina uma função de pesquisa que primeiro vai a um "bucket" com base no código de hash e depois escolhe o item em particular usando o teste de igualdade.
  • A forma mais fácil de criar um código de hash é chamar HashCode.Combine(), passando os valores usados no teste de igualdade (ou um subconjunto deles). Tem em conta que, quanta mais informação forneceres ao Combine(), mais eficiente será provavelmente 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 têm de ser estáveis enquanto a coleção com hash estiver em uso. Se adicionares um objeto à coleção com um conjunto de valores e depois alterares esses valores, o código de hash já não apontará para o "bucket" correto. Na prática, isto significa que o objeto deve ser imutável. Outras abordagens correm o risco de criar armadilhas para os responsáveis pela manutenção. A imutabilidade é abordada noutros exercícios.
  • É possível que consigas conceber um código de hash melhor do que o produzido pelas rotinas da biblioteca, mas ou é porque tens um conhecimento 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 ligeiramente o desempenho, especialmente quando os objetos pertencem a coleções, podes acrescentar um membro sobrecarregado public bool Equals(T other).

Isto poupará uma certa quantidade de verificação de tipos para os tipos de referência e poupará um passo de boxing para os tipos de valor, uma vez que não precisarão de ser convertidos num objeto (boxed) como argumento de public override bool Equals(object other).

Se acrescentares a interface IEquatable<T> à tua classe, isso exigirá que a sobrecarga seja implementada. A menos que o teu código contenha rotinas que recebam objetos do tipo IEquatable<T> (e que presumivelmente façam algo interessante relacionado com a igualdade, independentemente da classe que a implementa), não há realmente qualquer outra razão convincente para incluir a interface.

IEqualityComparer<T>

Se tiveres uma classe que pode ser identificada de forma única de duas maneiras diferentes, por exemplo, 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 de hash e de igualdade diferentes. Cada uma pode ter uma implementação diferente de IEqualityComparer<T>, com o seu próprio método Equals() e GetHashCode(). Podes ter um dicionário com chave no SSID e outro com chave no endereço de email.

Quando IEqualityComparer<T> está em jogo, normalmente continuarias a substituir Equals() e GetHashCode() na tua classe de item para evitar problemas fora das classes de coleção.

Uma consideração a ter ao usar IEqualityComparer<T> é que os 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 de hash no próprio objeto. Não é ideal, desde logo, que uma dependência fundamental 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 de comparação noutro sítio, seria ainda mais fácil para um responsável pela manutenção fazer alterações aos membros de uma classe sem ter em conta as consequências para a coleção.

Nota sobre a igualdade de números de vírgula flutuante

Um primitivo que pode desafiar o programador incauto é testar a igualdade de valores de vírgula flutuante. Isto é abordado no documento about.md do conceito floating-point-numbers.

Igualdade e herança

Este artigo mostra algumas das decisões que têm de ser tomadas em relação à igualdade e à herança. Só te dirão respeito se estiveres a manipular instâncias de uma classe e de uma classe derivada e precisares de testar a igualdade entre as duas.

Editar via GitHub A ligação abre numa nova janela ou separador

Aprende Igualdade

A prática está bloqueada

Desbloqueia mais 4 exercícios para praticares Igualdade