Distinct не работает с LINQ to Objects

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.

Это напечатает Джона Скита дважды. Зачем? Я даже попытался переопределить метод Equals в классе Author. По-прежнему Distinct не работает. Что мне не хватает?

Изменить: я также добавил перегрузку оператора == и! =. По-прежнему никакой помощи.

 public static bool operator ==(Author a, Author b)
    {
        return true;
    }
    public static bool operator !=(Author a, Author b)
    {
        return false;
    }
Tanmoy
источник

Ответы:

159

LINQ Distinct не так умен, когда дело касается настраиваемых объектов.

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

Один из способов решения этой проблемы - реализовать интерфейс IEquatable, как показано здесь .

Если вы измените свой класс Author, он должен работать.

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

skalb
источник
22
IEquatable - это хорошо, но неполно; вы всегда должны реализовывать Object.Equals () и Object.GetHashCode () вместе; IEquatable <T> .Equals не переопределяет Object.Equals, поэтому это не удастся при выполнении нестрого типизированных сравнений, что часто происходит во фреймворках и всегда в неуниверсальных коллекциях.
AndyM 02
Так что лучше использовать переопределение Distinct, которое принимает IEqualityComparer <T>, как предложил Рекс М.? Я имею в виду то, что мне следует делать, если я не хочу попасть в ловушку.
Tanmoy 02
3
@Tanmoy это зависит. Если вы хотите, чтобы Author обычно вел себя как обычный объект (т. Е. Только ссылочное равенство), но проверял значения имен для целей Distinct, используйте IEqualityComparer. Если вы всегда хотите, чтобы объекты Author сравнивались на основе значений имен, переопределите GetHashCode и Equals или реализуйте IEquatable.
Rex M
3
Я реализовал IEquatable(и переопределил Equals/ GetHashCode), но ни одна из моих точек останова не срабатывает в этих методах на Linq Distinct?
PeterX
2
@PeterX Я тоже это заметил. У меня были точки останова в GetHashCodeи Equals, они были достигнуты во время цикла foreach. Это связано с тем, что var temp = books.SelectMany(book => book.Authors).Distinct();возвращается IEnumerableзначение, что означает, что запрос не выполняется сразу, он выполняется только при использовании данных. Если вам нужен пример этого запуска прямо сейчас, добавьте .ToList()после, .Distinct()и вы увидите точки останова в Equalsи GetHashCodeперед foreach.
JabberwockyDecompiler
70

Distinct()Метод проверяет равенство ссылок для ссылочных типов. Это означает, что он ищет буквально один и тот же дублированный объект, а не разные объекты, содержащие одинаковые значения.

Существует перегрузка, которая использует IEqualityComparer , поэтому вы можете указать другую логику для определения того, равен ли данный объект другому.

Если вы хотите, чтобы Author обычно вел себя как обычный объект (т. Е. Только эталонное равенство), но для целей Distinct проверять равенство по значениям имен, используйте IEqualityComparer . Если вы всегда хотите, чтобы объекты Author сравнивались на основе значений имен, переопределите 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заключается в использовании LINQs GroupByметода и для выбора первого элемента из 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);
}
Jehof
источник
1
это помогло мне, просто учитывая производительность, работает ли это с той же скоростью ?, как с учетом вышеуказанных методов?
Бисваджит
намного лучше, чем усложнять его реализацией методов, и при использовании EF делегирует работу серверу sql.
Zapnologica
хотя этот метод может работать, возникнут проблемы с производительностью из-за
большого
@Bellash Заставьте это работать, а затем сделайте это быстро. Конечно, эта группировка может привести к тому, что предстоит проделать еще больше работы. но иногда сложно реализовать больше, чем вы хотите.
Jehof
2
Я предпочитаю это решение, но с использованием «нового» объекта в группе. Автор: .GroupBy(y => new { y.FirstName, y.LastName })
Дэйв де Йонг,
32

Есть еще один способ получить отдельные значения из списка пользовательских типов данных:

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

Конечно, это даст отчетливый набор данных

Ashu_90
источник
21

Distinct()выполняет сравнение равенства по умолчанию для объектов в перечисляемом. Если вы не переопределили Equals()и GetHashCode(), тогда будет использоваться реализация по умолчанию on object, которая сравнивает ссылки.

Простое решение - добавить правильную реализацию Equals()и GetHashCode()ко всем классам, которые участвуют в графе объектов, который вы сравниваете (например, Книга и Автор).

IEqualityComparerИнтерфейс является удобством , что позволяет реализовать Equals()и GetHashCode()в отдельном классе , когда у вас нет доступа к внутренним классам , которые необходимо сравнить, или если вы используете другой метод сравнения.

AndyM
источник
Большое спасибо за этот блестящий отзыв об участвующих объектах.
Suhyura
11

Вы переопределили Equals (), но не забудьте также переопределить GetHashCode ()

Эрик Кинг
источник
+1 за выделение GetHashCode (). Не добавляйте базовую реализацию HashCode, как в<custom>^base.GetHashCode()
Дэни
8

Вышеуказанные ответы неверны !!! Distinct, как указано в MSDN, возвращает Equator по умолчанию, который, как указано, Свойство Default проверяет, реализует ли тип T интерфейс System.IEquatable, и, если да, возвращает EqualityComparer, который использует эту реализацию. В противном случае он возвращает EqualityComparer, который использует переопределения Object.Equals и Object.GetHashCode, предоставленные T

Это означает, что пока вы переигрываете Equals, с вами все в порядке.

Причина, по которой ваш код не работает, заключается в том, что вы проверяете имя == фамилию.

см. https://msdn.microsoft.com/library/bb348436(v=vs.100).aspx и https://msdn.microsoft.com/en-us/library/ms224763(v=vs.100).aspx

Alex
источник
0

Вы можете использовать метод расширения в списке, который проверяет уникальность на основе вычисленного хэша. Вы также можете изменить метод расширения для поддержки 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("-", "");
            }
Chindirala Sampath Kumar
источник
-1

Вы можете добиться этого двумя способами:

1. Вы можете реализовать интерфейс IEquatable, как показано в методе Enumerable.Distinct, или вы можете увидеть ответ @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()
Рамиль Алиев
источник