Perbedaan LINQ () pada properti tertentu

1095

Saya bermain dengan LINQ untuk mempelajarinya, tetapi saya tidak dapat menemukan cara menggunakannya Distinctketika saya tidak memiliki daftar sederhana (daftar sederhana bilangan bulat cukup mudah dilakukan, ini bukan pertanyaannya). Bagaimana jika saya ingin menggunakan Distinct pada daftar Object pada satu atau lebih properti objek?

Contoh: Jika suatu objek adalah Person, dengan Properti Id. Bagaimana saya bisa mendapatkan semua Orang dan menggunakannya Distinctdengan properti Idobjek?

Person1: Id=1, Name="Test1"
Person2: Id=1, Name="Test1"
Person3: Id=2, Name="Test2"

Bagaimana saya bisa mendapatkan hanya Person1dan Person3? Apakah itu mungkin?

Jika itu tidak mungkin dengan LINQ, apa cara terbaik untuk memiliki daftar Persontergantung pada beberapa propertinya di .NET 3.5?

Patrick Desjardins
sumber

Jawaban:

1249

EDIT : Ini sekarang bagian dari MoreLINQ .

Yang Anda butuhkan adalah "berbeda" secara efektif. Saya tidak percaya ini adalah bagian dari LINQ sebagaimana adanya, meskipun cukup mudah untuk menulis:

public static IEnumerable<TSource> DistinctBy<TSource, TKey>
    (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
    HashSet<TKey> seenKeys = new HashSet<TKey>();
    foreach (TSource element in source)
    {
        if (seenKeys.Add(keySelector(element)))
        {
            yield return element;
        }
    }
}

Jadi untuk menemukan nilai yang berbeda hanya dengan menggunakan Idproperti, Anda dapat menggunakan:

var query = people.DistinctBy(p => p.Id);

Dan untuk menggunakan beberapa properti, Anda bisa menggunakan tipe anonim, yang menerapkan kesetaraan dengan tepat:

var query = people.DistinctBy(p => new { p.Id, p.Name });

Belum diuji, tetapi harus bekerja (dan sekarang setidaknya mengkompilasi).

Ini mengasumsikan pembanding default untuk kunci - jika Anda ingin meneruskan pembanding kesetaraan, cukup meneruskannya ke HashSetkonstruktor.

Jon Skeet
sumber
1
@ ashes999: Saya tidak yakin apa yang Anda maksud. Kode hadir dalam jawaban dan di perpustakaan - tergantung pada apakah Anda senang untuk mengambil ketergantungan.
Jon Skeet
10
@ ashes999: Jika Anda hanya melakukan ini di satu tempat, maka, tentu saja, menggunakan GroupBylebih mudah. Jika Anda membutuhkannya di lebih dari satu tempat, itu jauh lebih bersih (IMO) untuk merangkum niat.
Jon Skeet
5
@ MatthewWhited: Mengingat bahwa tidak disebutkan di IQueryable<T>sini, saya tidak melihat bagaimana itu relevan. Saya setuju bahwa ini tidak akan cocok untuk EF dll, tetapi dalam LINQ ke Objects saya pikir itu lebih cocok daripada GroupBy. Konteks pertanyaan selalu penting.
Jon Skeet
7
Proyek pindah pada github, inilah kode DistinctBy: github.com/morelinq/MoreLINQ/blob/master/MoreLinq/DistinctBy.cs
Phate01
1858

Bagaimana jika saya ingin mendapatkan daftar berbeda berdasarkan satu atau lebih properti?

Sederhana! Anda ingin mengelompokkan mereka dan memilih pemenang dari grup.

List<Person> distinctPeople = allPeople
  .GroupBy(p => p.PersonId)
  .Select(g => g.First())
  .ToList();

Jika Anda ingin mendefinisikan grup pada beberapa properti, berikut caranya:

List<Person> distinctPeople = allPeople
  .GroupBy(p => new {p.PersonId, p.FavoriteColor} )
  .Select(g => g.First())
  .ToList();
