简体   繁体   English

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

[英]Elegant way to create a nested Dictionary in C#

I realized that I didn't give enough information for most people to read my mind and understand all my needs, so I changed this somewhat from the original.我意识到我没有提供足够的信息让大多数人读懂我的想法并理解我的所有需求,所以我对原来的做了一些改动。

Say I've got a list of items of a class like this:假设我有一个这样的类的项目列表:

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

And I want to categorize the Baz string based on the values of Foo, then Bar.我想根据 Foo 和 Bar 的值对 Baz 字符串进行分类。 There will be at most one Thing for each possible combination of Foo and Bar values, but I'm not guaranteed to have a value for each one.对于 Foo 和 Bar 值的每种可能组合,最多只有一个 Thing,但我不能保证每个组合都有一个值。 It may help to conceptualize it as cell information for a table: Foo is the row number, Bar is the column number, and Baz is the value to be found there, but there won't necessarily be a value present for every cell.将其概念化为表格的单元格信息可能会有所帮助: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.
        }
    }
}

What's an easy, elegant way to generate the nested dictionary?生成嵌套字典的简单而优雅的方法是什么? I've been using C# long enough that I'm getting used to finding simple, one-line solutions for all of the common stuff like this, but this one has me stumped.我使用 C# 的时间已经足够长了,以至于我已经习惯于为所有像这样的常见问题寻找简单的单行解决方案,但是这个问题让我感到困惑。

Here's a solution using Linq:这是使用 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));

An elegant way would be to not create the dictionaries yourself but use LINQ GroupBy and ToDictionary to generate it for you.一种优雅的方法是自己创建字典,而是使用 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.");

I assume that by categorizing Baz using Foo and Bar you mean that if two things have both Foo and Bar equals then their Baz value also be the same as well.我假设通过使用FooBarBaz进行分类,您的意思是如果两件事同时具有FooBar相等,那么它们的Baz值也相同。 Please correct me if I'm wrong.如果我错了,请纠正我。

You're basically group by the Foo property first...您基本上首先按Foo属性分组...
then for each resulting group, you group on the Bar property...然后对于每个结果组,您对Bar属性进行分组...
then for each resulting group you take the first Baz value as the dictionary value.然后对于每个结果组,您将第一个Baz值作为字典值。

If you noticed, the method names matched exactly what you are trying to do.如果您注意到,方法名称与您要执行的操作完全匹配。 :-) :-)


EDIT: Here's another way using query comprehensions, they are longer but are quiet easier to read and grok:编辑:这是使用查询推导式的另一种方式,它们更长但更安静更易于阅读和理解:

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

Unfortunately, there are no query comprehension counterpart for ToDictionary so it's not as elegant as the lambda expressions.不幸的是,ToDictionary 没有对应的查询理解,所以它不像 lambda 表达式那么优雅。

... ...

Hope this helps.希望这会有所帮助。

Define your own custom generic NestedDictionary class定义您自己的自定义通用NestedDictionary

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

then in your code you write然后在你写的代码中

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

if you use the int, int, string one a lot, define a custom class for that too..如果您经常使用 int、int、string one,也请为此定义一个自定义类。

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

and then write:然后写:

  NestedIntStringDictionary dict = 
          new NestedIntStringDictionary();

EDIT: To add capability to construct specific instance from provided List of items:编辑:添加从提供的项目列表构建特定实例的功能:

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

and then write:然后写:

  NestedIntStringDictionary dict = 
       new NestedIntStringDictionary(GetThings());

Another approach would be to key your dictionary using an anonymous type based on both the Foo and Bar values.另一种方法是使用基于 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}];

This effectively flattens your hierarchy into a single dictionary.这有效地将您的层次结构扁平化为一个字典。 Note that this dictionary cannot be exposed externally since it is based on an anonymous type.请注意,此字典不能对外公开,​​因为它基于匿名类型。

If the Foo and Bar value combination isn't unique in your original collection, then you would need to group them first.如果 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)
{
    //...
}

You may be able to use a KeyedCollection where you define:您可以使用KeyedCollection 来定义:

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

Use BeanMap's two key Map class.使用 BeanMap 的两个关键 Map 类。 There is also a 3 key map, and it is quite extensible in case you need n keys.还有一个 3 键映射,如果您需要 n 键,它是非常可扩展的。

http://beanmap.codeplex.com/ http://beanmap.codeplex.com/

Your solution would then look like:您的解决方案将如下所示:

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

I think the simplest approach would be to use the LINQ extension methods.我认为最简单的方法是使用 LINQ 扩展方法。 Obviously I haven't tested this code for performace.显然我还没有测试过这段代码的性能。

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

Output:输出:

1:
  3:
    A
    B
  4:
    C
2:
  4:
    D
  5:
    E
    F
Dictionary<int, Dictionary<string, int>> nestedDictionary = 
            new Dictionary<int, Dictionary<string, int>>();

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM