LINQ to Objects ile çalışmayan ayırt edici


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

}

Bu, "LINQ in Action" daki bir örneğe dayanmaktadır. 4.16 listesi.

Bu, Jon Skeet'i iki kez basar. Neden? Yazar sınıfında Eşittir yöntemini geçersiz kılmayı bile denedim. Yine de Distinct işe yaramıyor gibi görünüyor. Neyi kaçırıyorum?

Düzenleme: == ve! = Operatör aşırı yüklemesini de ekledim. Hala yardım yok.

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

Yanıtlar:


159

LINQ Distinct, özel nesneler söz konusu olduğunda o kadar akıllı değildir.

Tek yaptığı listeye bakmak ve iki farklı nesneye sahip olduğunu görmektir (üye alanları için aynı değerlere sahip olmaları umurunda değildir).

Çözümlerden biri, burada gösterildiği gibi IEquatable arabirimini uygulamaktır .

Yazar sınıfınızı böyle değiştirirseniz çalışmalıdır.

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 olarak deneyin


22
IEquatable iyi ama eksik; yapmanız gerekir her zaman implemement Object.Equals () ve Object.GetHashCode () birlikte; IEquatable <T> .Equals, Object.Equals'ı geçersiz kılmaz, bu nedenle, genellikle çerçevelerde ve her zaman genel olmayan koleksiyonlarda meydana gelen, güçlü olmayan tipte karşılaştırmalar yaparken bu başarısız olur.
AndyM

Öyleyse, Rex M'nin önerdiği gibi IEqualityComparer <T> alan Distinct'i geçersiz kılmak daha mı iyi? Tuzağa düşmek istemiyorsam ne yapmam gerektiğini kastediyorum.
Tanmoy

3
@Tanmoy bağlıdır. Yazarın normalde normal bir nesne gibi davranmasını (yani yalnızca referans eşitliği), ancak Distinct amacıyla ad değerlerini kontrol etmesini istiyorsanız bir IEqualityComparer kullanın. Eğer varsa hep Yazar nesneleri isim değerlerine dayalı karşılaştırılabilir istiyorum, sonra GetHashCode geçersiz kılmak ve Eşittir veya IEquatable uygulamak.
Rex M

3
Ben uyguladım IEquatable(ve üzerine yazdım Equals/ GetHashCode), ancak kesme noktalarımın hiçbiri bu yöntemlerde bir Linq Distinct?
PeterX

2
@PeterX Bunu ben de fark ettim. Ben de kesme noktaları vardı GetHashCodeve Equalsonlar foreach döngüsü sırasında vuruldu. Bunun nedeni, var temp = books.SelectMany(book => book.Authors).Distinct();an'ın döndürülmesidir IEnumerable, yani isteğin hemen yerine getirilmemesi, yalnızca veri kullanıldığında yürütülür. Hemen bu ateş bir örneğini istiyorsanız, o zaman eklemek .ToList()sonra .Distinct()ve kesme noktaları göreceksiniz Equalsve GetHashCodeforeach önce.
JabberwockyDecompiler

70

Distinct()Başvuru türleri için bir yöntem kontrol referans eşitliği. Bu, aynı değerleri içeren farklı nesneleri değil, tam anlamıyla aynı nesneyi aradığı anlamına gelir.

IEqualityComparer alan bir aşırı yükleme vardır , bu nedenle belirli bir nesnenin diğerine eşit olup olmadığını belirlemek için farklı mantık belirleyebilirsiniz.

Yazarın normalde normal bir nesne gibi davranmasını istiyorsanız (yani yalnızca referans eşitliği), ancak ad değerlerine göre Eşitlik Eşitliğini Ayırmak amacıyla bir IEqualityComparer kullanın . Yazar nesnelerinin her zaman ad değerlerine göre karşılaştırılmasını istiyorsanız, GetHashCode ve Equals'ı geçersiz kılın veya IEquatable'ı uygulayın .

IEqualityComparerArayüzdeki iki üye Equalsve GetHashCode. İki Authornesnenin eşit olup olmadığını belirleme mantığınız, Ad ve Soyad dizelerinin aynı olması gibi görünür.

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
Teşekkür ederim! GetHashCode () uygulamanız bana hala neyi kaçırdığımı gösterdi. {Karşılaştırma için kullanılan özellik} .GetHashCode () değil, {geçirilen nesne} .GetHashCode () döndürüyordum. Bu fark yarattı ve benimkinin neden hala başarısız olduğunu açıklıyor - iki farklı referansın iki farklı karma kodu olacaktır.
pelazem

