Konversi daftar ke kamus menggunakan LINQ dan tidak khawatir tentang duplikat

163

Saya punya daftar objek Person. Saya ingin mengonversi ke Kamus di mana kuncinya adalah nama depan dan belakang (digabungkan) dan nilainya adalah objek Orang.

Masalahnya adalah saya memiliki beberapa orang yang terduplikasi, jadi ini meledak jika saya menggunakan kode ini:

private Dictionary<string, Person> _people = new Dictionary<string, Person>();

_people = personList.ToDictionary(
    e => e.FirstandLastName,
    StringComparer.OrdinalIgnoreCase);

Saya tahu ini kedengarannya aneh, tetapi saya tidak begitu peduli dengan nama duplikat untuk saat ini. Jika ada banyak nama, saya hanya ingin mengambil satu. Apakah ada yang bisa saya tuliskan kode ini di atas sehingga hanya membutuhkan satu nama dan tidak meledak pada duplikat?

leora
sumber
1
Duplikat (berdasarkan kunci), saya tidak yakin apakah Anda ingin menyimpannya atau kehilangan mereka? Menjaga mereka akan membutuhkan Dictionary<string, List<Person>>(atau setara).
Anthony Pegram
@Anthony Pegram - hanya ingin mempertahankan salah satunya. saya memperbarui pertanyaan menjadi lebih eksplisit
leora
baik Anda dapat menggunakan yang berbeda sebelum melakukan ToDictionary. tetapi Anda harus mengganti metode Equals () dan GetHashCode () untuk kelas orang sehingga CLR tahu cara membandingkan objek orang
Sujit.Warrier
@ Sujit.Warrier - Anda juga bisa membuat pembanding kesetaraan untuk dilewatiDistinct
Kyle Delaney

Jawaban:

71

Inilah solusi yang jelas dan tidak linq:

foreach(var person in personList)
{
  if(!myDictionary.Keys.Contains(person.FirstAndLastName))
    myDictionary.Add(person.FirstAndLastName, person);
}
Carra
sumber
207
thats so 2007 :)
leora
3
itu tidak mengabaikan kasus
on
Ya, sudah saatnya kami memperbarui dari .net 2.0 framework di kantor ... @onof Tidak sulit untuk mengabaikan case. Cukup tambahkan semua kunci dalam huruf besar.
Carra
bagaimana saya membuat kasus ini tidak sensitif
leora
11
Atau buat kamus dengan StringComparer yang akan mengabaikan case, jika itu yang Anda butuhkan, maka kode tambah / periksa Anda tidak peduli jika Anda mengabaikan case atau tidak.
Binary Worrier
423

Solusi LINQ:

// Use the first value in group
var _people = personList
    .GroupBy(p => p.FirstandLastName, StringComparer.OrdinalIgnoreCase)
    .ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);

// Use the last value in group
var _people = personList
    .GroupBy(p => p.FirstandLastName, StringComparer.OrdinalIgnoreCase)
    .ToDictionary(g => g.Key, g => g.Last(), StringComparer.OrdinalIgnoreCase);

Jika Anda lebih suka solusi non-LINQ maka Anda dapat melakukan sesuatu seperti ini:

// Use the first value in list
var _people = new Dictionary<string, Person>(StringComparer.OrdinalIgnoreCase);
foreach (var p in personList)
{
    if (!_people.ContainsKey(p.FirstandLastName))
        _people[p.FirstandLastName] = p;
}

// Use the last value in list
var _people = new Dictionary<string, Person>(StringComparer.OrdinalIgnoreCase);
foreach (var p in personList)
{
    _people[p.FirstandLastName] = p;
}
LukeH
sumber
2
+1 sangat elegan (saya akan memilih ASAP - tidak punya lagi suara untuk hari ini :))
onof
6
@LukeH Minor note: dua snippet Anda tidak setara: varian LINQ mempertahankan elemen pertama, snippet non-LINQ mempertahankan elemen terakhir?
toong
4
@ Toong: Itu benar dan pasti patut dicatat. (Meskipun dalam hal ini OP tampaknya tidak peduli dengan elemen mana mereka berakhir.)
LukeH
1
Untuk kasus "nilai pertama": solusi nonLinq melakukan pencarian kamus dua kali tetapi Linq melakukan instantiasi dan iterasi objek yang berlebihan. Keduanya tidak ideal.
SerG
Pencarian @SerG Untungnya kamus umumnya dianggap sebagai operasi O (1) dan memiliki dampak yang diabaikan.
MHollis
42

