Convertissez la liste en dictionnaire en utilisant linq et ne vous inquiétez pas des doublons

163

J'ai une liste d'objets Person. Je veux convertir en un dictionnaire où la clé est le prénom et le nom (concaténés) et la valeur est l'objet Person.

Le problème est que j'ai des personnes en double, donc cela explose si j'utilise ce code:

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

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

Je sais que cela semble étrange mais je ne me soucie pas vraiment des noms en double pour le moment. S'il y a plusieurs noms, je veux juste en prendre un. Est-il possible que je puisse écrire ce code ci-dessus pour qu'il ne prenne qu'un des noms et ne fasse pas exploser les doublons?

Léora
la source
1
Les doublons (en fonction de la clé), je ne sais pas si vous souhaitez les conserver ou les perdre? Les conserver nécessiterait un Dictionary<string, List<Person>>(ou équivalent).
Anthony Pegram
@Anthony Pegram - je veux juste en garder un. J'ai mis à jour la question pour être plus explicite
leora
Eh bien, vous pouvez utiliser distinct avant de faire ToDictionary. mais vous devrez remplacer les méthodes Equals () et GetHashCode () pour la classe person afin que CLR sache comment comparer les objets personne
Sujit.Warrier
@ Sujit.Warrier - Vous pouvez également créer un comparateur d'égalité pour passer àDistinct
Kyle Delaney le

Réponses:

71

Voici la solution évidente, non linq:

foreach(var person in personList)
{
  if(!myDictionary.Keys.Contains(person.FirstAndLastName))
    myDictionary.Add(person.FirstAndLastName, person);
}
Carra
la source
208
thats so 2007 :)
leora
3
qui n'ignore pas la casse
un du
Ouais, il est temps que nous mettions à jour à partir du framework .net 2.0 au travail ... @onof Pas vraiment difficile d'ignorer la casse. Ajoutez simplement toutes les clés en majuscules.
Carra
comment pourrais-je rendre cette cas insensible
leora
11
Ou créez le dictionnaire avec un StringComparer qui ignorera la casse, si c'est ce dont vous avez besoin, alors votre code d'ajout / de vérification ne se soucie pas de savoir si vous ignorez la casse ou non.
Binary Worrier
423

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

Si vous préférez une solution non LINQ, vous pouvez faire quelque chose comme ceci:

// 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
la source
6
@LukeH Note mineure: vos deux extraits de code ne sont pas équivalents: la variante LINQ conserve le premier élément, l'extrait non LINQ conserve le dernier élément?
toong
4
@toong: C'est vrai et ça vaut vraiment la peine d'être noté. (Bien que dans ce cas, l'OP ne semble pas se soucier de l'élément avec lequel ils se retrouvent.)
LukeH
1
Pour le cas "la première valeur": la solution nonLinq effectue deux recherches dans le dictionnaire, mais Linq effectue des instanciations et des itérations d'objets redondants. Les deux ne sont pas idéaux.
SerG
@SerG Heureusement, la recherche dans le dictionnaire est généralement considérée comme une opération O (1) et a un impact négligeable.
MHollis
43

Une solution Linq utilisant Distinct () et et aucun regroupement est:

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

Je ne sais pas si c'est mieux que la solution de LukeH mais ça marche aussi.

Tillito
la source
Êtes-vous sûr que cela fonctionne? Comment Distinct va-t-il comparer le nouveau type de référence que vous créez? Je pense que vous auriez besoin de passer une sorte de IEqualityComparer dans Distinct pour obtenir ce travail comme prévu.
Simon Gillbee
5
Ignorez mon commentaire précédent. Voir stackoverflow.com/questions/543482/…
Simon Gillbee
Si vous voulez remplacer la façon dont la distinction est déterminée, consultez stackoverflow.com/questions/489258/…
James McMahon
30

Cela devrait fonctionner avec l'expression lambda:

personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
Ankit Dass
la source
2
Il doit être:personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
Gh61
4
Cela ne fonctionnera que si le IEqualityComparer par défaut pour la classe Person se compare par prénom et nom, en ignorant la casse. Sinon, écrivez un tel IEqualityComparer et utilisez la surcharge Distinct appropriée. Votre méthode ToDIctionary doit également prendre un comparateur insensible à la casse pour correspondre aux exigences de l'OP.
Joe
13

Vous pouvez également utiliser la ToLookupfonction LINQ, que vous pouvez ensuite utiliser presque de manière interchangeable avec un dictionnaire.

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

Cela fera essentiellement le GroupBy dans la réponse de LukeH , mais donnera le hachage fourni par un dictionnaire. Ainsi, vous n'avez probablement pas besoin de le convertir en dictionnaire, mais utilisez simplement la Firstfonction LINQ chaque fois que vous avez besoin d'accéder à la valeur de la clé.

palswim
la source
8

Vous pouvez créer une méthode d'extension similaire à ToDictionary (), la différence étant qu'elle autorise les doublons. Quelque chose comme:

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

Dans ce cas, s'il y a des doublons, la dernière valeur l'emporte.

Eric
la source
7

Pour gérer l'élimination des doublons, implémentez un IEqualityComparer<Person>qui peut être utilisé dans la Distinct()méthode, puis obtenir votre dictionnaire sera facile. Donné:

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

Obtenez votre dictionnaire:

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
la source
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);
        }
Roi
la source
Je vous suggère d'améliorer votre exemple en lisant l'exemple Minimal, Complet et vérifiable .
IlGala
2

Dans le cas où nous voulons que toute la personne (au lieu d'une seule personne) dans le dictionnaire de retour, nous pourrions:

var _people = personList
.GroupBy(p => p.FirstandLastName)
.ToDictionary(g => g.Key, g => g.Select(x=>x));
Shane Lu
la source
1
Désolé, ignorez ma révision-modification (je ne trouve pas où supprimer ma révision-modification). Je voulais juste ajouter une suggestion sur l'utilisation de g.First () au lieu de g.Select (x => x).
Alex 75 le
1

Le problème avec la plupart des autres réponses est qu'elles utilisent Distinct, GroupByou ToLookup, ce qui crée un dictionnaire supplémentaire sous le capot. De même, ToUpper crée une chaîne supplémentaire. C'est ce que j'ai fait, qui est une copie presque exacte du code de Microsoft, à l'exception d'un changement:

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

Étant donné qu'un ensemble sur l'indexeur lui fait ajouter la clé, il ne lancera pas et effectuera également une seule recherche de clé. Vous pouvez également lui donner un IEqualityComparer, par exempleStringComparer.OrdinalIgnoreCase

Charlie
la source
0

À partir de la solution de Carra, vous pouvez également l'écrire comme:

foreach(var person in personList.Where(el => !myDictionary.ContainsKey(el.FirstAndLastName)))
{
    myDictionary.Add(person.FirstAndLastName, person);
}
Cinquo
la source
3
Non pas que quiconque essaie jamais d'utiliser ceci, mais n'essayez pas de l'utiliser. Modifier les collections au fur et à mesure que vous les itérez est une mauvaise idée.
kidmosey