Berbeda () dengan lambda?

746

Benar, jadi saya punya enumerable dan ingin mendapatkan nilai yang berbeda dari itu.

Menggunakan System.Linq, tentu saja ada metode ekstensi yang disebut Distinct. Dalam kasus sederhana, ini dapat digunakan tanpa parameter, seperti:

var distinctValues = myStringList.Distinct();

Baik dan bagus, tetapi jika saya memiliki banyak objek yang harus saya tentukan persamaannya, satu-satunya kelebihan yang tersedia adalah:

var distinctValues = myCustomerList.Distinct(someEqualityComparer);

Argumen pembanding kesetaraan harus merupakan turunan dari IEqualityComparer<T>. Saya bisa melakukan ini, tentu saja, tetapi agak bertele-tele dan, yah, tidak jelas.

Apa yang saya harapkan adalah kelebihan yang akan mengambil lambda, katakanlah Func <T, T, bool>:

var distinctValues
    = myCustomerList.Distinct((c1, c2) => c1.CustomerId == c2.CustomerId);

Adakah yang tahu jika ada ekstensi seperti itu, atau beberapa solusi yang setara? Atau apakah saya melewatkan sesuatu?

Atau, apakah ada cara untuk menentukan inline IEqualityComparer (mempermalukan saya)?

Memperbarui

Saya menemukan balasan oleh Anders Hejlsberg ke sebuah posting di forum MSDN tentang hal ini. Dia berkata:

Masalah yang akan Anda hadapi adalah ketika dua objek membandingkan sama, mereka harus memiliki nilai balik GetHashCode yang sama (atau tabel hash yang digunakan secara internal oleh Distinct tidak akan berfungsi dengan benar). Kami menggunakan IEqualityComparer karena ini mengemas implementasi Equals dan GetHashCode yang kompatibel ke dalam satu antarmuka.

Saya kira itu masuk akal ..

Tor Haugen
sumber
2
lihat stackoverflow.com/questions/1183403/... untuk solusi menggunakan GroupBy
17
Terima kasih atas pembaruan Anders Hejlsberg!
Tor Haugen
Tidak, itu tidak masuk akal - bagaimana dua objek yang berisi nilai yang identik dapat mengembalikan dua kode hash yang berbeda ??
GY
Ini bisa membantu - solusi untuk .Distinct(new KeyEqualityComparer<Customer,string>(c1 => c1.CustomerId)), dan menjelaskan mengapa GetHashCode () penting untuk bekerja dengan baik.
marbel82
Duplikat terkait / kemungkinan: LINQ's Distinct () pada properti tertentu
Marc.2377

Jawaban:

1029
IEnumerable<Customer> filteredList = originalList
  .GroupBy(customer => customer.CustomerId)
  .Select(group => group.First());
Carlo Bos
sumber
12
Luar biasa! Ini juga sangat mudah untuk dienkapsulasi dalam metode ekstensi, seperti DistinctBy(atau bahkan Distinct, karena tanda tangannya akan unik).
Tomas Aschan
1
Tidak bekerja untuk saya! <Metode 'Pertama' hanya dapat digunakan sebagai operasi permintaan akhir. Pertimbangkan untuk menggunakan metode 'FirstOrDefault' dalam contoh ini sebagai gantinya.> Bahkan saya mencoba 'FirstOrDefault' itu tidak berhasil.
JatSing
63
@ Torhaugen: Perlu diketahui bahwa ada biaya yang diperlukan untuk membuat semua grup itu. Ini tidak dapat mengalirkan input, dan akan berakhir dengan buffering semua data sebelum mengembalikan apa pun. Itu mungkin tidak relevan untuk situasi Anda tentu saja, tetapi saya lebih suka keanggunan dari DistinctBy :)
Jon Skeet
2
@ JonSkeet: Ini cukup baik untuk coders VB.NET yang tidak ingin mengimpor perpustakaan tambahan hanya untuk satu fitur. Tanpa ASync CTP, VB.NET tidak mendukung yieldpernyataan tersebut sehingga streaming tidak dimungkinkan secara teknis. Terima kasih atas jawaban Anda. Saya akan menggunakannya ketika coding di C #. ;-)
Alex Essilfie
2
@ BenGripka: Itu tidak persis sama. Itu hanya memberi Anda id pelanggan. Saya ingin seluruh pelanggan :)
ryanman
496