Solusi Linq menggunakan Distinct () dan dan tanpa pengelompokan adalah:

var _people = personList
    .Select(item => new { Key = item.Key, FirstAndLastName = item.FirstAndLastName })
    .Distinct()
    .ToDictionary(item => item.Key, item => item.FirstFirstAndLastName, StringComparer.OrdinalIgnoreCase);

Saya tidak tahu apakah itu lebih baik daripada solusi LukeH tetapi berfungsi juga.

Tillito
sumber
Apakah Anda yakin itu berhasil? Bagaimana Distinct akan membandingkan jenis referensi baru yang Anda buat? Saya akan berpikir Anda harus melewati semacam IEqualityComparer ke Distinct untuk mendapatkan pekerjaan ini sebagaimana dimaksud.
Simon Gillbee
5
Abaikan komentar saya sebelumnya. Lihat stackoverflow.com/questions/543482/…
Simon Gillbee
Jika Anda ingin mengganti perbedaan yang ditentukan, periksa stackoverflow.com/questions/489258/…
James McMahon
30

Ini harus bekerja dengan ekspresi lambda:

personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
Ankit Dass
sumber
2
Itu harus:personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
Gh61
4
Ini hanya akan berfungsi jika IEqualityComparer default untuk kelas Person membandingkan dengan nama depan dan belakang, mengabaikan case. Kalau tidak, tulis IEqualityComparer seperti itu dan gunakan kelebihan Perbedaan yang relevan. Metode ToDIctionary Anda juga harus menggunakan pembanding yang tidak peka terhadap huruf besar-kecil agar sesuai dengan persyaratan OP.
Joe
13

Anda juga dapat menggunakan ToLookupfungsi LINQ, yang kemudian dapat Anda gunakan hampir secara bergantian dengan Kamus.

_people = personList
    .ToLookup(e => e.FirstandLastName, StringComparer.OrdinalIgnoreCase);
_people.ToDictionary(kl => kl.Key, kl => kl.First()); // Potentially unnecessary

Ini pada dasarnya akan melakukan GroupBy dalam jawaban LukeH , tetapi akan memberikan hashing bahwa Kamus menyediakan. Jadi, Anda mungkin tidak perlu mengubahnya ke Kamus, tetapi cukup gunakan Firstfungsi LINQ setiap kali Anda perlu mengakses nilai untuk kunci.

palswim
sumber
8

Anda dapat membuat metode ekstensi yang mirip dengan ToDictionary () dengan perbedaannya yaitu memungkinkan duplikat. Sesuatu seperti:

    public static Dictionary<TKey, TElement> SafeToDictionary<TSource, TKey, TElement>(
        this IEnumerable<TSource> source, 
        Func<TSource, TKey> keySelector, 
        Func<TSource, TElement> elementSelector, 
        IEqualityComparer<TKey> comparer = null)
    {
        var dictionary = new Dictionary<TKey, TElement>(comparer);

        if (source == null)
        {
            return dictionary;
        }

        foreach (TSource element in source)
        {
            dictionary[keySelector(element)] = elementSelector(element);
        }

        return dictionary; 
    }

Dalam hal ini, jika ada duplikat, maka nilai terakhir yang menang.

Eric
sumber
7

Untuk menangani menghilangkan duplikat, implementasikan metode IEqualityComparer<Person>yang dapat digunakan dalam Distinct()metode ini, dan kemudian mendapatkan kamus Anda akan mudah. Diberikan:

class PersonComparer : IEqualityComparer<Person>
{
    public bool Equals(Person x, Person y)
    {
        return x.FirstAndLastName.Equals(y.FirstAndLastName, StringComparison.OrdinalIgnoreCase);
    }

