Konvertieren Sie die Liste mit linq in ein Wörterbuch und sorgen Sie sich nicht um Duplikate

163

Ich habe eine Liste von Personenobjekten. Ich möchte in ein Wörterbuch konvertieren, in dem der Schlüssel der Vor- und Nachname (verkettet) und der Wert das Objekt Person ist.

Das Problem ist, dass ich einige doppelte Personen habe. Wenn ich diesen Code verwende, geht dies in die Luft:

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

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

Ich weiß, dass es komisch klingt, aber ich kümmere mich momentan nicht wirklich um doppelte Namen. Wenn es mehrere Namen gibt, möchte ich nur einen greifen. Kann ich diesen Code trotzdem oben schreiben, damit er nur einen der Namen annimmt und nicht auf Duplikate explodiert?

Leora
quelle
1
Bei den Duplikaten (basierend auf dem Schlüssel) bin ich mir nicht sicher, ob Sie sie behalten oder verlieren wollen? Um sie zu behalten, wäre ein Dictionary<string, List<Person>>(oder ein gleichwertiges) erforderlich .
Anthony Pegram
@ Anthony Pegram - will nur einen von ihnen behalten. Ich habe die Frage aktualisiert, um expliziter zu sein
Leora
Nun, Sie können verschiedene verwenden, bevor Sie ToDictionary ausführen. Sie müssten jedoch die Methoden Equals () und GetHashCode () für die Personenklasse überschreiben, damit CLR weiß, wie Personenobjekte verglichen werden
Sujit.Warrier
@ Sujit.Warrier - Sie können auch einen Gleichstellungsvergleich erstellen, an den Sie übergeben könnenDistinct
Kyle Delaney

Antworten:

70

Hier ist die offensichtliche, nicht linq Lösung:

foreach(var person in personList)
{
  if(!myDictionary.Keys.Contains(person.FirstAndLastName))
    myDictionary.Add(person.FirstAndLastName, person);
}
Carra
quelle
206
das ist so 2007 :)
Leora
3
das ignoriert den Fall nicht
Uhr
Ja, ungefähr zu der Zeit aktualisieren wir das .net 2.0-Framework bei der Arbeit ... @onof Nicht gerade schwer zu ignorieren. Fügen Sie einfach alle Tasten in Großbuchstaben hinzu.
Carra
Wie würde ich diesen Fall unempfindlich machen
Leora
11
Oder erstellen Sie das Wörterbuch mit einem StringComparer, der den Fall ignoriert. Wenn Sie dies benötigen, ist es Ihrem Code zum Hinzufügen / Überprüfen egal, ob Sie den Fall ignorieren oder nicht.
Binary Worrier
422

LINQ-Lösung:

// 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);

Wenn Sie eine Nicht-LINQ-Lösung bevorzugen, können Sie Folgendes tun:

// 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
quelle
2
+1 sehr elegant (ich werde so schnell wie möglich abstimmen - habe heute keine Stimmen mehr :))
Uhr
6
@LukeH Kleiner Hinweis: Ihre beiden Snippets sind nicht gleichwertig: Die LINQ-Variante behält das erste Element bei, das Nicht-LINQ-Snippet behält das letzte Element?
Toong
4
@toong: Das stimmt und ist definitiv erwähnenswert. (Obwohl es dem OP in diesem Fall egal zu sein scheint, mit welchem ​​Element sie enden.)
LukeH
1
Für den Fall "Der erste Wert": Die nonLinq-Lösung sucht zweimal im Wörterbuch, aber Linq führt redundante Objektinstanziierungen und Iterationen durch. Beides ist nicht ideal.
SerG
@SerG Zum Glück wird die Suche nach Wörterbüchern im Allgemeinen als O (1) -Operation betrachtet und hat vernachlässigbare Auswirkungen.
MHollis
42

Eine Linq-Lösung mit Distinct () und ohne Gruppierung ist:

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

Ich weiß nicht, ob es schöner ist als die Lösung von LukeH, aber es funktioniert auch.