Sepertinya saya inginkan DistinctBydari MoreLINQ . Anda kemudian dapat menulis:

var distinctValues = myCustomerList.DistinctBy(c => c.CustomerId);

Berikut adalah versi cut-down dari DistinctBy(tidak ada nullity memeriksa dan tidak ada opsi untuk menentukan pembanding kunci Anda sendiri):

public static IEnumerable<TSource> DistinctBy<TSource, TKey>
     (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
    HashSet<TKey> knownKeys = new HashSet<TKey>();
    foreach (TSource element in source)
    {
        if (knownKeys.Add(keySelector(element)))
        {
            yield return element;
        }
    }
}
Jon Skeet
sumber
14
Saya tahu jawaban terbaik akan diposting oleh Jon Skeet hanya dengan membaca judul posting. Jika itu ada hubungannya dengan LINQ, Skeet adalah orangmu. Baca 'C # In Depth' untuk mendapatkan pengetahuan LINQ seperti Tuhan.
nocarrier
2
jawaban yang bagus !!! juga, untuk semua VB_Pengaduan tentang yield+ lib ekstra, foreach dapat ditulis ulang sebagaireturn source.Where(element => knownKeys.Add(keySelector(element)));
denis morozov
5
@ sudhAnsu63 ini adalah batasan LinqToSql (dan penyedia linq lainnya). Maksud dari LinqToX adalah untuk menerjemahkan ekspresi C # lambda Anda ke dalam konteks asli X. Artinya, LinqToSql mengubah C # Anda menjadi SQL dan mengeksekusi perintah itu secara native jika memungkinkan. Ini berarti setiap metode yang berada di C # tidak dapat "melewati" sebuah linqProvider jika tidak ada cara untuk mengekspresikannya dalam SQL (atau penyedia linq apa pun yang Anda gunakan). Saya melihat ini dalam metode ekstensi untuk mengkonversi objek data untuk melihat model. Anda dapat mengatasi ini dengan "mematerialisasi" kueri, memanggil ToList () sebelum DistinctBy ().
Michael Blackburn
1
Dan setiap kali saya kembali ke pertanyaan ini saya terus bertanya-tanya mengapa mereka tidak mengadopsi setidaknya beberapa MoreLinq ke dalam BCL.
Shimmy Weitzhandler
2
@ Kimmy: Saya tentu saja menyambut itu ... Saya tidak yakin apa kelayakannya. Saya dapat meningkatkannya di .NET Foundation ...
Jon Skeet
39

Membungkus semuanya . Saya pikir sebagian besar orang yang datang ke sini seperti saya menginginkan solusi sesederhana mungkin tanpa menggunakan perpustakaan dan dengan kinerja terbaik .

(Grup yang diterima dengan metode untuk saya, saya pikir adalah berlebihan dalam hal kinerja.)

Berikut ini adalah metode ekstensi sederhana menggunakan antarmuka IEqualityComparer yang juga berfungsi untuk nilai null.

Pemakaian:

var filtered = taskList.DistinctBy(t => t.TaskExternalId).ToArray();

Kode Metode Perpanjangan

public static class LinqExtensions
{
    public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> items, Func<T, TKey> property)
    {
        GeneralPropertyComparer<T, TKey> comparer = new GeneralPropertyComparer<T,TKey>(property);
        return items.Distinct(comparer);
    }   
}
public class GeneralPropertyComparer<T,TKey> : IEqualityComparer<T>
{
    private Func<T, TKey> expr { get; set; }
    public GeneralPropertyComparer (Func<T, TKey> expr)
    {
        this.expr = expr;
    }
    public bool Equals(T left, T right)
    {
        var leftProp = expr.Invoke(left);
        var rightProp = expr.Invoke(right);
        if (leftProp == null && rightProp == null)
            return true;
        else if (leftProp == null ^ rightProp == null)
            return false;
        else
            return leftProp.Equals(rightProp);
    }
    public int GetHashCode(T obj)
    {
        var prop = expr.Invoke(obj);
        return (prop==null)? 0:prop.GetHashCode();
    }
}
Anestis Kivranoglou
sumber
19

