【问题标题】:Elegant way to create a nested Dictionary in C#在 C# 中创建嵌套字典的优雅方式
【发布时间】:2009-12-17 00:25:51
【问题描述】:

我意识到我没有提供足够的信息让大多数人读懂我的想法并理解我的所有需求,所以我在原来的基础上做了一些改动。

假设我有一个这样的类的项目列表:

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

我想根据 Foo 的值对 Baz 字符串进行分类,然后是 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.
        }
    }
}

生成嵌套字典的简单、优雅的方法是什么?我使用 C# 的时间已经够长了,以至于我已经习惯于为所有此类常见问题寻找简单的单行解决方案,但这个解决方案让我很困惑。

【问题讨论】:

  • 起点是什么? Thing 对象列表?
  • 感谢您到目前为止的回答。我已经更新了问题以使其更清楚。看起来你们中的一些人已经有了正确的想法。测试完您的答案后,我将开始投票并分配获胜者。

标签: c# linq


【解决方案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));

【讨论】:

  • 使用 "var dict =" 你可以使用 LINQ 来折叠你的多个 foreach 语句: var bazs = dict.SelectMany(topPair => topPair.Value.Values); foreach(string baz in bazs) { // ... }
  • 这似乎是我正在寻找的简短而优雅的解决方案。 GroupBy/ToDictionary 组合是我自己想出来的。谢谢。
【解决方案2】:

一种优雅的方法是自己创建字典,而是使用 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.");

我假设通过使用FooBarBaz 进行分类,您的意思是如果两个事物同时具有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);

不幸的是,ToDictionary 没有对应的查询理解,因此它不如 lambda 表达式优雅。

...

希望这会有所帮助。

【讨论】:

  • +1 以确保您的答案的完整性。我很想将此标记为 the 答案,但 Mark 的答案通过消除对第二个 GroupByFirst 的需要而获得了更多的优雅分数。 (马特,我也很同情。LINQ 几乎是自 OO 以来编程中最酷的东西,恕我直言)
  • 我倾向于在可读性和明确意图方面犯错。我认为只使用 LINQ 就足够简洁了,不需要做所有聪明的把戏。虽然,我同意字典技巧一很好:)
  • 一个问题当我有多个things要添加时如何添加到现有字典中
  • @User sn-ps 主要用于对现有数据进行分类。但是,生成的字典不会阻止您添加它。只需像通常添加到任何字典一样执行dict["key1"]["key2"].Add("key3", "value")。不知道你是不是这个意思?
  • 接受的答案抛出异常(字典中已存在键)。当然,我的用例与问题不同(第二个字典的值必须是 DataRows 的列表)。第二组为我解决了。
【解决方案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,也可以为它定义一个自定义类..

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

【讨论】:

  • 访问器会是什么样子?
  • 这如何帮助我从我得到的数据中优雅地构建一个嵌套字典?
  • 抱歉,我回答了原始问题,但并没有说明这一点...编辑了我的答案,明确地向您展示如何做到这一点...
【解决方案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 的两个关键 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));
}

【讨论】:

    【解决方案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>>();
      

      【讨论】:

      • 你不太明白我的问题的要点。我正在寻找一个 linq 语句或允许我从现有列表中填充字典的东西,而不是简单地实例化它。
      猜你喜欢
      • 2019-10-23
      • 2017-11-27
      • 1970-01-01
      • 2023-04-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多