Tillito
quelle
Bist du sicher, dass das funktioniert? Wie wird Distinct den von Ihnen erstellten neuen Referenztyp vergleichen? Ich würde denken, Sie müssten eine Art IEqualityComparer an Distinct übergeben, um diese Arbeit wie beabsichtigt zu erhalten.
Simon Gillbee
5
Ignoriere meinen vorherigen Kommentar. Siehe stackoverflow.com/questions/543482/…
Simon Gillbee
Wenn Sie überschreiben möchten, wie unterschiedlich bestimmt wird, überprüfen Sie stackoverflow.com/questions/489258/…
James McMahon
30

Dies sollte mit dem Lambda-Ausdruck funktionieren:

personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
Ankit Dass
quelle
2
Es muss sein:personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
Gh61
4
Dies funktioniert nur, wenn der Standard-IEqualityComparer für die Person-Klasse nach Vor- und Nachnamen verglichen wird, wobei Groß- und Kleinschreibung ignoriert wird. Andernfalls schreiben Sie einen solchen IEqualityComparer und verwenden Sie die entsprechende Distinct-Überladung. Außerdem sollte Ihre ToDIctionary-Methode einen Vergleicher verwenden, bei dem die Groß- und Kleinschreibung nicht berücksichtigt wird, um den Anforderungen des OP zu entsprechen.
Joe
13

Sie können auch die ToLookupLINQ-Funktion verwenden, die Sie dann fast austauschbar mit einem Wörterbuch verwenden können.

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

Dies wird im Wesentlichen das GroupBy in der Antwort von LukeH ausführen , aber das Hashing geben, das ein Wörterbuch bereitstellt. Sie müssen es also wahrscheinlich nicht in ein Wörterbuch konvertieren, sondern verwenden einfach die LINQ- FirstFunktion, wenn Sie auf den Wert für den Schlüssel zugreifen müssen.

palswim
quelle
8

Sie können eine Erweiterungsmethode ähnlich wie ToDictionary () erstellen, mit dem Unterschied, dass Duplikate zulässig sind. Etwas wie:

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

In diesem Fall gewinnt der letzte Wert, wenn Duplikate vorhanden sind.

Eric
quelle
7

Um Duplikate zu entfernen, implementieren Sie eine IEqualityComparer<Person>, die in der Distinct()Methode verwendet werden kann, und dann ist es einfach, Ihr Wörterbuch zu erhalten. Gegeben:

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

Holen Sie sich Ihr Wörterbuch:

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
quelle
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);
        }
König
quelle
Ich empfehle Ihnen, Ihr Beispiel zu verbessern, indem Sie das minimale, vollständige und überprüfbare Beispiel lesen .
IlGala
2

Falls wir alle Personen (anstelle von nur einer Person) im zurückkehrenden Wörterbuch haben möchten, könnten wir:

var _people = personList
.GroupBy(p => p.FirstandLastName)
.ToDictionary(g => g.Key, g => g.Select(x=>x));
Shane Lu
quelle
1
Entschuldigung, ignoriere meine Review-Bearbeitung (ich kann nicht finden, wo ich meine Review-Bearbeitung löschen kann). Ich wollte nur einen Vorschlag zur Verwendung von g.First () anstelle von g.Select (x => x) hinzufügen.
Alex 75
1

Das Problem mit den meisten anderen Antworten ist , dass sie verwenden Distinct, GroupByoder ToLookup, was ein zusätzliches Wörterbuch unter der Haube schafft. Ebenso erstellt ToUpper eine zusätzliche Zeichenfolge. Dies ist, was ich getan habe, was bis auf eine Änderung eine fast exakte Kopie des Microsoft-Codes ist:

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

Da ein Satz auf dem Indexer bewirkt, dass er den Schlüssel hinzufügt, wird er nicht geworfen und führt auch nur eine Schlüsselsuche durch. Sie können es IEqualityComparerzum Beispiel auch gebenStringComparer.OrdinalIgnoreCase

Charlie
quelle
0

Ausgehend von Carras Lösung können Sie sie auch wie folgt schreiben:

foreach(var person in personList.Where(el => !myDictionary.ContainsKey(el.FirstAndLastName)))
{
    myDictionary.Add(person.FirstAndLastName, person);
}
Cinquo
quelle
3
Nicht, dass irgendjemand jemals versuchen würde, dies zu verwenden, aber versuchen Sie nicht, dies zu verwenden. Das Ändern von Sammlungen während der Iteration ist eine schlechte Idee.
Kidmosey