Элегантный способ создания вложенного словаря в С#

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

Скажем, у меня есть список элементов такого класса:

public class Thing
{
    int Foo;
    int Bar;
    string Baz;
}

И я хочу классифицировать строку Baz на основе значений Foo, затем Bar. Для каждой возможной комбинации значений Foo и Bar будет не более одной Thing, но я не гарантированно получаю ценность для каждого из них. Это может помочь концептуализировать его как информацию ячейки для таблицы: Foo - номер строки, Bar - номер столбца, а Baz - это значение, которое можно найти там, но не обязательно будет значение, присутствующее для каждой ячейки.

IEnumerable<Thing> things = GetThings();
List<int> foos = GetAllFoos();
List<int> bars = GetAllBars();
Dictionary<int, Dictionary<int, string>> dict = // what do I put here?
foreach(int foo in foos)
{
    // I may have code here to do something for each foo...
    foreach(int bar in bars)
    {
        // I may have code here to do something for each bar...
        if (dict.ContainsKey(foo) && dict[foo].ContainsKey(bar))
        {
            // I want to have O(1) lookups
            string baz = dict[foo][bar];
            // I may have code here to do something with the baz.
        }
    }
}

Какой простой и элегантный способ создания вложенного словаря? Я слишком долго использовал С#, чтобы привыкнуть к поиску простых однострочных решений для всех подобных вещей, но это меня озадачило.

Ответы

Ответ 1

Здесь решение с использованием Linq:

Dictionary<int, Dictionary<int, string>> dict = things
    .GroupBy(thing => thing.Foo)
    .ToDictionary(fooGroup => fooGroup.Key,
                  fooGroup => fooGroup.ToDictionary(thing => thing.Bar,
                                                    thing => thing.Baz));

Ответ 2

Элегантным способом было бы не создавать словари самостоятельно, а использовать LINQ GroupBy и ToDictionary, чтобы сгенерировать его для вас.

var things = new[] {
    new Thing { Foo = 1, Bar = 2, Baz = "ONETWO!" },
    new Thing { Foo = 1, Bar = 3, Baz = "ONETHREE!" },
    new Thing { Foo = 1, Bar = 2, Baz = "ONETWO!" }
}.ToList();

var bazGroups = things
    .GroupBy(t => t.Foo)
    .ToDictionary(gFoo => gFoo.Key, gFoo => gFoo
        .GroupBy(t => t.Bar)
        .ToDictionary(gBar => gBar.Key, gBar => gBar.First().Baz));

Debug.Fail("Inspect the bazGroups variable.");

Я предполагаю, что, классифицируя Baz с помощью Foo и Bar, вы имеете в виду, что если две вещи имеют как Foo, так и Bar, то их значение Baz также будет таким же. Пожалуйста, поправьте меня, если я ошибаюсь.

В основном вы являетесь группой по свойству Foo...
то для каждой результирующей группы вы группируетесь по свойству Bar...
то для каждой результирующей группы вы берете первое значение Baz как значение словаря.

Если вы заметили, имена методов совпадают именно с тем, что вы пытаетесь сделать.: -)


РЕДАКТИРОВАТЬ: Другим способом, использующим понимание запросов, они длиннее, но тише легче читать и пересматривать:

var bazGroups =
    (from t1 in things
     group t1 by t1.Foo into gFoo
     select new
     {
         Key = gFoo.Key,
         Value = (from t2 in gFoo
                  group t2 by t2.Bar into gBar
                  select gBar)
                  .ToDictionary(g => g.Key, g => g.First().Baz)
     })
     .ToDictionary(g => g.Key, g => g.Value);

К сожалению, для ToDictionary не существует сопоставления понимания запроса, поэтому он не так изящен, как лямбда-выражения.

...

Надеюсь, что это поможет.

Ответ 3

Определите свой собственный общий NestedDictionary класс

public class NestedDictionary<K1, K2, V>: 
     Dictionary<K1, Dictionary<K2, V>> {}

то в вашем коде вы пишете

NestedDictionary<int, int, string> dict = 
       new NestedDictionary<int, int, string> ();

если вы используете int, int, string one many, также определите для него собственный класс.

   public class NestedIntStringDictionary: 
        NestedDictionary<int, int, string> {}

а затем напишите:

  NestedIntStringDictionary dict = 
          new NestedIntStringDictionary();

EDIT: добавить возможность создания определенного экземпляра из предоставленного списка элементов:

   public class NestedIntStringDictionary: 
        NestedDictionary<int, int, string> 
   {
        public NestedIntStringDictionary(IEnumerable<> items)
        {
            foreach(Thing t in items)
            {
                Dictionary<int, string> innrDict = 
                       ContainsKey(t.Foo)? this[t.Foo]: 
                           new Dictionary<int, string> (); 
                if (innrDict.ContainsKey(t.Bar))
                   throw new ArgumentException(
                        string.Format(
                          "key value: {0} is already in dictionary", t.Bar));
                else innrDict.Add(t.Bar, t.Baz);
            }
        }
   }

