Como faço para combinar as chaves e valores de um dictionary em uma lista usando o LINQ?

Eu tenho um dictionary, onde a chave é uma string e o valor é uma lista de strings que correspondem a essa chave. Gostaria de exibir todas as chaves no dictionary, com os valores associados a essa tecla sob a tecla abaixo. Algo assim:

Key 1 Value 1 Value 2 Value 3 Key 2 Value 1 Value 2 

Em c # 2.0, eu faria isso assim ( values é o Dictionary ):

 StringBuilder sb = new StringBuilder(); foreach(KeyValuePair<string, List> pair in values) { sb.AppendLine(pair.Key); foreach(string item in pair.Value) { sb.AppendLine('\t' + item); } } 

Como eu faria o equivalente usando o LINQ? Parece que deveria ser possível, mas não consigo descobrir como fazer isso.

Se eu usar values.SelectMany(p => p.Values) , somente os valores estarão no resultado final, e não nas chaves também.

Qualquer outra solução que eu tenha pensado tem uma limitação semelhante.

Uma solução em c #

Aqui está uma solução usando o método de extensão agregada :

 string result = values.Aggregate("", (keyString, pair) => keyString + "\n" + pair.Key + ":" + pair.Value.Aggregate("", (str, val) => str + "\n\t" + val) ); 

Não há syntax LINQ para a cláusula agregada em C # (mas aparentemente há no Visual Basic para algumas funções predefinidas).

Essa solução pode parecer um pouco complexa, mas o método agregado é bastante útil.

Como funciona o agregado

Funciona assim: Se você tiver uma List poderá combinar todos os valores em um único valor agregado .

Isso está em contraste com o método Select , que não modifica o tamanho da lista. Ou o método Where , que reduz a lista, mas ainda permanece como uma lista (em vez de um valor único).

Por exemplo, se você tiver uma lista {1, 2, 3, 4} poderá combiná-las em um único valor como este:

 int[] xs = {1, 2, 3, 4}; int sum = xs.Aggregate(0, (sumSoFar, x) => sumSoFar + x); 

Então você dá dois valores para o método Aggregate ; um valor de semente e uma function ‘combinador’:

  • Você começa a agregar com um único valor inicial ( 0 neste caso).
  • Sua function combinada é chamada para cada valor na lista.
    O primeiro argumento é o resultado computado até agora (zero na primeira vez, mais tarde isso terá outros valores) e combina-o com o valor x .

Em suma, como o método Aggregate funciona na List . Ele funciona da mesma forma em KeyValuePair> , mas apenas com tipos diferentes.

Não sei como você faria isso no LINQ, mas usando lambdas você poderia fazer algo como:

 string foo = string.Join(Environment.NewLine, values.Select(k => k.Key + Environment.NewLine + string.Join(Environment.NewLine, k.Value.Select(v => "\t" + v).ToArray())).ToArray()); 

Isso não é muito legível, no entanto.

Não há nada no LINQ que o ajude aqui, porque você tem um conjunto separado de requisitos para os valores do que você faz com as chaves (você guia entre os valores).

Mesmo que esse não fosse o caso, na melhor das hipóteses, o LINQ ajudará você a obter apenas uma fonte de enumeração para percorrer, o que teria de ter algum tipo de lógica de particionamento para indicar quando estiver processando um conjunto de valores versus uma chave.

O que acontece é que o Dicionário já dá a você um agrupamento natural no qual o LINQ não poderá mais ajudar, a menos que você esteja lidando com uma operação diferente de agrupamento (sorting, projeção, filtragem).

Se você tivesse um método como este:

 public IEnumerable GetStrings (KeyValuePair> kvp) { List result = new List(); result.Add(kvp.Key); result.AddRange(kvp.Value.Select(s => "\t" + s)); return result; } 

Então você poderia fazer isso:

 List result = theDictionary .SelectMany(kvp => GetStrings(kvp)).ToList(); 

Ou genericamente:

 public static IEnumerable GetFlattened ( this KeyValuePair> kvp, Func ValueTransform ) { List result = new List(); result.Add(kvp.Key); result.AddRange(kvp.Value.Select(v => ValueTransform(v))); return result; } List result = theDictionary .SelectMany(kvp => kvp.GetFlattened(v => "\t" + v)) .ToList(); 

Eu faria uma seleção sobre os valores para colocar as coisas em uma lista inicial por item (usando string.Junte para os valores) e, em seguida, pop em uma string (novamente usando string.Join).

 IEnumerable items = values.Select(v => string.Format("{0}{1}{2}", v.Key, Environment.NewLine + "\t", string.Join(Environment.NewLine + "\t", v.Value.ToArray())); string itemList = string.Join(Environment.NewLine, items.ToArray());