Відмінність не працює з LINQ до об'єктів


120
class Program
{
    static void Main(string[] args)
    {
        List<Book> books = new List<Book> 
        {
            new Book
            {
                Name="C# in Depth",
                Authors = new List<Author>
                {
                    new Author 
                    {
                        FirstName = "Jon", LastName="Skeet"
                    },
                     new Author 
                    {
                        FirstName = "Jon", LastName="Skeet"
                    },                       
                }
            },
            new Book
            {
                Name="LINQ in Action",
                Authors = new List<Author>
                {
                    new Author 
                    {
                        FirstName = "Fabrice", LastName="Marguerie"
                    },
                     new Author 
                    {
                        FirstName = "Steve", LastName="Eichert"
                    },
                     new Author 
                    {
                        FirstName = "Jim", LastName="Wooley"
                    },
                }
            },
        };


        var temp = books.SelectMany(book => book.Authors).Distinct();
        foreach (var author in temp)
        {
            Console.WriteLine(author.FirstName + " " + author.LastName);
        }

        Console.Read();
    }

}
public class Book
{
    public string Name { get; set; }
    public List<Author> Authors { get; set; }
}
public class Author
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public override bool Equals(object obj)
    {
        return true;
        //if (obj.GetType() != typeof(Author)) return false;
        //else return ((Author)obj).FirstName == this.FirstName && ((Author)obj).FirstName == this.LastName;
    }

}

Це ґрунтується на прикладі в "LINQ в дії". Лістинг 4.16.

Це друкує Джон Скіт двічі. Чому? Я навіть спробував перемогти метод рівності в класі автора. Досі розрізнення, здається, не працює. Що я пропускаю?

Редагувати: Я також додав == і! = Оператор перевантажений. Досі допомоги немає.

 public static bool operator ==(Author a, Author b)
    {
        return true;
    }
    public static bool operator !=(Author a, Author b)
    {
        return false;
    }

Відповіді:


159

LINQ Distinct не такий розумний, коли мова йде про власні об'єкти.

Все, що він робить, - це переглянути свій список і побачити, що в ньому є два різних об'єкти (не байдуже, що вони мають однакові значення для полів-членів).

Одне вирішення - це реалізація інтерфейсу IEquatable, як показано тут .

Якщо ви модифікуєте свій авторський клас так, він повинен працювати.

public class Author : IEquatable<Author>
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public bool Equals(Author other)
    {
        if (FirstName == other.FirstName && LastName == other.LastName)
            return true;

        return false;
    }

    public override int GetHashCode()
    {
        int hashFirstName = FirstName == null ? 0 : FirstName.GetHashCode();
        int hashLastName = LastName == null ? 0 : LastName.GetHashCode();

        return hashFirstName ^ hashLastName;
    }
}

Спробуйте це як DotNetFiddle


22
IEquatable - це добре, але неповно; Ви завжди повинні разом реалізовувати Object.Equals () та Object.GetHashCode (); IEquatable <T> .Equals не перекриває Object.Equals, тому це стане невдалим при порівнянні не сильно типізованих порівнянь, що часто трапляється в рамках і завжди в негенеральних колекціях.
AndyM

Тож чи краще використовувати переопределення Distinct, яке приймає IEqualityComparer <T>, як запропонував Rex M? Я маю на увазі, що я повинен робити, якщо не хочу потрапити в пастку.
Танмой

3
@Tanmoy це залежить. Якщо ви хочете, щоб Автор нормально поводився як звичайний об'єкт (тобто лише довідкова рівність), але перевіряв значення імен для розрізнення, використовуйте IEqualityComparer. Якщо ви завжди хочете, щоб авторські об'єкти порівнювались на основі значень імен, то замініть GetHashCode та Equals або застосуйте IEquatable.
Rex M

3
Я реалізував IEquatable(і переоцінив Equals/ GetHashCode), але жодна з моїх точок перериву не працює в цих методах на Linq Distinct?
PeterX

