Como o HashSet compara elementos para igualdade?

127

Eu tenho uma classe que é IComparable:

public class a : IComparable
{
    public int Id { get; set; }
    public string Name { get; set; }

    public a(int id)
    {
        this.Id = id;
    }

    public int CompareTo(object obj)
    {
        return this.Id.CompareTo(((a)obj).Id);
    }
}

Quando adiciono uma lista de objetos desta classe a um conjunto de hash:

a a1 = new a(1);
a a2 = new a(2);
HashSet<a> ha = new HashSet<a>();
ha.add(a1);
ha.add(a2);
ha.add(a1);

Está tudo bem e ha.countestá 2, mas:

a a1 = new a(1);
a a2 = new a(2);
HashSet<a> ha = new HashSet<a>();
ha.add(a1);
ha.add(a2);
ha.add(new a(1));

Agora ha.counté 3.

  1. Por que não HashSetrespeita ao CompareTométodo?
  2. É HashSeta melhor maneira de ter uma lista de objetos exclusivos?
nima
fonte
Adicionar uma implementação IEqualityComparer<T>no construtor ou implementá-lo na classe a. msdn.microsoft.com/pt-br/library/bb301504(v=vs.110).aspx
Jaider

Respostas:

137

Ele usa um IEqualityComparer<T>(a EqualityComparer<T>.Defaultmenos que você especifique um diferente na construção).

Quando você adiciona um elemento ao conjunto, ele encontra o código hash usando IEqualityComparer<T>.GetHashCodee armazena o código hash e o elemento (depois de verificar se o elemento já está no conjunto, é claro).

Para procurar um elemento, ele primeiro usará o IEqualityComparer<T>.GetHashCodepara encontrar o código de hash e, em seguida, para todos os elementos com o mesmo código de hash, ele será usado IEqualityComparer<T>.Equalspara comparar a igualdade real.

Isso significa que você tem duas opções:

  • Passe um costume IEqualityComparer<T>para o construtor. Essa é a melhor opção se você não puder modificar a Tsi mesmo ou se desejar uma relação de igualdade não padrão (por exemplo, "todos os usuários com um ID de usuário negativo são considerados iguais"). Isso quase nunca é implementado no próprio tipo (isto Fooé, não implementa IEqualityComparer<Foo>), mas em um tipo separado, que é usado apenas para comparações.
  • Implemente a igualdade no próprio tipo, substituindo GetHashCodee Equals(object). Idealmente, implemente também IEquatable<T>no tipo, principalmente se for um tipo de valor. Esses métodos serão chamados pelo comparador de igualdade padrão.

Observe como nada disso é em termos de comparação ordenada - o que faz sentido, pois certamente existem situações em que você pode especificar facilmente a igualdade, mas não uma ordenação total. Isso é tudo igual Dictionary<TKey, TValue>, basicamente.

Se você deseja um conjunto que use ordenação em vez de apenas comparações de igualdade, use o SortedSet<T>.NET 4 - que permite especificar um em IComparer<T>vez de um IEqualityComparer<T>. Isso usará IComparer<T>.Compare- que delegará IComparable<T>.CompareToou IComparable.CompareTose você estiver usando Comparer<T>.Default.

Jon Skeet
fonte
7
+1 Observe também a resposta da @ tyriker (que a IMO deve ser um comentário aqui), que aponta que a maneira mais simples de alavancar a questão IEqualityComparer<T>.GetHashCode/Equals()é implementar Equalse GetHashCodesobre Tsi mesma (e enquanto você faz isso, você também implementaria a contraparte fortemente tipada : - bool IEquatable<T>.Equals(T other))
Ruben Bartelink
5
Embora muito preciso esta resposta pode ser um pouco confuso, especialmente para novos usuários, pois não indicar claramente que, para o caso mais simples substituir Equalse GetHashCodeé suficiente - como mencionado na resposta de @ tyriker.
precisa saber é o seguinte
Imo depois de implementar IComparable(ou, IComparernesse caso), você não deve ser solicitado a implementar a igualdade separadamente (mas apenas GetHashCode). Em certo sentido, as interfaces de comparabilidade devem herdar das interfaces de igualdade. Eu entendo os benefícios de desempenho em ter duas funções separadas (onde você pode otimizar a igualdade separadamente apenas dizendo se algo é igual ou não), mas ainda assim .. Muito confuso caso contrário, quando você especificar quando as instâncias são iguais em CompareTofunção e estrutura não serão consideradas aquele.
Nawfal
@nawfal nem tudo tem uma ordem lógica. se você está comparando duas coisas que contêm uma propriedade bool, é simplesmente horrível ter que escrever algo como a.boolProp == b.boolProp ? 1 : 0ou deveria ser a.boolProp == b.boolProp ? 0 : -1ou a.boolProp == b.boolProp ? 1 : -1. Yuk!
Simon_Weaver
1
@Simon_Weaver é. Quero, de alguma forma, evitá-lo em meu aspecto hipotético que estava propondo.
Nawfal
77