Tidak, tidak ada metode ekstensi yang berlebihan untuk ini. Saya merasa frustrasi di masa lalu dan karena itu saya biasanya menulis kelas pembantu untuk mengatasi masalah ini. Tujuannya adalah untuk mengkonversi Func<T,T,bool>ke IEqualityComparer<T,T>.

Contoh

public class EqualityFactory {
  private sealed class Impl<T> : IEqualityComparer<T,T> {
    private Func<T,T,bool> m_del;
    private IEqualityComparer<T> m_comp;
    public Impl(Func<T,T,bool> del) { 
      m_del = del;
      m_comp = EqualityComparer<T>.Default;
    }
    public bool Equals(T left, T right) {
      return m_del(left, right);
    } 
    public int GetHashCode(T value) {
      return m_comp.GetHashCode(value);
    }
  }
  public static IEqualityComparer<T,T> Create<T>(Func<T,T,bool> del) {
    return new Impl<T>(del);
  }
}

Ini memungkinkan Anda untuk menulis yang berikut ini

var distinctValues = myCustomerList
  .Distinct(EqualityFactory.Create((c1, c2) => c1.CustomerId == c2.CustomerId));
JaredPar
sumber
8
Itu memiliki implementasi kode hash jahat sekalipun. Lebih mudah untuk membuat IEqualityComparer<T>dari proyeksi: stackoverflow.com/questions/188120/...
Jon Skeet
7
(Hanya untuk menjelaskan komentar saya tentang kode hash - sangat mudah dengan kode ini untuk berakhir dengan Persamaan (x, y) == true, tapi GetHashCode (x)! = GetHashCode (y). Pada dasarnya merusak apa pun seperti hashtable .)
Jon Skeet
Saya setuju dengan keberatan kode hash. Namun, +1 untuk polanya.
Tor Haugen
@ Jon, ya saya setuju implementasi asli GetHashcode kurang optimal (sedang malas). Saya beralih ke dasarnya menggunakan sekarang EqualityComparer <T> .Default.GetHashcode () yang sedikit lebih standar. Sejujurnya, satu-satunya implementasi GetHashcode yang dijamin untuk bekerja dalam skenario ini adalah dengan mengembalikan nilai konstan. Membunuh pencarian yang hashtable tetapi dijamin benar secara fungsional.
JaredPar
1
@JaredPar: Tepat. Kode hash harus konsisten dengan fungsi kesetaraan yang Anda gunakan, yang mungkin bukan yang standar kalau tidak Anda tidak akan repot :) Itu sebabnya saya lebih suka menggunakan proyeksi - Anda bisa mendapatkan kesetaraan dan hash yang masuk akal kode seperti itu. Itu juga membuat kode panggilan memiliki duplikasi lebih sedikit. Diakui hanya bekerja dalam kasus di mana Anda ingin proyeksi yang sama dua kali, tapi itu setiap kasus yang pernah kulihat dalam praktek :)
Jon Skeet
18

Solusi singkat

myCustomerList.GroupBy(c => c.CustomerId, (key, c) => c.FirstOrDefault());
Arasu RRK
sumber
1
Bisakah Anda menambahkan beberapa penjelasan mengapa ini diperbaiki?
Keith Pinson
Ini benar-benar bekerja untuk saya dengan baik ketika Konrad tidak.
neoscribe
13

Ini akan melakukan apa yang Anda inginkan tetapi saya tidak tahu tentang kinerja:

var distinctValues =
    from cust in myCustomerList
    group cust by cust.CustomerId
    into gcust
    select gcust.First();

Setidaknya itu tidak bertele-tele.

