Berbeda tidak bekerja dengan LINQ ke Objek

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

}

Ini didasarkan pada contoh dalam "LINQ in Action". Daftar 4.16.

Ini mencetak Jon Skeet dua kali. Mengapa? Saya bahkan telah mencoba mengesampingkan metode Setara di kelas Penulis. Still Distinct sepertinya tidak berhasil. Apa yang saya lewatkan?

Sunting: Saya telah menambahkan == dan! = Kelebihan operator juga. Masih tidak ada bantuan.

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

Jawaban:

159

LINQ Distinct tidak begitu pintar dalam hal objek khusus.

Yang dilakukannya hanyalah melihat daftar Anda dan melihat bahwa ia memiliki dua objek yang berbeda (tidak peduli mereka memiliki nilai yang sama untuk bidang anggota).

Salah satu solusinya adalah dengan mengimplementasikan antarmuka IEquatable seperti yang ditunjukkan di sini .

Jika Anda mengubah kelas Penulis Anda seperti itu seharusnya berfungsi.

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

Cobalah sebagai DotNetFiddle

skalb
sumber
22
IEquatable baik-baik saja tetapi tidak lengkap; Anda harus selalu mengimplementasikan Object.Equals () dan Object.GetHashCode () bersama-sama; IEquatable <T> .Equals tidak menimpa Object.Equals, jadi ini akan gagal saat membuat perbandingan yang tidak diketik dengan kuat, yang sering terjadi dalam framework dan selalu dalam koleksi non-generik.
AndyM
Jadi, apakah lebih baik menggunakan penggantian Distinct yang menggunakan IEqualityComparer <T> seperti yang disarankan Rex M? Maksud saya apa yang harus saya lakukan jika saya tidak ingin jatuh ke dalam perangkap.
Tanmoy
3
@Tanoy tergantung. Jika Anda ingin Penulis berperilaku normal seperti objek normal (yaitu hanya persamaan referensi) tetapi periksa nilai nama untuk tujuan Distinct, gunakan IEqualityComparer. Jika Anda selalu ingin objek Author dibandingkan berdasarkan nilai nama, maka timpa GetHashCode dan Equals, atau terapkan IEquatable.
Rex M
3
Saya menerapkan IEquatable(dan menimpa Equals/ GetHashCode) tetapi tidak ada titik putus saya yang aktif dalam metode ini di Linq Distinct?
PeterX
2
@PeterX Saya memperhatikan ini juga. Saya memiliki breakpoint di GetHashCodedan Equals, mereka terkena selama loop foreach. Ini karena var temp = books.SelectMany(book => book.Authors).Distinct();return an IEnumerable, artinya request tidak langsung dieksekusi, hanya dieksekusi saat data digunakan. Jika Anda ingin contoh penembakan ini segera, tambahkan .ToList()setelah .Distinct()dan Anda akan melihat breakpoints di Equalsdan GetHashCodesebelum foreach.
JabberwockyDecompiler
70

The Distinct()kesetaraan referensi Metode pemeriksaan untuk jenis referensi. Ini berarti secara harfiah mencari objek yang sama digandakan, bukan objek berbeda yang berisi nilai yang sama.

Ada kelebihan beban yang membutuhkan IEqualityComparer , sehingga Anda dapat menentukan logika yang berbeda untuk menentukan apakah suatu objek sama dengan yang lain.

Jika Anda ingin Penulis berperilaku seperti objek normal (yaitu hanya persamaan referensi), tetapi untuk tujuan persamaan periksa Distinct berdasarkan nilai nama, gunakan IEqualityComparer . Jika Anda selalu ingin objek Author dibandingkan berdasarkan nilai nama, maka timpa GetHashCode dan Equals , atau terapkan IEquatable .

Dua anggota pada IEqualityComparerantarmuka adalah Equalsdan GetHashCode. Logika Anda untuk menentukan apakah dua Authorobjek sama tampaknya jika string nama Depan dan Belakang sama.

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();
    }
}
Rex M
sumber
1
Terima kasih! Implementasi GetHashCode () Anda menunjukkan apa yang masih saya lewatkan. Saya mengembalikan {objek yang diteruskan} .GetHashCode (), bukan {properti yang digunakan untuk perbandingan} .GetHashCode (). Itu membuat perbedaan dan menjelaskan mengapa saya masih gagal - dua referensi berbeda akan memiliki dua kode hash yang berbeda.
pelazem
44