Amy B
sumber
1
@ErenErsonmez yakin. Dengan kode saya yang diposting, jika eksekusi ditunda diinginkan, tinggalkan panggilan ToList.
Amy B
5
Jawaban yang sangat bagus! Realllllly membantu saya dalam Linq-to-Entities didorong dari tampilan sql di mana saya tidak dapat mengubah tampilan. Saya perlu menggunakan FirstOrDefault () daripada First () - semuanya baik.
Alex KeySmith
8
Saya mencobanya dan harus berubah menjadi Pilih (g => g.FirstOrDefault ())
26
@ChapapicSz Tidak. Keduanya Single()dan SingleOrDefault()setiap lemparan ketika sumber memiliki lebih dari satu item. Dalam operasi ini, kami mengharapkan kemungkinan bahwa setiap kelompok dapat memiliki lebih dari satu item. Dalam hal ini, First()lebih disukai daripada FirstOrDefault()karena setiap grup harus memiliki setidaknya satu anggota .... kecuali jika Anda menggunakan EntityFramework, yang tidak dapat mengetahui bahwa setiap grup memiliki setidaknya satu anggota dan permintaan FirstOrDefault().
Amy B
2
Tampaknya saat ini tidak didukung di EF Core, bahkan menggunakan FirstOrDefault() github.com/dotnet/efcore/issues/12088 Saya di 3.1, dan saya mendapatkan "tidak dapat menerjemahkan" kesalahan.
Collin M. Barrett
78

Menggunakan:

List<Person> pList = new List<Person>();
/* Fill list */

var result = pList.Where(p => p.Name != null).GroupBy(p => p.Id).Select(grp => grp.FirstOrDefault());

Ini wheremembantu Anda menyaring entri (bisa lebih kompleks) dan groupbydan selectmelakukan fungsi yang berbeda.

karcsi
sumber
1
Sempurna, dan berfungsi tanpa memperpanjang Linq atau menggunakan dependensi lain.
DavidScherer
77

Anda juga bisa menggunakan sintaks kueri jika Anda ingin semua sintaks seperti LINQ:

var uniquePeople = from p in people
                   group p by new {p.ID} //or group by new {p.ID, p.Name, p.Whatever}
                   into mygroup
                   select mygroup.FirstOrDefault();
Chuck Rostance
sumber
4
Hmm pikiran saya adalah sintaks kueri dan sintaks API lancar sama seperti LINQ satu sama lain dan hanya preferensi yang digunakan orang. Saya sendiri lebih suka API yang lancar jadi saya akan menganggap itu lebih seperti Tautan tapi saya kira itu subjektif
Max Carroll
Seperti LINQ tidak ada hubungannya dengan preferensi, menjadi "seperti LINQ" ada hubungannya dengan tampak seperti bahasa permintaan yang berbeda yang tertanam ke dalam C #, saya lebih suka antarmuka yang lancar, yang berasal dari aliran java, tetapi BUKAN Seperti LINQ.
Ryan The Leach
Luar biasa !! Kamu adalah pahlawanku!
Farzin Kanzi
63

Saya pikir sudah cukup:

list.Select(s => s.MyField).Distinct();
Ivan
sumber
43
Bagaimana jika dia membutuhkan kembali objek penuhnya, bukan hanya bidang khusus itu?
Festim Cahani
1
Apa sebenarnya objek dari beberapa objek yang memiliki nilai properti yang sama?
donRumatta
40

Solusi kelompok pertama dengan bidang Anda kemudian pilih item firstordefault.

    List<Person> distinctPeople = allPeople
   .GroupBy(p => p.PersonId)
   .Select(g => g.FirstOrDefault())
   .ToList();
cahit beyaz
sumber
26

Anda dapat melakukan ini dengan standar Linq.ToLookup(). Ini akan membuat kumpulan nilai untuk setiap kunci unik. Cukup pilih item pertama dalam koleksi

Persons.ToLookup(p => p.Id).Select(coll => coll.First());
David Fahlander
sumber
17

Kode berikut secara fungsional setara dengan jawaban Jon Skeet .

Diuji pada .NET 4.5, harus berfungsi pada versi LINQ sebelumnya.

public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
  this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
  HashSet<TKey> seenKeys = new HashSet<TKey>();
  return source.Where(element => seenKeys.Add(keySelector(element)));
}

Secara kebetulan, lihat versi terbaru Jon Skeet tentang DistinctBy.cs di Google Code .

