Удаление дубликатов из списка С#

Я следую предыдущему сообщению в stackoverflow об удалении дубликатов из списка на С#.

Если <T> - это определенный пользовательский тип типа:

class Contact
{
  public string firstname;
  public string lastname;
  public string phonenum;
}

Предлагаемый (HashMap) не удаляет дубликат. Я думаю, мне нужно переопределить некоторый метод для сравнения двух объектов, не так ли?

Ответ 1

A HashSet<T> удаляет дубликаты, потому что это набор... но только тогда, когда ваш тип правильно определяет равенство.

Я подозреваю, что "дублировать" означает "объект с равными значениями поля для другого объекта" - вам нужно переопределить Equals/GetHashCode для этого, и/или реализовать IEquatable<Contact>... или вы можете предоставить IEqualityComparer<Contact> конструктору HashSet<T>.

Вместо использования HashSet<T> вы можете просто вызвать метод расширения Distinct LINQ. Например:

list = list.Distinct().ToList();

Но опять же вам нужно будет предоставить соответствующее определение равенства, так или иначе.

Вот пример реализации. Обратите внимание, как я сделал его неизменным (равенство нечетно с изменяемыми типами, поскольку два объекта могут быть равны одной минуте и не равны следующей) и сделал поля частные, с общедоступными свойствами. Наконец, я запечатал класс - неизменяемые типы, как правило, должны быть запечатаны, и это облегчает обсуждение равенства.

using System;
using System.Collections.Generic; 

public sealed class Contact : IEquatable<Contact>
{
    private readonly string firstName;
    public string FirstName { get { return firstName; } }

    private readonly string lastName;
    public string LastName { get { return lastName; } }

    private readonly string phoneNumber;
    public string PhoneNumber { get { return phoneNumber; } }

    public Contact(string firstName, string lastName, string phoneNumber)
    {
        this.firstName = firstName;
        this.lastName = lastName;
        this.phoneNumber = phoneNumber;
    }

    public override bool Equals(object other)
    {
        return Equals(other as Contact);
    }

    public bool Equals(Contact other)
    {
        if (object.ReferenceEquals(other, null))
        {
            return false;
        }
        if (object.ReferenceEquals(other, this))
        {
            return true;
        }
        return FirstName == other.FirstName &&
               LastName == other.LastName &&
               PhoneNumber == other.PhoneNumber;
    }

    public override int GetHashCode()
    {
        // Note: *not* StringComparer; EqualityComparer<T>
        // copes with null; StringComparer doesn't.
        var comparer = EqualityComparer<string>.Default;

        // Unchecked to allow overflow, which is fine
        unchecked
        {
            int hash = 17;
            hash = hash * 31 + comparer.GetHashCode(FirstName);
            hash = hash * 31 + comparer.GetHashCode(LastName);
            hash = hash * 31 + comparer.GetHashCode(PhoneNumber);
            return hash;
        }
    }
}

EDIT: Хорошо, в ответ на запросы об объяснении реализации GetHashCode():

  • Мы хотим объединить хэш-коды свойств этого объекта
  • Мы нигде не проверяем значение nullity, поэтому мы должны предположить, что некоторые из них могут быть нулевыми. EqualityComparer<T>.Default всегда обрабатывает это, что приятно... поэтому я использую это, чтобы получить хэш-код каждого поля.
  • "Добавление и умножение" подхода к объединению нескольких хеш-кодов в один из них является стандартным, рекомендованным Джошем Блохом. Существует множество других алгоритмов хэширования общего назначения, но это отлично подходит для большинства приложений.
  • Я не знаю, по умолчанию ли вы компилируете в проверенном контексте, поэтому я поставил вычисление в неконтролируемый контекст. Нам действительно все равно, если повторное умножение/добавление приводит к переполнению, потому что мы не ищем "величину" как таковую... просто число, которое мы можем неоднократно использовать для равных объектов.

Два альтернативных способа обращения с недействительностью, кстати:

public override int GetHashCode()
{
    // Unchecked to allow overflow, which is fine
    unchecked
    {
        int hash = 17;
        hash = hash * 31 + (FirstName ?? "").GetHashCode();
        hash = hash * 31 + (LastName ?? "").GetHashCode();
        hash = hash * 31 + (PhoneNumber ?? "").GetHashCode();
        return hash;
    }
}

или

public override int GetHashCode()
{
    // Unchecked to allow overflow, which is fine
    unchecked
    {
        int hash = 17;
        hash = hash * 31 + (FirstName == null ? 0 : FirstName.GetHashCode());
        hash = hash * 31 + (LastName == null ? 0 : LastName.GetHashCode());
        hash = hash * 31 + (PhoneNumber == null ? 0 : PhoneNumber.GetHashCode());
        return hash;
    }
}

Ответ 2

class Contact {
    public int Id { get; set; }
    public string Name { get; set; }

    public override string ToString()
    {
        return string.Format("{0}:{1}", Id, Name);
    }

    static private IEqualityComparer<Contact> comparer;
    static public IEqualityComparer<Contact> Comparer {
        get { return comparer ?? (comparer = new EqualityComparer()); }
    }

    class EqualityComparer : IEqualityComparer<Contact> {
        bool IEqualityComparer<Contact>.Equals(Contact x, Contact y)
        {
            if (x == y) 
                return true;

            if (x == null || y == null)
                return false;

            return x.Name == y.Name; // let compare by Name
        }

        int IEqualityComparer<Contact>.GetHashCode(Contact c)
        {
            return c.Name.GetHashCode(); // let compare by Name
        }
    }
}

class Program {
    public static void Main()
    {
        var list = new List<Contact> {
            new Contact { Id = 1, Name = "John" },
            new Contact { Id = 2, Name = "Sylvia" },
            new Contact { Id = 3, Name = "John" }
        };

        var distinctNames = list.Distinct(Contact.Comparer).ToList();
        foreach (var contact in distinctNames)
            Console.WriteLine(contact);
    }
}

дает

1:John
2:Sylvia

Ответ 3

Для этой задачи я не обязательно считаю, что реализация IComparable является очевидным решением. Вы можете сортировать и тестировать уникальность разными способами.

Я бы предпочел реализовать a IEqualityComparer<Contact>:

sealed class ContactFirstNameLastNameComparer : IEqualityComparer<Contact>
{
  public bool Equals (Contact x, Contact y)
  {
     return x.firstname == y.firstname && x.lastname == y.lastname;
  }

  public int GetHashCode (Contact obj)
  {
     return obj.firstname.GetHashCode () ^ obj.lastname.GetHashCode ();
  }
}

И затем используйте System.Linq.Enumerable.Distinct (при условии, что вы используете хотя бы .NET 3.5)

var unique = contacts.Distinct (new ContactFirstNameLastNameComparer ()).ToArray ();

PS. Говоря о HashSet<> Обратите внимание, что HashSet<> принимает IEqualityComparer<> в качестве параметра конструктора.