а затем напишите:

  NestedIntStringDictionary dict = 
       new NestedIntStringDictionary(GetThings());

Ответ 4

Другой подход заключается в том, чтобы закрепить свой словарь с помощью анонимного типа, основываясь на значениях Foo и Bar.

var things = new List<Thing>
                 {
                     new Thing {Foo = 3, Bar = 4, Baz = "quick"},
                     new Thing {Foo = 3, Bar = 8, Baz = "brown"},
                     new Thing {Foo = 6, Bar = 4, Baz = "fox"},
                     new Thing {Foo = 6, Bar = 8, Baz = "jumps"}
                 };
var dict = things.ToDictionary(thing => new {thing.Foo, thing.Bar},
                               thing => thing.Baz);
var baz = dict[new {Foo = 3, Bar = 4}];

Это эффективно выравнивает вашу иерархию в один словарь. Обратите внимание: этот словарь не может быть открыт извне, поскольку он основан на анонимном типе.

Если комбинация значений Foo и Bar не уникальна в вашей исходной коллекции, вам сначала нужно сгруппировать их.

var dict = things
    .GroupBy(thing => new {thing.Foo, thing.Bar})
    .ToDictionary(group => group.Key,
                  group => group.Select(thing => thing.Baz));
var bazes = dict[new {Foo = 3, Bar = 4}];
foreach (var baz in bazes)
{
    //...
}

Ответ 5

Возможно, вы сможете использовать KeyedCollection, где вы определяете:

class ThingCollection
    : KeyedCollection<Dictionary<int,int>,Employee>
{
    ...
}

Ответ 6

Используйте BeanMap два ключевых класса карты. Существует также карта с тремя ключами, и она достаточно расширяема, если вам нужны n ключей.

http://beanmap.codeplex.com/

Ваше решение будет выглядеть следующим образом:

class Thing
{
  public int Foo { get; set; }
  public int Bar { get; set; }
  public string Baz { get; set; }
}

[TestMethod]
public void ListToMapTest()
{
  var things = new List<Thing>
             {
                 new Thing {Foo = 3, Bar = 3, Baz = "quick"},
                 new Thing {Foo = 3, Bar = 4, Baz = "brown"},
                 new Thing {Foo = 6, Bar = 3, Baz = "fox"},
                 new Thing {Foo = 6, Bar = 4, Baz = "jumps"}
             };

  var thingMap = Map<int, int, string>.From(things, t => t.Foo, t => t.Bar, t => t.Baz);

  Assert.IsTrue(thingMap.ContainsKey(3, 4));
  Assert.AreEqual("brown", thingMap[3, 4]);

  thingMap.DefaultValue = string.Empty;
  Assert.AreEqual("brown", thingMap[3, 4]);
  Assert.AreEqual(string.Empty, thingMap[3, 6]);

  thingMap.DefaultGeneration = (k1, k2) => (k1.ToString() + k2.ToString());

  Assert.IsFalse(thingMap.ContainsKey(3, 6));
  Assert.AreEqual("36", thingMap[3, 6]);
  Assert.IsTrue(thingMap.ContainsKey(3, 6));
}

Ответ 7

Я думаю, что самым простым подходом было бы использовать методы расширения LINQ. Очевидно, я не тестировал этот код для выполнения.

var items = new[] {
  new Thing { Foo = 1, Bar = 3, Baz = "a" },
  new Thing { Foo = 1, Bar = 3, Baz = "b" },
  new Thing { Foo = 1, Bar = 4, Baz = "c" },
  new Thing { Foo = 2, Bar = 4, Baz = "d" },
  new Thing { Foo = 2, Bar = 5, Baz = "e" },
  new Thing { Foo = 2, Bar = 5, Baz = "f" }
};

var q = items
  .ToLookup(i => i.Foo) // first key
  .ToDictionary(
    i => i.Key, 
    i => i.ToLookup(
      j => j.Bar,       // second key
      j => j.Baz));     // value

foreach (var foo in q) {
  Console.WriteLine("{0}: ", foo.Key);
  foreach (var bar in foo.Value) {
    Console.WriteLine("  {0}: ", bar.Key);
    foreach (var baz in bar) {
      Console.WriteLine("    {0}", baz.ToUpper());
    }
  }
}

Console.ReadLine();

Вывод:

1:
  3:
    A
    B
  4:
    C
2:
  4:
    D
  5:
    E
    F

Ответ 8

Dictionary<int, Dictionary<string, int>> nestedDictionary = 
            new Dictionary<int, Dictionary<string, int>>();