Khác biệt không hoạt động với LINQ cho các đối tượng


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;
    }

}

Điều này dựa trên một ví dụ trong "LINQ đang hoạt động". Liệt kê 4.16.

Điều này in Jon Skeet hai lần. Tại sao? Tôi thậm chí đã thử ghi đè phương thức Equals trong lớp Tác giả. Vẫn có vẻ như không hoạt động. Tôi đang thiếu gì?

Chỉnh sửa: Tôi cũng đã thêm toán tử == và! = Quá tải. Vẫn không được giúp đỡ.

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

Câu trả lời:


159

LINQ Distinct không thông minh lắm khi nói đến các đối tượng tùy chỉnh.

Tất cả những gì nó làm là nhìn vào danh sách của bạn và thấy rằng nó có hai đối tượng khác nhau (nó không quan tâm đến việc chúng có cùng giá trị cho các trường thành viên).

Một cách giải quyết là thực hiện giao diện IEquatable như được hiển thị ở đây .

Nếu bạn sửa đổi lớp Tác giả của mình như vậy thì nó sẽ hoạt động.

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;
    }
}

Hãy thử nó như DotNetFiddle


22
IEquatable tốt nhưng không đầy đủ; bạn nên luôn ghép Object.Equals () và Object.GetHashCode () với nhau; IEquatable <T> .Equals không ghi đè Object.Equals, vì vậy điều này sẽ không thành công khi thực hiện các so sánh không được đánh máy mạnh, thường xảy ra trong các khuôn khổ và luôn nằm trong các bộ sưu tập không chung chung.
AndyM 02/09/09

Vì vậy, tốt hơn là sử dụng ghi đè của Distinction lấy IEqualityComparer <T> như Rex M đã đề xuất? Ý tôi là tôi nên làm gì nếu tôi không muốn rơi vào bẫy.
Tanmoy 02/09/09

3
@Tanmoy thì tùy. Nếu bạn muốn Tác giả bình thường hoạt động như một đối tượng bình thường (tức là chỉ bình đẳng tham chiếu) nhưng kiểm tra các giá trị tên cho mục đích Phân biệt, hãy sử dụng IEqualityComparer. Nếu bạn luôn muốn các đối tượng Tác giả được so sánh dựa trên các giá trị tên, thì hãy ghi đè GetHashCode và Equals hoặc triển khai IEquatable.
Rex M

3
Tôi đã triển khai IEquatable(và ghi đè Equals/ GetHashCode) nhưng không có điểm ngắt nào của tôi được kích hoạt trong các phương pháp này trên Linq Distinct?
PeterX

2
@PeterX Tôi cũng nhận thấy điều này. Tôi có các điểm ngắt trong GetHashCodeEquals, chúng đã bị đánh trong vòng lặp foreach. Điều này là do var temp = books.SelectMany(book => book.Authors).Distinct();trả về an IEnumerable, nghĩa là yêu cầu không được thực hiện ngay lập tức, nó chỉ được thực thi khi dữ liệu được sử dụng. Nếu bạn muốn có một ví dụ về cách kích hoạt này ngay lập tức, hãy thêm .ToList()vào sau dấu .Distinct()và bạn sẽ thấy các điểm ngắt trong EqualsGetHashCodetrước foreach.
JabberwockyDecompiler

70

Các Distinct()bình đẳng phương pháp kiểm tra tài liệu tham khảo đối với các loại tài liệu tham khảo. Điều này có nghĩa là nó đang tìm kiếm theo nghĩa đen cùng một đối tượng được sao chép, không phải các đối tượng khác nhau có chứa các giá trị giống nhau.

Có một tình trạng quá tải chiếm một IEqualityComparer , vì vậy bạn có thể chỉ định logic khác nhau để xác định xem một đối tượng nhất định có bằng một đối tượng khác hay không.

Nếu bạn muốn Tác giả bình thường hoạt động như một đối tượng bình thường (tức là chỉ bình đẳng tham chiếu), nhưng với mục đích Kiểm tra phân biệt bình đẳng theo giá trị tên, hãy sử dụng IEqualityComparer . Nếu bạn luôn muốn các đối tượng Tác giả được so sánh dựa trên các giá trị tên, thì hãy ghi đè GetHashCode và Equals hoặc triển khai IEquatable .

Hai thành viên trên IEqualityComparergiao diện là EqualsGetHashCode. Logic của bạn để xác định xem hai Authorđối tượng có bằng nhau hay không nếu chuỗi Họ và Tên giống nhau.

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
Cảm ơn bạn! Việc triển khai GetHashCode () của bạn đã cho tôi thấy những gì tôi vẫn còn thiếu. Tôi đang trả về {đối tượng được truyền vào} .GetHashCode (), không phải {thuộc tính đang được sử dụng để so sánh} .GetHashCode (). Điều đó đã tạo ra sự khác biệt và giải thích tại sao của tôi vẫn không thành công - hai tham chiếu khác nhau sẽ có hai mã băm khác nhau.
pelazem