Aqui estão os esclarecimentos sobre uma parte da resposta que não foi dita: O tipo de objeto seu HashSet<T>não precisa ser implementado, IEqualityComparer<T>mas apenas substituído Object.GetHashCode()e Object.Equals(Object obj).

Em vez disso:

public class a : IEqualityComparer<a>
{
  public int GetHashCode(a obj) { /* Implementation */ }
  public bool Equals(a obj1, a obj2) { /* Implementation */ }
}

Você faz isso:

public class a
{
  public override int GetHashCode() { /* Implementation */ }
  public override bool Equals(object obj) { /* Implementation */ }
}

É sutil, mas isso me levou a maior parte do dia tentando fazer com que o HashSet funcionasse da maneira que se destina. E, como outros já disseram, HashSet<a>acabará ligando a.GetHashCode()e a.Equals(obj)conforme necessário ao trabalhar com o aparelho.

tyriker
fonte
2
Bom ponto. BTW, como mencionado no meu comentário na resposta do @ JonSkeet, você também deve implementar bool IEquatable<T>.Equals(T other)para obter um leve ganho de eficiência, mas mais importante ainda, o benefício da clareza. Por razões OBV, além da necessidade de implementar GetHashCodeao lado IEquatable<T>, o doc para IEquatable <T> menciona que, para fins de consistência você também deve substituir o object.Equalsde consistência
Ruben Bartelink
Eu tentei implementar isso. As ovveride getHashcodeobras, mas override bool equalsrecebe o erro: nenhum método encontrado para substituição. qualquer ideia?
Stefanvds
Finalmente a informação que eu estava procurando. Obrigado.
Mauro Sampietro
Dos meus comentários na resposta acima - No seu caso "Em vez de", você pode ter public class a : IEqualityComparer<a> {e, em seguida new HashSet<a>(a).
HankCa
Mas veja os comentários de Jon Skeets acima.
HankCa
9

HashSetusa Equalse GetHashCode().

CompareTo é para conjuntos encomendados.

Se você deseja objetos únicos, mas não se importa com a ordem de iteração deles, HashSet<T>geralmente é a melhor opção.

CodesInChaos
fonte
5

O construtor HashSet recebe o objeto que implementa IEqualityComparer para adicionar novo objeto. se você deseja usar o método no HashSet, anula a substituição de Equals, GetHashCode

namespace HashSet
{
    public class Employe
    {
        public Employe() {
        }

        public string Name { get; set; }

        public override string ToString()  {
            return Name;
        }

        public override bool Equals(object obj) {
            return this.Name.Equals(((Employe)obj).Name);
        }

        public override int GetHashCode() {
            return this.Name.GetHashCode();
        }
    }

    class EmployeComparer : IEqualityComparer<Employe>
    {
        public bool Equals(Employe x, Employe y)
        {
            return x.Name.Trim().ToLower().Equals(y.Name.Trim().ToLower());
        }

        public int GetHashCode(Employe obj)
        {
            return obj.Name.GetHashCode();
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            HashSet<Employe> hashSet = new HashSet<Employe>(new EmployeComparer());
            hashSet.Add(new Employe() { Name = "Nik" });
            hashSet.Add(new Employe() { Name = "Rob" });
            hashSet.Add(new Employe() { Name = "Joe" });
            Display(hashSet);
            hashSet.Add(new Employe() { Name = "Rob" });
            Display(hashSet);

            HashSet<Employe> hashSetB = new HashSet<Employe>(new EmployeComparer());
            hashSetB.Add(new Employe() { Name = "Max" });
            hashSetB.Add(new Employe() { Name = "Solomon" });
            hashSetB.Add(new Employe() { Name = "Werter" });
            hashSetB.Add(new Employe() { Name = "Rob" });
            Display(hashSetB);

            var union = hashSet.Union<Employe>(hashSetB).ToList();
            Display(union);
            var inter = hashSet.Intersect<Employe>(hashSetB).ToList();
            Display(inter);
            var except = hashSet.Except<Employe>(hashSetB).ToList();
            Display(except);

            Console.ReadKey();
        }

        static void Display(HashSet<Employe> hashSet)
        {
            if (hashSet.Count == 0)
            {
                Console.Write("Collection is Empty");
                return;
            }
            foreach (var item in hashSet)
            {
                Console.Write("{0}, ", item);
            }
            Console.Write("\n");
        }

        static void Display(List<Employe> list)
        {
            if (list.Count == 0)
            {
                Console.WriteLine("Collection is Empty");
                return;
            }
            foreach (var item in list)
            {
                Console.Write("{0}, ", item);
            }
            Console.Write("\n");
        }
    }
}
Nikolai Nechai
fonte
E se o nome for nulo? qual é o valor hash de null?
joe