2
@PeterX Я теж це помітив. У мене були точки прориву в, GetHashCodeі Equalsвони потрапляли під час цикла передбачення. Це тому, що var temp = books.SelectMany(book => book.Authors).Distinct();return an IEnumerable, тобто запит не виконується відразу, він виконується лише тоді, коли дані використовуються. Якщо ви хочете одразу прикладу цієї стрільби, додайте .ToList()після цього, .Distinct()і ви побачите точки прориву в передній частині Equalsта GetHashCodeперед цим.
JabberwockyDecompiler

70

Distinct()Метод перевіряє рівність посилань для посилальних типів. Це означає, що він шукає буквально один і той же об'єкт, що дублюється, а не різні об'єкти, які містять однакові значення.

Існує перевантаження, яка приймає IEqualityComparer , тому ви можете вказати іншу логіку для визначення того, чи є даний об'єкт рівним іншому.

Якщо ви хочете, щоб Автор нормально поводився як звичайний об'єкт (тобто лише опорна рівність), але для цілей розрізнення перевіряйте рівність за значеннями імен, використовуйте IEqualityComparer . Якщо ви завжди хочете, щоб авторські об'єкти порівнювались на основі значень імен, то замініть GetHashCode та Equals або застосуйте IEquatable .

Два учасника в IEqualityComparerінтерфейсі є Equalsі GetHashCode. Ваша логіка визначення того, чи є два Authorоб'єкти рівними, здається, якщо рядки Імені та Прізвища однакові.

public class AuthorEquals : IEqualityComparer<Author>
{
    public bool Equals(Author left, Author right)
    {
        if((object)left == null && (object)right == null)
        {
            return true;
        }
        if((object)left == null || (object)right == null)
        {
            return false;
        }
        return left.FirstName == right.FirstName && left.LastName == right.LastName;
    }

    public int GetHashCode(Author author)
    {
        return (author.FirstName + author.LastName).GetHashCode();
    }
}

1
Дякую! Ваша реалізація GetHashCode () показала мені те, чого я все ще бракував. Я повертався {переданий об’єкт} .GetHashCode (), а не {властивість використовується для порівняння} .GetHashCode (). Це змінило значення і пояснює, чому мій ще не вдається - два різних посилання мали б два різних хеш-коди.
pelazem

44

Ще одне рішення без впровадження IEquatable, Equalsі GetHashCodeполягає у використанні GroupByметоду LINQ і вибору першого пункту з IGrouping.

var temp = books.SelectMany(book => book.Authors)
                .GroupBy (y => y.FirstName + y.LastName )
                .Select (y => y.First ());

foreach (var author in temp){
  Console.WriteLine(author.FirstName + " " + author.LastName);
}

1
це допомогло мені, тільки враховуючи продуктивність, чи працює це з однаковою швидкістю?
Biswajeet

набагато приємніше, ніж ускладнювати це методами реалізації, і якщо використовувати EF, делегуватиме роботу на сервер sql.
Zapnologica

Хоча цей метод може спрацювати, виникла проблема продуктивності через кількість речей, згрупованих
Bellash

@Bellash Зробіть це, то зробіть це швидким. Впевнений, що це групування може призвести до більшої роботи. але іноді громіздко реалізувати більше, ніж хочеш.
Jehof

2
Я віддаю перевагу цьому рішенню, але потім, використовуючи "новий" об'єкт у групі: .GroupBy(y => new { y.FirstName, y.LastName })
Дейв де Чен,

32

Є ще один спосіб отримати відмінні значення зі списку визначених користувачем типів даних:

YourList.GroupBy(i => i.Id).Select(i => i.FirstOrDefault()).ToList();

Звичайно, це дасть чіткий набір даних


21

Distinct()виконує порівняння рівності за замовчуванням для об'єктів у переліченні. Якщо ви не переотримали Equals()і GetHashCode(), то він використовує реалізацію за замовчуванням на object, яка порівнює посилання.

Просте рішення полягає в тому, щоб додати правильну реалізацію Equals()та GetHashCode()всі класи, які беруть участь у об'єктному графіку, який ви порівнюєте (наприклад, Book and Author).