Gordon Freeman
sumber
12

Inilah metode ekstensi sederhana yang melakukan apa yang saya butuhkan ...

public static class EnumerableExtensions
{
    public static IEnumerable<TKey> Distinct<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector)
    {
        return source.GroupBy(selector).Select(x => x.Key);
    }
}

Sayang mereka tidak memanggang metode yang berbeda seperti ini ke dalam kerangka kerja, tapi hei ho.

David Kirkland
sumber
ini adalah solusi terbaik tanpa harus menambahkan perpustakaan itu morelinq.
toddmo
Tapi, saya harus mengubah x.Keyke x.First()dan mengubah nilai kembali keIEnumerable<T>
toddmo
@toddmo Terima kasih atas umpan baliknya :-) Ya, kedengarannya logis ... Saya akan memperbarui jawabannya setelah menyelidiki lebih lanjut.
David Kirkland
1
tidak pernah ada kata terlambat untuk mengucapkan terima kasih atas solusinya, sederhana dan bersih
Ali
4

Sesuatu yang saya gunakan yang bekerja dengan baik untuk saya.

/// <summary>
/// A class to wrap the IEqualityComparer interface into matching functions for simple implementation
/// </summary>
/// <typeparam name="T">The type of object to be compared</typeparam>
public class MyIEqualityComparer<T> : IEqualityComparer<T>
{
    /// <summary>
    /// Create a new comparer based on the given Equals and GetHashCode methods
    /// </summary>
    /// <param name="equals">The method to compute equals of two T instances</param>
    /// <param name="getHashCode">The method to compute a hashcode for a T instance</param>
    public MyIEqualityComparer(Func<T, T, bool> equals, Func<T, int> getHashCode)
    {
        if (equals == null)
            throw new ArgumentNullException("equals", "Equals parameter is required for all MyIEqualityComparer instances");
        EqualsMethod = equals;
        GetHashCodeMethod = getHashCode;
    }
    /// <summary>
    /// Gets the method used to compute equals
    /// </summary>
    public Func<T, T, bool> EqualsMethod { get; private set; }
    /// <summary>
    /// Gets the method used to compute a hash code
    /// </summary>
    public Func<T, int> GetHashCodeMethod { get; private set; }

    bool IEqualityComparer<T>.Equals(T x, T y)
    {
        return EqualsMethod(x, y);
    }

    int IEqualityComparer<T>.GetHashCode(T obj)
    {
        if (GetHashCodeMethod == null)
            return obj.GetHashCode();
        return GetHashCodeMethod(obj);
    }
}
Kleinux
sumber
@Mukus Saya tidak yakin mengapa Anda bertanya tentang nama kelas di sini. Saya perlu memberi nama kelas sesuatu untuk menerapkan IEqualityComparer jadi saya hanya awalan My.
Kleinux
4

Semua solusi yang saya lihat di sini bergantung pada pemilihan bidang yang sudah dapat dibandingkan. Jika seseorang perlu membandingkan dengan cara yang berbeda, solusi ini tampaknya berfungsi secara umum, untuk sesuatu seperti:

somedoubles.Distinct(new LambdaComparer<double>((x, y) => Math.Abs(x - y) < double.Epsilon)).Count()
Dmitry Ledentsov
sumber
Apa itu LambdaComparer, dari mana Anda mengimpor itu?
Patrick Graham
@ PatrickGraham ditautkan dalam jawaban: brendan.enrick.com/post/…
Dmitry Ledentsov
3

Ambil jalan lain:

var distinctValues = myCustomerList.
Select(x => x._myCaustomerProperty).Distinct();

Urutan mengembalikan elemen berbeda membandingkannya dengan properti '_myCaustomerProperty'.

Bob
sumber
1
Datang ke sini untuk mengatakan ini. INI harus menjadi jawaban yang diterima
Still.Tony
5
Tidak, ini seharusnya bukan jawaban yang diterima, kecuali yang Anda inginkan adalah nilai berbeda dari properti kustom. Pertanyaan OP yang umum adalah bagaimana mengembalikan objek yang berbeda berdasarkan pada properti spesifik dari objek tersebut.
tomo
2

