在C#中创建嵌套字典的优雅方式

时间:2009-12-17 00:25:52

标签: c# linq

我意识到我没有给大多数人提供足够的信息来阅读我的想法并理解我的所有需求,所以我从原来的那些改变了。

说我有一个像这样的课程列表:

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

我想根据Foo的值对Baz字符串进行分类,然后是Bar。对于Foo和Bar值的每种可能组合,最多只有一个Thing,但我不保证每个值都有一个值。将它概念化为表的单元格信息可能有所帮助:Foo是行号,Bar是列号,Baz是要在那里找到的值,但不一定存在每个单元格的值。 / p>

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

生成嵌套字典的简单,优雅的方法是什么?我一直在使用C#,以至于我已经习惯于为所有常见的东西找到简单的单行解决方案,但这个让我感到难过。

8 个答案:

答案 0 :(得分:29)

以下是使用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));

答案 1 :(得分:19)

一种优雅的方式是自己创建字典,但使用LINQ GroupByToDictionary为您生成字典。

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.");

我认为通过使用BazFooBar进行分类,您的意思是,如果两件事同时FooBar等于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); 值作为字典值。

如果您注意到,方法名称与您尝试的完全匹配。 : - )


编辑:这是使用查询理解的另一种方式,它们更长但更安静更容易阅读和理解:

{{1}}

不幸的是,ToDictionary没有查询理解对应物,所以它不像lambda表达式那样优雅。

...

希望这有帮助。

答案 2 :(得分:4)

定义您自己的自定义通用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,那么也为它定义一个自定义类。

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

然后写:

  NestedIntStringDictionary dict = 
          new NestedIntStringDictionary();

编辑:添加从提供的项目列表构建特定实例的功能:

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

答案 3 :(得分:3)

另一种方法是使用基于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)
{
    //...
}

答案 4 :(得分:2)

您可以使用定义的KeyedCollection

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

答案 5 :(得分:2)

使用BeanMap的两个关键Map类。还有一个3键映射,如果你需要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));
}

答案 6 :(得分:1)

我认为最简单的方法是使用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

答案 7 :(得分:-2)

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