    public int GetHashCode(Person obj)
    {
        return obj.FirstAndLastName.ToUpper().GetHashCode();
    }
}

class Person
{
    public string FirstAndLastName { get; set; }
}

Dapatkan kamus Anda:

List<Person> people = new List<Person>()
{
    new Person() { FirstAndLastName = "Bob Sanders" },
    new Person() { FirstAndLastName = "Bob Sanders" },
    new Person() { FirstAndLastName = "Jane Thomas" }
};

Dictionary<string, Person> dictionary =
    people.Distinct(new PersonComparer()).ToDictionary(p => p.FirstAndLastName, p => p);
Anthony Pegram
sumber
2
        DataTable DT = new DataTable();
        DT.Columns.Add("first", typeof(string));
        DT.Columns.Add("second", typeof(string));

        DT.Rows.Add("ss", "test1");
        DT.Rows.Add("sss", "test2");
        DT.Rows.Add("sys", "test3");
        DT.Rows.Add("ss", "test4");
        DT.Rows.Add("ss", "test5");
        DT.Rows.Add("sts", "test6");

        var dr = DT.AsEnumerable().GroupBy(S => S.Field<string>("first")).Select(S => S.First()).
            Select(S => new KeyValuePair<string, string>(S.Field<string>("first"), S.Field<string>("second"))).
           ToDictionary(S => S.Key, T => T.Value);

        foreach (var item in dr)
        {
            Console.WriteLine(item.Key + "-" + item.Value);
        }
Raja
sumber
Saya menyarankan Anda untuk meningkatkan contoh Anda dengan membaca contoh Minimal, Lengkap, dan dapat diverifikasi .
IlGala
2

Jika kita ingin semua Orang (bukan hanya satu Orang) dalam kamus yang kembali, kita dapat:

var _people = personList
.GroupBy(p => p.FirstandLastName)
.ToDictionary(g => g.Key, g => g.Select(x=>x));
Shane Lu
sumber
1
Maaf, abaikan ulasan-edit saya (saya tidak dapat menemukan tempat untuk menghapus ulasan-edit saya). Saya hanya ingin menambahkan saran tentang penggunaan g.First () alih-alih g.Select (x => x).
Alex 75
1

Masalah dengan sebagian besar jawaban lain adalah bahwa mereka menggunakan Distinct, GroupByatau ToLookup, yang menciptakan Kamus tambahan di bawah tenda. Sama dengan ToUpper menciptakan string ekstra. Inilah yang saya lakukan, yang merupakan salinan persis kode Microsoft kecuali satu perubahan:

    public static Dictionary<TKey, TSource> ToDictionaryIgnoreDup<TSource, TKey>
        (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer = null) =>
        source.ToDictionaryIgnoreDup(keySelector, i => i, comparer);

    public static Dictionary<TKey, TElement> ToDictionaryIgnoreDup<TSource, TKey, TElement>
        (this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, TElement> elementSelector, IEqualityComparer<TKey> comparer = null)
    {
        if (keySelector == null)
            throw new ArgumentNullException(nameof(keySelector));
        if (elementSelector == null)
            throw new ArgumentNullException(nameof(elementSelector));
        var d = new Dictionary<TKey, TElement>(comparer ?? EqualityComparer<TKey>.Default);
        foreach (var element in source)
            d[keySelector(element)] = elementSelector(element);
        return d;
    }

Karena satu set pada indexer menyebabkannya menambahkan kunci, itu tidak akan membuang, dan juga hanya akan melakukan satu pencarian kunci. Anda juga dapat memberikannya IEqualityComparer, misalnyaStringComparer.OrdinalIgnoreCase

Charlie
sumber
0

Mulai dari solusi Carra, Anda juga dapat menuliskannya sebagai:

foreach(var person in personList.Where(el => !myDictionary.ContainsKey(el.FirstAndLastName)))
{
    myDictionary.Add(person.FirstAndLastName, person);
}
Cinquo
sumber
3
Bukan berarti siapa pun akan pernah mencoba untuk menggunakan ini, tetapi jangan mencoba untuk menggunakan ini. Memodifikasi koleksi saat Anda mengulanginya adalah ide yang buruk.
kidmosey