44

Một giải pháp mà không thực hiện IEquatable, EqualsGetHashCodelà sử dụng LINQs GroupByphương pháp và chọn mục đầu tiên từ 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
nó đã giúp tôi, chỉ xem xét hiệu suất, điều này có hoạt động ở cùng tốc độ không ?, như xem xét các phương pháp ở trên?
Biswajeet

đẹp hơn nhiều so với việc phức tạp hóa nó với các phương thức triển khai, và nếu sử dụng EF sẽ ủy thác công việc cho máy chủ sql.
Zapnologica,

trong khi phương pháp này có thể làm việc, sẽ có một vấn đề hiệu suất do số lượng của những thứ được nhóm lại
Bellash

@Bellash Làm cho nó hoạt động sau đó làm cho nó nhanh chóng. Chắc chắn việc nhóm này có thể dẫn đến nhiều việc phải làm hơn. nhưng đôi khi nó là rườm rà để thực hiện nhiều hơn bạn muốn.
Jehof

2
Tôi thích giải pháp này nhưng sau đó bằng cách sử dụng một đối tượng "mới" trong groupby: .GroupBy(y => new { y.FirstName, y.LastName })
Dave de Jong

32

Có một cách khác để nhận các giá trị khác biệt từ danh sách kiểu dữ liệu do người dùng xác định:

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

Chắc chắn, nó sẽ cung cấp tập hợp dữ liệu riêng biệt


21

Distinct()thực hiện so sánh bình đẳng mặc định trên các đối tượng trong liệt kê. Nếu bạn chưa ghi đè Equals()GetHashCode(), thì nó sử dụng triển khai mặc định trên objectđể so sánh các tham chiếu.

Giải pháp đơn giản là thêm một triển khai đúngEquals()GetHashCode()cho tất cả các lớp tham gia vào biểu đồ đối tượng mà bạn đang so sánh (tức là Sách và Tác giả).

Các IEqualityComparergiao diện là một sự tiện lợi cho phép bạn thực hiện Equals()GetHashCode()trong một lớp học riêng biệt khi bạn không có quyền truy cập vào bên trong của lớp bạn cần phải so sánh, hoặc nếu bạn đang sử dụng một phương pháp khác nhau để so sánh.


Cảm ơn bạn rất nhiều về ý kiến ​​thú vị này về các đối tượng tham gia.
suhyura

11

Bạn đã ghi đè Equals (), nhưng hãy đảm bảo rằng bạn cũng ghi đè GetHashCode ()


+1 để nhấn mạnh GetHashCode (). Không thêm triển khai HashCode cơ sở như trong<custom>^base.GetHashCode()
Dani

8

Các câu trả lời trên là sai !!! Phân biệt như đã nêu trên MSDN trả về Xích đạo mặc định như đã nêu Thuộc tính Mặc định kiểm tra xem kiểu T có triển khai giao diện System.IEquatable hay không và nếu có, trả về EqualityComparer sử dụng việc triển khai đó. Nếu không, nó trả về một EqualityComparer sử dụng ghi đè của Object.Equals và Object.GetHashCode được cung cấp bởi T

Có nghĩa là miễn là bạn ghi đè lên Bằng thì bạn vẫn ổn.

Lý do mã của bạn không hoạt động là vì bạn kiểm tra tên đầu tiên = tên họ.

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


0

Bạn có thể sử dụng phương pháp mở rộng trên danh sách để kiểm tra tính duy nhất dựa trên Hash được tính toán. Bạn cũng có thể thay đổi phương thức mở rộng để hỗ trợ IEnumerable.

Thí dụ:

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. 

Phương pháp mở rộng:

    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

Bạn có thể đạt được điều này bằng hai cách:

1. Bạn có thể triển khai giao diện IEquatable như hiển thị Phương pháp Enumerable.Distinct hoặc bạn có thể xem câu trả lời của @ skalb tại bài đăng này

2. Nếu đối tượng của bạn không có khóa duy nhất, Bạn có thể sử dụng phương thức GroupBy để tìm kiếm danh sách đối tượng riêng biệt, bạn phải nhóm tất cả các thuộc tính của đối tượng và sau khi chọn đối tượng đầu tiên.

Ví dụ như dưới đây và làm việc cho tôi:

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()

Lớp MyObject giống như bên dưới:

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

3. Nếu đối tượng của bạn có khóa duy nhất, bạn chỉ có thể sử dụng nó theo nhóm.

Ví dụ, khóa duy nhất của đối tượng của tôi là Id.

var distinctList= list.GroupBy(x =>x.Id)
                      .Select(x => x.First())
                      .ToList()
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.