Как объединить ключи и значения словаря в один список с помощью LINQ?

у меня есть словарь, где ключ-это строка, а значение-это список строк, соответствующих этому ключу. Я хотел бы отобразить все ключи в словаре со значениями, связанными с этим ключом, вложенным под этим ключом. Что-то вроде этого:--6-->

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

в C# 2.0 я бы сделал это так (values - это Dictionary):

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

как бы я сделал эквивалент с помощью LINQ? Кажется, это должно быть возможно, однако я не могу понять как это сделать.

если я использую values.SelectMany(p => p.Values), тогда в конечном результате будут только значения, а не ключи.

любое другое решение, о котором я думал, имеет аналогичное ограничение.

5 ответов


решение в C#

вот решение, использующее метод расширения aggregate:

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

нет синтаксиса LINQ для предложения aggregate в C# (но, по-видимому, есть в Visual Basic для некоторых предопределенных функций).

это решение может выглядеть несколько сложным, но метод aggregate довольно полезен.

как работает Aggregate

он работает следующим образом: если у вас есть List<int> вы можете объединить все значения в один общей стоимостью.

в отличие от Select метод, который не изменяет длину списка. Или Where метод, который сжимает список, но он по-прежнему остается списком (вместо одного значения).

например, если у вас есть список {1, 2, 3, 4} вы можете объединить их в одно значение, например:

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

таким образом, вы даете два значения Aggregate способ; семя значение и функция "объединитель":

  • вы начинаете агрегировать с одним значением seed (0 в данном случае).
  • ваш combiner функция вызывается для каждого значения в списке.
    Первый аргумент-это вычисленный результат до сих пор (ноль в первый раз, позже это будет иметь другие значения), и он сочетает его со значением x.

вот вкратце, как Aggregate метод работает на List<int>. Он работает так же на KeyValuePair<string, List<string>>, но только с разными типами.


Я не уверен, как вы это сделаете в LINQ, но с помощью lambdas вы можете сделать что-то вроде:

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

это не очень читабельно, хотя.


в LINQ нет ничего, что поможет вам здесь, потому что у вас есть отдельный набор требований к значениям, чем вы делаете ключи (вы вкладываете между значениями).

даже если это не так, в лучшем случае LINQ поможет вам просто получить один источник перечисления для цикла, который вам нужно будет иметь какую-то логику разбиения в любом случае, чтобы указать, когда вы обрабатываете набор значений по сравнению с ключом.

Что это сводится к тому, что словарь, который у вас уже есть, дает вам естественную группировку, с которой LINQ больше не сможет помочь, если вы не имеете дело с операцией, отличной от группировки (сортировка, проекция, фильтрация).


Если бы у вас был способ такой:

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

тогда вы могли бы сделать это:

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

или в общем виде:

public static IEnumerable<T> GetFlattened<T, U>
  ( this KeyValuePair<T, List<U>> kvp,
    Func<U, T> ValueTransform
  )
{
  List<T> result = new List<T>();
  result.Add(kvp.Key);
  result.AddRange(kvp.Value.Select(v => ValueTransform(v)));
  return result;
}

List<string> result = theDictionary
  .SelectMany(kvp => kvp.GetFlattened(v => "\t" + v))
  .ToList();

Я бы сделал выбор значений, чтобы поместить вещи в начальный список по элементам (используя строку.Присоединяйтесь к значениям), а затем вставьте это в строку (снова используя string.Присоединяться.)

IEnumerable<string> 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());