Anda dapat menggunakan InlineComparer

public class InlineComparer<T> : IEqualityComparer<T>
{
    //private readonly Func<T, T, bool> equalsMethod;
    //private readonly Func<T, int> getHashCodeMethod;
    public Func<T, T, bool> EqualsMethod { get; private set; }
    public Func<T, int> GetHashCodeMethod { get; private set; }

    public InlineComparer(Func<T, T, bool> equals, Func<T, int> hashCode)
    {
        if (equals == null) throw new ArgumentNullException("equals", "Equals parameter is required for all InlineComparer instances");
        EqualsMethod = equals;
        GetHashCodeMethod = hashCode;
    }

    public bool Equals(T x, T y)
    {
        return EqualsMethod(x, y);
    }

    public int GetHashCode(T obj)
    {
        if (GetHashCodeMethod == null) return obj.GetHashCode();
        return GetHashCodeMethod(obj);
    }
}

Sampel penggunaan :

  var comparer = new InlineComparer<DetalleLog>((i1, i2) => i1.PeticionEV == i2.PeticionEV && i1.Etiqueta == i2.Etiqueta, i => i.PeticionEV.GetHashCode() + i.Etiqueta.GetHashCode());
  var peticionesEV = listaLogs.Distinct(comparer).ToList();
  Assert.IsNotNull(peticionesEV);
  Assert.AreNotEqual(0, peticionesEV.Count);

Sumber: https://stackoverflow.com/a/5969691/206730
Menggunakan IEqualityComparer untuk Union
Bisakah saya menentukan inline komparator tipe eksplisit saya?

Kiquenet
sumber
2

Anda dapat menggunakan LambdaEqualityComparer:

var distinctValues
    = myCustomerList.Distinct(new LambdaEqualityComparer<OurType>((c1, c2) => c1.CustomerId == c2.CustomerId));


public class LambdaEqualityComparer<T> : IEqualityComparer<T>
    {
        public LambdaEqualityComparer(Func<T, T, bool> equalsFunction)
        {
            _equalsFunction = equalsFunction;
        }

        public bool Equals(T x, T y)
        {
            return _equalsFunction(x, y);
        }

        public int GetHashCode(T obj)
        {
            return obj.GetHashCode();
        }

        private readonly Func<T, T, bool> _equalsFunction;
    }
Валентин Миронов
sumber
1

Cara rumit untuk melakukan ini adalah menggunakan Aggregate()ekstensi, menggunakan kamus sebagai akumulator dengan nilai properti-kunci sebagai kunci:

var customers = new List<Customer>();

var distincts = customers.Aggregate(new Dictionary<int, Customer>(), 
                                    (d, e) => { d[e.CustomerId] = e; return d; },
                                    d => d.Values);

Dan solusi gaya GroupBy menggunakan ToLookup():

var distincts = customers.ToLookup(c => c.CustomerId).Select(g => g.First());
Arturo Menchaca
sumber
Bagus, tapi mengapa tidak membuat Dictionary<int, Customer>saja?
ruffin
0

Saya berasumsi Anda memiliki IEnumerable, dan dalam delegasi contoh Anda, Anda ingin c1 dan c2 merujuk pada dua elemen dalam daftar ini?

Saya yakin Anda bisa mencapainya dengan bergabung sendiri var differResults = dari c1 di myList, gabung di c2 di myList on

MattH
sumber
0

Jika Distinct()tidak menghasilkan hasil yang unik, coba yang ini:

var filteredWC = tblWorkCenter.GroupBy(cc => cc.WCID_I).Select(grp => grp.First()).Select(cc => new Model.WorkCenter { WCID = cc.WCID_I }).OrderBy(cc => cc.WCID); 

ObservableCollection<Model.WorkCenter> WorkCenter = new ObservableCollection<Model.WorkCenter>(filteredWC);
Andy Singh
sumber
0

