在C#中创建嵌套字典的优雅方式
我意识到我没有给大多数人提供足够的信息来阅读我的想法并了解我的所有需求,所以我对此有所改变。
假设我有一个像这样的类的项目列表:
public class Thing { int Foo; int Bar; string Baz; }
我想根据Foo的值对Baz字符串进行分类,然后是Bar。 对于Foo和Bar值的每种可能组合,最多只有一个Thing,但我不保证每个值都有一个值。 将它概念化为表格的单元格信息可能有所帮助:Foo是行号,Bar是列号,Baz是要在那里找到的值,但不一定存在每个单元格的值。
IEnumerable things = GetThings(); List foos = GetAllFoos(); List bars = GetAllBars(); Dictionary<int, Dictionary> 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#,以至于我已经习惯于为所有常见的东西找到简单的单行解决方案,但这个让我很难过。
这是使用Linq的解决方案:
Dictionary> dict = things .GroupBy(thing => thing.Foo) .ToDictionary(fooGroup => fooGroup.Key, fooGroup => fooGroup.ToDictionary(thing => thing.Bar, thing => thing.Baz));
一种优雅的方式是不自己创建字典,而是使用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.");
我假设通过使用Foo
和Bar
对Baz
进行分类,你的意思是如果两个东西同时具有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没有查询理解对应,因此它不如lambda表达式那么优雅。
…
希望这可以帮助。
定义您自己的自定义通用NestedDictionary
类
public class NestedDictionary: Dictionary> {}
然后在你编写的代码中
NestedDictionary dict = new NestedDictionary ();
如果你经常使用int,int,string,那么也为它定义一个自定义类。
public class NestedIntStringDictionary: NestedDictionary {}
然后写:
NestedIntStringDictionary dict = new NestedIntStringDictionary();
编辑:添加从提供的项目列表构造特定实例的function:
public class NestedIntStringDictionary: NestedDictionary { public NestedIntStringDictionary(IEnumerable<> items) { foreach(Thing t in items) { Dictionary innrDict = ContainsKey(t.Foo)? this[t.Foo]: new Dictionary (); 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());
另一种方法是使用基于Foo和Bar值的匿名类型来键入字典。
var things = new List { 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) { //... }
您可以使用KeyedCollection定义:
class ThingCollection : KeyedCollection,Employee> { ... }
使用BeanMap的两个关键Map类。 还有一个3键映射,如果你需要n个键,它是非常可扩展的。
您的解决方案将如下所示:
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 { 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.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)); }
我认为最简单的方法是使用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
Dictionary> nestedDictionary = new Dictionary>();