IEqualityComparerІнтерфейс є зручністю , що дозволяє реалізувати Equals()і GetHashCode()в окремому класі , коли у вас немає доступу до внутрішніх класів , які необхідно порівняти, або якщо ви використовуєте інший метод порівняння.


Дуже дякую за цей яскравий коментар щодо об’єктів, що беруть участь.
suhyura

11

Ви перекрили рівність (), але переконайтесь, що ви також перекрили GetHashCode ()


+1 для підкреслення GetHashCode (). Не додайте базову реалізацію HashCode, як у<custom>^base.GetHashCode()
Дані,

8

Наведені вище відповіді невірні !!! Розрізнення, як зазначено в MSDN, повертає Екватор за замовчуванням, який, як зазначено, Властивість за замовчуванням перевіряє, чи реалізує тип T інтерфейс System.IEquatable і, якщо так, повертає EqualityComparer, який використовує цю реалізацію. В іншому випадку він повертає EqualityComparer, який використовує переопределення Object.Equals і Object.GetHashCode, надані T

Що означає, що поки ти перебуваєш за рівний, ти добре.

Причина, що код не працює, полягає в тому, що ви перевіряєте ім'я == прізвище.

див. https://msdn.microsoft.com/library/bb348436(v=vs.100).aspx та https://msdn.microsoft.com/en-us/library/ms224763(v=vs.100).aspx


0

Ви можете використовувати метод розширення у списку, який перевіряє унікальність на основі обчисленого Hash. Ви також можете змінити метод розширення, щоб підтримувати IEnumerable.

Приклад:

public class Employee{
public string Name{get;set;}
public int Age{get;set;}
}

List<Employee> employees = new List<Employee>();
employees.Add(new Employee{Name="XYZ", Age=30});
employees.Add(new Employee{Name="XYZ", Age=30});

employees = employees.Unique(); //Gives list which contains unique objects. 

Спосіб розширення:

    public static class LinqExtension
        {
            public static List<T> Unique<T>(this List<T> input)
            {
                HashSet<string> uniqueHashes = new HashSet<string>();
                List<T> uniqueItems = new List<T>();

                input.ForEach(x =>
                {
                    string hashCode = ComputeHash(x);

                    if (uniqueHashes.Contains(hashCode))
                    {
                        return;
                    }

                    uniqueHashes.Add(hashCode);
                    uniqueItems.Add(x);
                });

                return uniqueItems;
            }

            private static string ComputeHash<T>(T entity)
            {
                System.Security.Cryptography.SHA1CryptoServiceProvider sh = new System.Security.Cryptography.SHA1CryptoServiceProvider();
                string input = JsonConvert.SerializeObject(entity);

                byte[] originalBytes = ASCIIEncoding.Default.GetBytes(input);
                byte[] encodedBytes = sh.ComputeHash(originalBytes);

                return BitConverter.ToString(encodedBytes).Replace("-", "");
            }

-1

Домогтися цього можна двома способами:

1. Ви можете реалізувати інтерфейс IEquatable, як показано Enumerable.Distinct Method, або ви можете побачити відповідь @ skalb в цьому дописі

2. Якщо ваш об'єкт не має унікального ключа, ви можете використовувати метод GroupBy для отримання різного списку об'єктів, який повинен згрупувати всі властивості об'єкта і після вибору першого об'єкта.

Наприклад, як, як показано нижче, і працює для мене:

var distinctList= list.GroupBy(x => new {
                            Name= x.Name,
                            Phone= x.Phone,
                            Email= x.Email,
                            Country= x.Country
                        }, y=> y)
                       .Select(x => x.First())
                       .ToList()

Клас MyObject виглядає так:

public class MyClass{
       public string Name{get;set;}
       public string Phone{get;set;}
       public string Email{get;set;}
       public string Country{get;set;}
}

3. Якщо у вашого об'єкта є унікальний ключ, ви можете використовувати його лише у групі.

Наприклад, унікальний ключ мого об'єкта - Id.

var distinctList= list.GroupBy(x =>x.Id)
                      .Select(x => x.First())
                      .ToList()
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.