Paket Microsoft System.Interactive memiliki versi Perbedaan yang mengambil lambda pemilih kunci. Ini secara efektif sama dengan solusi Jon Skeet, tetapi mungkin bermanfaat bagi orang untuk mengetahuinya, dan untuk memeriksa perpustakaan lainnya.

Niall Connaughton
sumber
0

Inilah cara Anda dapat melakukannya:

public static class Extensions
{
    public static IEnumerable<T> MyDistinct<T, V>(this IEnumerable<T> query,
                                                    Func<T, V> f, 
                                                    Func<IGrouping<V,T>,T> h=null)
    {
        if (h==null) h=(x => x.First());
        return query.GroupBy(f).Select(h);
    }
}

Metode ini memungkinkan Anda untuk menggunakannya dengan menentukan satu parameter seperti .MyDistinct(d => d.Name), tetapi juga memungkinkan Anda menentukan kondisi memiliki sebagai parameter kedua seperti:

var myQuery = (from x in _myObject select x).MyDistinct(d => d.Name,
        x => x.FirstOrDefault(y=>y.Name.Contains("1") || y.Name.Contains("2"))
        );

NB Ini juga akan memungkinkan Anda untuk menentukan fungsi lain seperti misalnya .LastOrDefault(...)juga.


Jika Anda ingin mengekspos kondisi ini, Anda dapat membuatnya lebih sederhana dengan menerapkannya sebagai:

public static IEnumerable<T> MyDistinct2<T, V>(this IEnumerable<T> query,
                                                Func<T, V> f,
                                                Func<T,bool> h=null
                                                )
{
    if (h == null) h = (y => true);
    return query.GroupBy(f).Select(x=>x.FirstOrDefault(h));
}

Dalam hal ini, kueri akan terlihat seperti:

var myQuery2 = (from x in _myObject select x).MyDistinct2(d => d.Name,
                    y => y.Name.Contains("1") || y.Name.Contains("2")
                    );

NB Di sini, ekspresinya lebih sederhana, tetapi note .MyDistinct2menggunakan .FirstOrDefault(...)secara implisit.


Catatan: Contoh di atas menggunakan kelas demo berikut

class MyObject
{
    public string Name;
    public string Code;
}

private MyObject[] _myObject = {
    new MyObject() { Name = "Test1", Code = "T"},
    new MyObject() { Name = "Test2", Code = "Q"},
    new MyObject() { Name = "Test2", Code = "T"},
    new MyObject() { Name = "Test5", Code = "Q"}
};
Mat
sumber
0

IEnumerable ekstensi lambda:

public static class ListExtensions
{        
    public static IEnumerable<T> Distinct<T>(this IEnumerable<T> list, Func<T, int> hashCode)
    {
        Dictionary<int, T> hashCodeDic = new Dictionary<int, T>();

        list.ToList().ForEach(t => 
            {   
                var key = hashCode(t);
                if (!hashCodeDic.ContainsKey(key))
                    hashCodeDic.Add(key, t);
            });

        return hashCodeDic.Select(kvp => kvp.Value);
    }
}

Pemakaian:

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

//Add 5 employees to List
List<Employee> lst = new List<Employee>();

Employee e = new Employee { Name = "Shantanu", EmployeeID = 123456 };
lst.Add(e);
lst.Add(e);

Employee e1 = new Employee { Name = "Adam Warren", EmployeeID = 823456 };
lst.Add(e1);
//Add a space in the Name
Employee e2 = new Employee { Name = "Adam  Warren", EmployeeID = 823456 };
lst.Add(e2);
//Name is different case
Employee e3 = new Employee { Name = "adam warren", EmployeeID = 823456 };
lst.Add(e3);            

//Distinct (without IEqalityComparer<T>) - Returns 4 employees
var lstDistinct1 = lst.Distinct();

//Lambda Extension - Return 2 employees
var lstDistinct = lst.Distinct(employee => employee.EmployeeID.GetHashCode() ^ employee.Name.ToUpper().Replace(" ", "").GetHashCode()); 
Shantanu
sumber