Contango
sumber
3
Ini memberi saya "urutan tidak memiliki kesalahan nilai", tetapi jawaban Skeet menghasilkan hasil yang benar.
What Would Be Cool
10

Saya telah menulis sebuah artikel yang menjelaskan cara memperluas fungsi Distinct sehingga Anda dapat melakukan hal berikut:

var people = new List<Person>();

people.Add(new Person(1, "a", "b"));
people.Add(new Person(2, "c", "d"));
people.Add(new Person(1, "a", "b"));

foreach (var person in people.Distinct(p => p.ID))
    // Do stuff with unique list here.

Inilah artikelnya: Memperluas LINQ - Menentukan Properti dalam Fungsi Berbeda

Timothy Khouri
sumber
3
Artikel Anda memiliki kesalahan, harus ada <T> setelah Distinct: public static IEnumerable <T> Distinct (this ... Juga tidak terlihat seperti itu akan bekerja (baik) pada lebih dari satu properti yaitu kombinasi dari yang pertama dan nama belakang
row1
2
+1, kesalahan kecil bukan alasan yang cukup untuk downvote, yang konyol, sering diketik salah ketik. Dan saya belum melihat fungsi generik yang akan bekerja untuk sejumlah properti! Saya harap downvoter telah menurunkan semua jawaban di utas ini juga. Tapi hei apa jenis objek kedua ini ?? Saya keberatan !
nawfal
4
Tautan Anda rusak
Tom Lint
7

Secara pribadi saya menggunakan kelas berikut:

public class LambdaEqualityComparer<TSource, TDest> : 
    IEqualityComparer<TSource>
{
    private Func<TSource, TDest> _selector;

    public LambdaEqualityComparer(Func<TSource, TDest> selector)
    {
        _selector = selector;
    }

    public bool Equals(TSource obj, TSource other)
    {
        return _selector(obj).Equals(_selector(other));
    }

    public int GetHashCode(TSource obj)
    {
        return _selector(obj).GetHashCode();
    }
}

Kemudian, metode ekstensi:

public static IEnumerable<TSource> Distinct<TSource, TCompare>(
    this IEnumerable<TSource> source, Func<TSource, TCompare> selector)
{
    return source.Distinct(new LambdaEqualityComparer<TSource, TCompare>(selector));
}

Akhirnya, penggunaan yang dimaksud:

var dates = new List<DateTime>() { /* ... */ }
var distinctYears = dates.Distinct(date => date.Year);

Keuntungan yang saya temukan menggunakan pendekatan ini adalah penggunaan kembali LambdaEqualityComparerkelas untuk metode lain yang menerima IEqualityComparer. (Oh, dan saya menyerahkan yieldbarang ke implementasi LINQ asli ...)

Joel
sumber
5

Jika Anda memerlukan metode Berbeda pada beberapa properti, Anda dapat melihat perpustakaan PowerfulExtensions saya . Saat ini masih dalam tahap yang sangat muda, tetapi Anda sudah dapat menggunakan metode seperti Distinct, Union, Intersect, Kecuali pada sejumlah properti;

Ini adalah bagaimana Anda menggunakannya:

using PowerfulExtensions.Linq;
...
var distinct = myArray.Distinct(x => x.A, x => x.B);
Andrzej Gis
sumber
5

Ketika kami menghadapi tugas seperti itu dalam proyek kami, kami mendefinisikan API kecil untuk menyusun komparator.

Jadi, use casenya seperti ini:

var wordComparer = KeyEqualityComparer.Null<Word>().
    ThenBy(item => item.Text).
    ThenBy(item => item.LangID);
...
source.Select(...).Distinct(wordComparer);

Dan API itu sendiri terlihat seperti ini:

using System;
using System.Collections;
using System.Collections.Generic;

public static class KeyEqualityComparer
{
    public static IEqualityComparer<T> Null<T>()
    {
        return null;
    }

    public static IEqualityComparer<T> EqualityComparerBy<T, K>(
        this IEnumerable<T> source,
        Func<T, K> keyFunc)
    {
        return new KeyEqualityComparer<T, K>(keyFunc);
    }

    public static KeyEqualityComparer<T, K> ThenBy<T, K>(
        this IEqualityComparer<T> equalityComparer,
        Func<T, K> keyFunc)
    {
        return new KeyEqualityComparer<T, K>(keyFunc, equalityComparer);
    }
}

public struct KeyEqualityComparer<T, K>: IEqualityComparer<T>
{
    public KeyEqualityComparer(
        Func<T, K> keyFunc,
        IEqualityComparer<T> equalityComparer = null)
    {
        KeyFunc = keyFunc;
        EqualityComparer = equalityComparer;
    }

    public bool Equals(T x, T y)
    {
        return ((EqualityComparer == null) || EqualityComparer.Equals(x, y)) &&
                EqualityComparer<K>.Default.Equals(KeyFunc(x), KeyFunc(y));
    }

    public int GetHashCode(T obj)
    {
        var hash = EqualityComparer<K>.Default.GetHashCode(KeyFunc(obj));

        if (EqualityComparer != null)
        {
            var hash2 = EqualityComparer.GetHashCode(obj);

            hash ^= (hash2 << 5) + hash2;
        }

        return hash;
    }

    public readonly Func<T, K> KeyFunc;
    public readonly IEqualityComparer<T> EqualityComparer;
}

Rincian lebih lanjut ada di situs kami: IEqualityComparer di LINQ .

Vladimir Nesterovsky
sumber
5

Anda bisa menggunakan DistinctBy () untuk mendapatkan catatan berbeda oleh properti objek. Cukup tambahkan pernyataan berikut sebelum menggunakannya:

menggunakan Microsoft.Ajax.Utilities;

dan kemudian gunakan seperti berikut:

var listToReturn = responseList.DistinctBy(x => x.Index).ToList();

di mana 'Indeks' adalah properti tempat saya ingin data berbeda.

Harry .Naeem
sumber
4

Anda dapat melakukannya (walaupun tidak secepat kilat) seperti:

people.Where(p => !people.Any(q => (p != q && p.Id == q.Id)));

Yaitu, "pilih semua orang di mana tidak ada orang lain yang berbeda dalam daftar dengan ID yang sama."

Ingat, dalam contoh Anda, itu hanya akan memilih orang 3. Saya tidak yakin bagaimana mengatakan mana yang Anda inginkan, dari dua sebelumnya.

mqp
sumber
4

Jika Anda tidak ingin menambahkan pustaka MoreLinq ke proyek Anda hanya untuk mendapatkan DistinctByfungsionalitas maka Anda bisa mendapatkan hasil akhir yang sama menggunakan kelebihan Distinctmetode Linq yang membutuhkan IEqualityComparerargumen.

Anda mulai dengan membuat kelas pembanding kesetaraan khusus generik yang menggunakan sintaks lambda untuk melakukan perbandingan kustom dua contoh kelas generik:

public class CustomEqualityComparer<T> : IEqualityComparer<T>
{
    Func<T, T, bool> _comparison;
    Func<T, int> _hashCodeFactory;

    public CustomEqualityComparer(Func<T, T, bool> comparison, Func<T, int> hashCodeFactory)
    {
        _comparison = comparison;
        _hashCodeFactory = hashCodeFactory;
    }

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

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

Kemudian dalam kode utama Anda, Anda menggunakannya seperti ini:

Func<Person, Person, bool> areEqual = (p1, p2) => int.Equals(p1.Id, p2.Id);

Func<Person, int> getHashCode = (p) => p.Id.GetHashCode();

var query = people.Distinct(new CustomEqualityComparer<Person>(areEqual, getHashCode));

Voila! :)

Di atas mengasumsikan sebagai berikut:

  • Properti Person.Id adalah tipeint
  • The peoplekoleksi tidak mengandung unsur nol

Jika koleksi dapat berisi nol maka cukup tulis ulang lambda untuk memeriksa nol, misalnya:

Func<Person, Person, bool> areEqual = (p1, p2) => 
{
    return (p1 != null && p2 != null) ? int.Equals(p1.Id, p2.Id) : false;
};

EDIT

Pendekatan ini mirip dengan yang ada di jawaban Vladimir Nesterovsky tetapi lebih sederhana.

Ini juga mirip dengan yang ada di jawaban Joel tetapi memungkinkan untuk logika perbandingan kompleks yang melibatkan beberapa properti.

Namun, jika objek Anda hanya dapat berbeda Idsaat itu maka pengguna lain memberikan jawaban yang benar bahwa semua yang perlu Anda lakukan adalah mengesampingkan implementasi default dari GetHashCode()dan Equals()di Personkelas Anda dan kemudian hanya menggunakan Distinct()metode Linq out-of-the-box untuk menyaring duplikat.

Kaspia Canuck
sumber
Saya hanya ingin mendapatkan item unik dalam kamus, Bisakah Anda membantu, saya menggunakan kode ini Jika TempDT Tidak Ada Apa-apa Lalu m_ConcurrentScriptDictionary = TempDT.AsEnumerable.ToDictionary (Function (x) x.SafeField (fldClusterId, NULL_ID_VALUE), Function (y) y.SafeField (fldParamValue11, NULL_ID_VALUE))
RSB
2

Cara terbaik untuk melakukan ini yang akan kompatibel dengan versi .NET lainnya adalah dengan mengesampingkan Persamaan dan GetHash untuk menangani ini (lihat pertanyaan Stack Overflow Kode ini mengembalikan nilai yang berbeda. Namun, yang saya inginkan adalah mengembalikan koleksi yang sangat diketik sebagai lawan dari tipe anonim ), tetapi jika Anda memerlukan sesuatu yang generik di seluruh kode Anda, solusi dalam artikel ini sangat bagus.

gcoleman0828
sumber
1
List<Person>lst=new List<Person>
        var result1 = lst.OrderByDescending(a => a.ID).Select(a =>new Player {ID=a.ID,Name=a.Name} ).Distinct();
Arindam
sumber
Apakah maksud Anda Select() new Personalih-alih new Player? Namun, fakta bahwa Anda memesan IDtidak memberitahu Distinct()untuk menggunakan properti itu dalam menentukan keunikan, jadi ini tidak akan berhasil.
BACON
1

Override Equals (object obj) dan GetHashCode () metode:

class Person
{
    public int Id { get; set; }
    public int Name { get; set; }

    public override bool Equals(object obj)
    {
        return ((Person)obj).Id == Id;
        // or: 
        // var o = (Person)obj;
        // return o.Id == Id && o.Name == Name;
    }
    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

dan kemudian panggil saja:

List<Person> distinctList = new[] { person1, person2, person3 }.Distinct().ToList();
Waldemar Gałęzinowski
sumber
Namun GetHashCode () harus lebih maju (untuk menghitung juga Nama), jawaban ini mungkin yang terbaik menurut saya. Sebenarnya, untuk mengarsipkan logika target, tidak perlu mengesampingkan GetHashCode (), Equals () sudah cukup, tetapi jika kita membutuhkan kinerja, kita harus menimpanya. Semua perbandingan algs, cek hash pertama, dan jika keduanya sama maka panggil Equals ().
Oleg Skripnyak
Juga, ada dalam Persamaan () baris pertama harus "jika (! (Orang adalah Person)) kembali salah". Tetapi praktik terbaik adalah menggunakan objek terpisah yang dilemparkan ke tipe, seperti "var o = obj sebagai Person; jika (o == null) return false;" kemudian periksa kesetaraan dengan o tanpa casting
Oleg Skripnyak
1
Mengesampingkan Persamaan seperti ini bukan ide yang baik karena bisa memiliki konsekuensi yang tidak diinginkan bagi programmer lain yang mengharapkan Kesetaraan Orang ditentukan pada lebih dari satu properti.
B2K
0

Anda harus dapat mengesampingkan Persamaan pada orang untuk benar-benar melakukan Persamaan di Person.id. Ini seharusnya menghasilkan perilaku yang Anda cari.

GWLlosa
sumber
-5

Silakan coba dengan kode di bawah ini.

var Item = GetAll().GroupBy(x => x .Id).ToList();
Mohamed Hammam
sumber
3
Jawaban singkat dipersilahkan, namun itu tidak akan memberikan banyak nilai kepada pengguna yang terakhir yang mencoba memahami apa yang terjadi di balik masalah. Luangkan waktu untuk menjelaskan apa masalah sebenarnya yang menyebabkan masalah dan bagaimana menyelesaikannya. Terima kasih ~
Dengar