Converter lista em dicionário usando linq e não se preocupar com duplicatas

163

Eu tenho uma lista de objetos Pessoa. Quero converter para um dicionário em que a chave seja o nome e o sobrenome (concatenados) e o valor seja o objeto Pessoa.

O problema é que eu tenho algumas pessoas duplicadas, então isso explode se eu usar este código:

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

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

Sei que parece estranho, mas não me preocupo com nomes duplicados por enquanto. Se houver vários nomes, eu só quero pegar um. Existe alguma maneira que eu possa escrever esse código acima, então ele pega um dos nomes e não explode em duplicatas?

leora
fonte
1
As duplicatas (com base na chave), não tenho certeza se você deseja mantê-las ou perdê-las? Mantê-los exigiria um Dictionary<string, List<Person>>(ou equivalente).
Anthony Pegram
@ Anthony Pegram - só quero manter um deles. eu atualizei a questão para ser mais explícito
Leora
bem, você pode usar distintas antes de fazer o ToDictionary. mas você teria que substituir Equals () e GetHashCode () métodos para a classe pessoa de modo que CLR sabe como comparar pessoa objetos
Sujit.Warrier
@ Sujit.Warrier - Você também pode criar um comparador de igualdade para passar paraDistinct
Kyle Delaney

Respostas:

71

Aqui está a solução óbvia e não linq:

foreach(var person in personList)
{
  if(!myDictionary.Keys.Contains(person.FirstAndLastName))
    myDictionary.Add(person.FirstAndLastName, person);
}
Carra
fonte
207
isso é tão 2007 :)
Leora
3
que não ignora o caso
23/07/10
Sim, na hora em que atualizamos a partir da estrutura .net 2.0 no trabalho ... @onof Não é exatamente difícil ignorar o caso. Basta adicionar todas as chaves em maiúsculas.
Carra
Como eu poderia fazer este caso insensível
Leora
11
Ou crie o dicionário com um StringComparer que ignorará o caso, se é isso que você precisa, então o código de adição / verificação não se importa se você está ignorando o caso ou não.
Worrier binário
423

Solução 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);

Se você preferir uma solução não-LINQ, poderá fazer algo assim:

// 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
fonte
2
+1 muito elegante (vou votar o mais rápido possível - não tenho mais votos para hoje :))
23/07/10
6
@LukeH Nota secundária: seus dois trechos não são equivalentes: a variante LINQ retém o primeiro elemento, o trecho não-LINQ retém o último elemento?
toong
4
@ toong: Isso é verdade e definitivamente vale a pena notar. (Embora, neste caso, o OP não parece se importar com qual elemento eles acabam com.)
LukeH
1
Para o caso "o primeiro valor": a solução nonLinq consulta o dicionário duas vezes, mas o Linq faz instâncias de objetos redundantes e iteração. Ambos não são ideais.
SerG
Felizmente, a pesquisa no dicionário é geralmente considerada uma operação O (1) e tem um impacto insignificante.
MHollis
42

Uma solução Linq usando Distinct () e sem agrupamento é:

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

Não sei se é melhor que a solução de LukeH, mas também funciona.

Tillito
fonte
Você tem certeza que funciona? Como o Distinct vai comparar o novo tipo de referência que você cria? Eu acho que você precisaria passar algum tipo de IEqualityComparer para o Distinct para obter esse trabalho como pretendido.
Simon Gillbee
5
Ignore meu comentário anterior. Veja stackoverflow.com/questions/543482/…
Simon Gillbee
Se você deseja substituir como distinta é determinado check-out stackoverflow.com/questions/489258/...
James McMahon
30

Isso deve funcionar com a expressão lambda:

personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
Ankit Dass
fonte
2
Deve ser:personList.Distinct().ToDictionary(i => i.FirstandLastName, i => i);
Gh61 8/08/14
4
Isso funcionará apenas se o IEqualityComparer padrão da classe Person for comparado pelo nome e pelo sobrenome, ignorando maiúsculas e minúsculas. Caso contrário, escreva um IEqualityComparer e use a sobrecarga Distinct relevante. Além disso, seu método ToDIctionary deve usar um comparador que não diferencia maiúsculas de minúsculas para atender aos requisitos do OP.
23416 Joe
13

Você também pode usar a ToLookupfunção LINQ, que você pode usar quase de forma intercambiável com um dicionário.

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

Isso fará essencialmente o GroupBy na resposta de LukeH , mas fornecerá o hash que um Dicionário fornece. Portanto, você provavelmente não precisará convertê-lo em um dicionário, mas use a Firstfunção LINQ sempre que precisar acessar o valor da chave.

palswim
fonte
8

Você pode criar um método de extensão semelhante ao ToDictionary (), com a diferença de que ele permite duplicatas. Algo como:

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

Nesse caso, se houver duplicatas, o último valor vence.

Eric
fonte
7

Para lidar com a eliminação de duplicatas, implemente uma IEqualityComparer<Person>que possa ser usada no Distinct()método e obter o seu dicionário será fácil. Dado:

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

Obtenha seu dicionário:

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
fonte
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);
        }
Rei
fonte
Sugiro que você melhore seu exemplo lendo o exemplo Mínimo, Completo e verificável .
IlGala
2

Caso desejemos toda a Pessoa (em vez de apenas uma Pessoa) no dicionário retornado, poderíamos:

var _people = personList
.GroupBy(p => p.FirstandLastName)
.ToDictionary(g => g.Key, g => g.Select(x=>x));
Shane Lu
fonte
1
Desculpe, ignore minha edição de revisão (não consigo encontrar onde excluir minha edição de revisão). Eu só queria adicionar uma sugestão sobre o uso de g.First () em vez de g.Select (x => x).
Alex 75
1

O problema com a maioria das outras respostas é que elas usam Distinct, GroupByou ToLookup, o que cria um dicionário extra sob o capô. Igualmente, o ToUpper cria uma sequência extra. Foi o que fiz, que é uma cópia quase exata do código da Microsoft, exceto por uma alteração:

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

Como um conjunto no indexador faz com que ele adicione a chave, ele não será acionado e também fará apenas uma pesquisa de chave. Você também pode fornecer IEqualityComparer, por exemploStringComparer.OrdinalIgnoreCase

Charlie
fonte
0

A partir da solução de Carra, você também pode escrevê-lo como:

foreach(var person in personList.Where(el => !myDictionary.ContainsKey(el.FirstAndLastName)))
{
    myDictionary.Add(person.FirstAndLastName, person);
}
Cinquo
fonte
3
Não que alguém tente usar isso, mas não tente usar isso. Modificar coleções enquanto você as está iterando é uma má idéia.
Kidmosey