44

Uygulamadan Başka bir çözüm IEquatable, Equalsve GetHashCodeLINQs kullanmaktır GroupByyöntem ve IGrouping gelen ilk öğeyi seçin.

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
bana yardımcı oldu, sadece performansı göz önünde bulundurarak, bu, yukarıdaki yöntemler dikkate alındığında aynı hızda mı çalışıyor?
Biswajeet

yöntemleri uygulama ile karmaşıklaştırmaktan çok daha iyi ve EF kullanıyorsanız, işi sql sunucusuna devredecektir.
Zapnologica

Bu yöntem işe yarayabilirken, gruplanan şeylerin sayısı nedeniyle bir performans sorunu olacaktır
Bellash 09

@Bellash Çalışmasını sağlayın ve ardından hızlı hale getirin. Elbette bu gruplama daha fazla işin yapılmasına yol açabilir mi? ancak bazen istediğinizden fazlasını uygulamak zahmetlidir.
Jehof

2
Bu çözümü tercih ediyorum ama sonra grupta "yeni" bir nesne kullanarak: .GroupBy(y => new { y.FirstName, y.LastName })
Dave de Jong

32

Kullanıcı tanımlı veri türü listesinden farklı değerler almanın bir yolu daha vardır:

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

Elbette, farklı bir veri kümesi verecektir.


21

Distinct()numaralandırılabilir nesneler üzerinde varsayılan eşitlik karşılaştırmasını gerçekleştirir. Eğer geçersiz kılmadıysanız Equals()ve GetHashCode(), o zaman objectreferansları karşılaştıran varsayılan uygulamayı kullanır .

Basit bir çözüm bir eklemektir doğru uygulanmasını Equals()ve GetHashCode()sen (yani Kitap ve Yazar) karşılaştırdığınız nesne grafiğinde katılan tüm sınıflara.

IEqualityComparerArayüzü uygulamaya izin veren bir kolaylık Equals()ve GetHashCode()ayrı bir sınıfta size karşılaştırmanın farklı bir yöntem kullanıyorsanız, karşılaştırmak gerekir, ya da sınıfların iç yapısına erişimi yoktur zaman.


Katılan nesnelerle ilgili bu parlak yorum için çok teşekkür ederim.
suhyura

11

Eşittir () öğesini geçersiz kıldınız, ancak GetHashCode () öğesini de geçersiz kıldığınızdan emin olun.


GetHashCode () öğesini vurgulamak için +1. Temel HashCode uygulamasını şu şekilde eklemeyin<custom>^base.GetHashCode()
Dani

8

Yukarıdaki cevaplar yanlış !!! MSDN'de belirtildiği gibi farklı, belirtildiği gibi varsayılan Equator'u döndürür Varsayılan özellik, T türünün System.IEquatable arabirimini uygulayıp uygulamadığını kontrol eder ve eğer öyleyse, bu uygulamayı kullanan bir EqualityComparer döndürür. Aksi takdirde, T tarafından sağlanan Object.Equals ve Object.GetHashCode geçersiz kılmalarını kullanan bir EqualityComparer döndürür.

Bu demek oluyor ki, üstesinden geldiğin sürece Eşittir iyisin.

Kodunuzun çalışmamasının nedeni, ad == soyad'ı kontrol etmenizdir.

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


0

Hesaplanan Hash'e göre benzersizliği kontrol eden listede uzantı yöntemini kullanabilirsiniz. IEnumerable'ı desteklemek için uzantı yöntemini de değiştirebilirsiniz.

Misal:

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. 

Uzatma Yöntemi:

    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

Bunu iki yolla başarabilirsiniz:

1. IEquatable arayüzünü Enumerable.Distinct Metodu'nda gösterildiği gibi uygulayabilir veya bu yazıda @ skalb'ın cevabını görebilirsiniz.

2. Nesnenizin benzersiz anahtarı yoksa, nesnenin tüm özelliklerini gruplamanız gereken ve ilk nesneyi seçtikten sonra, farklı nesne listesi elde etmek için GroupBy yöntemini kullanabilirsiniz.

Örneğin aşağıdaki gibi ve benim için çalışıyor:

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 sınıfı aşağıdaki gibidir:

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

3. Nesnenizin benzersiz anahtarı varsa, onu yalnızca grup halinde kullanabilirsiniz.

Örneğin, nesnemin benzersiz anahtarı Id'dir.

var distinctList= list.GroupBy(x =>x.Id)
                      .Select(x => x.First())
                      .ToList()
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.