Solusi lain tanpa implementasi IEquatable, Equalsdan GetHashCodeadalah dengan menggunakan GroupBymetode LINQs dan untuk memilih item pertama dari 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
sumber
1
itu membantu saya, hanya dengan mempertimbangkan kinerja, apakah ini bekerja pada kecepatan yang sama ?, seperti mempertimbangkan metode di atas?
Biswajeet
jauh lebih baik daripada memperumitnya dengan menerapkan metode, dan jika menggunakan EF akan mendelegasikan pekerjaan ke sql server.
Zapnologica
sementara metode ini dapat berfungsi, akan ada masalah kinerja karena jumlah hal yang dikelompokkan
Bellash
@Bellash Jadikan bekerja lalu buat cepat. Tentu apakah pengelompokan ini dapat menyebabkan lebih banyak pekerjaan yang harus dilakukan. tetapi terkadang sulit untuk menerapkan lebih dari yang Anda inginkan.
Jehof
2
Saya lebih suka solusi ini tetapi kemudian dengan menggunakan objek "baru" di groupby: .GroupBy(y => new { y.FirstName, y.LastName })
Dave de Jong
32

Ada satu cara lagi untuk mendapatkan nilai yang berbeda dari daftar tipe data yang ditentukan pengguna:

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

Tentunya akan memberikan kumpulan data yang berbeda

Ashu_90
sumber
21

Distinct()melakukan perbandingan kesetaraan default pada objek di enumerable. Jika Anda belum diganti Equals()dan GetHashCode(), kemudian menggunakan implementasi default pada object, yang membandingkan referensi.

Solusi sederhana adalah menambahkan implementasi yang benar dari Equals()dan GetHashCode()ke semua kelas yang berpartisipasi dalam grafik objek yang Anda bandingkan (yaitu Buku dan Penulis).

The IEqualityComparerantarmuka adalah kenyamanan yang memungkinkan Anda untuk menerapkan Equals()dan GetHashCode()di kelas terpisah ketika Anda tidak memiliki akses ke internal kelas Anda perlu membandingkan, atau jika Anda menggunakan metode yang berbeda dari perbandingan.

AndyM
sumber
Terima kasih banyak atas komentar cemerlang tentang objek yang berpartisipasi ini.
suhyura
11

Anda telah mengganti Equals (), tetapi pastikan Anda juga mengganti GetHashCode ()

Eric King
sumber
+1 untuk menekankan GetHashCode (). Jangan menambahkan implementasi HashCode dasar seperti dalam<custom>^base.GetHashCode()
Dani
8

Jawaban di atas salah !!! Berbeda seperti yang dinyatakan di MSDN mengembalikan Equator default yang dinyatakan Properti Default memeriksa apakah tipe T mengimplementasikan antarmuka System.IEquatable dan, jika demikian, mengembalikan EqualityComparer yang menggunakan implementasi itu. Jika tidak, ia mengembalikan EqualityComparer yang menggunakan penggantian Object.Equals dan Object.GetHashCode yang disediakan oleh T

Yang berarti selama Anda melebihi sama Anda baik-baik saja.

Alasan kode Anda tidak berfungsi adalah karena Anda memeriksa nama depan == nama belakang.

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

Alex
sumber
0

Anda dapat menggunakan metode ekstensi pada daftar yang memeriksa keunikan berdasarkan Hash yang dihitung. Anda juga dapat mengubah metode ekstensi untuk mendukung IEnumerable.

Contoh:

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. 

Metode Ekstensi:

    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
sumber
-1

Anda dapat mencapai ini dengan dua cara:

1. Anda dapat mengimplementasikan antarmuka IEquatable seperti yang ditunjukkan Enumerable.Distinct Method atau Anda dapat melihat jawaban @ skalb di posting ini

2. Jika objek Anda tidak memiliki kunci unik, Anda dapat menggunakan metode GroupBy untuk mencapai daftar objek berbeda, bahwa Anda harus mengelompokkan semua properti objek dan setelah memilih objek pertama.

Misalnya seperti di bawah ini dan bekerja untuk saya:

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

Kelas MyObject seperti di bawah ini:

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

3. Jika objek Anda memiliki kunci unik, Anda hanya dapat menggunakannya dalam kelompok dengan.

Misalnya kunci unik objek saya adalah Id.

var distinctList= list.GroupBy(x =>x.Id)
                      .Select(x => x.First())
                      .ToList()
Ramil Aliyev
sumber