c#不同深度的嵌套字典
本文关键字:嵌套 字典 深度 | 更新日期: 2023-09-27 18:16:50
本质上我需要的是不同int变量到字典的映射。至少我是这么想的。我能想到的最简单的方法是使用switch语句来解释我想要什么。
string s = "";
int a = 1;
int b = 2;
int c = 0;
switch (a){
case 0:
s = "a0";
break;
case 1:
switch (b){
case 0:
s = "b0";
break
case 1:
switch (c){
case 0:
s = "c0";
break;
case 1:
s = "c1";
break;
}
break
case 2:
s = "b2";
break;
}
break;
case 2:
s = "a2";
break;
}
这是为了简洁起见的简化版本,否则您可能会有许多巢,并且在多个情况下等等。我认为一个很好的解决方案是一个字典来快速选择正确的值,但这并不能很好地嵌套,因为嵌套字典的大多数内部巢不需要有值。
我首先想到字典的原因是,类似于下面的声明语法会很好(这类似于字典的字典)。
thing = {
{0, "a0"},
{1, {
{0, "b0"},
{1, {
{0, "c0"},
{1, "c1"}
}
},
{2, "b2"}
}
},
{2, "a2"}
}
// Next line is sort of hopeful but potentially unrealistic syntax
s = thing[a][b][c]; // or = thing(a,b,c);
编辑:这不是必需的声明语法,但它简短易懂,这就是我正在寻找的。
编辑:或LINQ,我已经看到了很多LINQ建议类似的问题,但我不是特别熟悉它。
假设您正在查找键的部分匹配,那么您将无法使用单个字典完成此操作。原因:
假设你有某种"规则"类。我们叫它"钥匙"。您可以这样实例化它:
Key.Create(0) // this "rule" would match any query key starting with 0 (e.g., {0}, {0, 1}, or {0, 1, 9, 2, 23, 243})
现在假设您想使用某种"事实"或"查询键"类查询它。由于使用在Add操作期间用作键的值类型查询字典,因此必须重用相同的类型:
Key.Create(0, 2, 13) // this fact should be matched by rules {0}, {0,2} or {0, 2, 13}
现在你要尝试获取值:
var value = map[Key.Create(0, 2, 13)]
Key类可以重写Equals以允许部分键匹配。但是,字典将首先使用哈希码,并使用Key的哈希码。Create(0,2,13)永远不会匹配Key.Create(0)的hascode。使用任何类型的基/派生类型也无法解决这个问题。
最好的选择可能是滚动你自己的类。像这样的代码应该做:
class ResultMap
{
public void Add(int[] key, string value)
{
Debug.Assert(key != null);
Debug.Assert(key.Length > 0);
var currentMap = _root;
foreach (var i in key.Take(key.Length - 1))
{
object levelValue;
if (currentMap.TryGetValue(i, out levelValue))
{
currentMap = levelValue as Dictionary<int, object>;
if (currentMap == null)
throw new Exception("A rule is already defined for this key.");
}
else
{
var newMap = new Dictionary<int, object>();
currentMap.Add(i, newMap);
currentMap = newMap;
}
}
var leaf = key[key.Length - 1];
if (currentMap.ContainsKey(leaf))
throw new Exception("A rule is already defined for this key.");
currentMap.Add(leaf, value);
}
public string TryGetValue(params int[] key)
{
Debug.Assert(key != null);
Debug.Assert(key.Length > 0);
var currentMap = _root;
foreach (var i in key)
{
object levelValue;
if (!currentMap.TryGetValue(i, out levelValue))
return null;
currentMap = levelValue as Dictionary<int, object>;
if (currentMap == null)
return (string) levelValue;
}
return null;
}
private readonly Dictionary<int, object> _root = new Dictionary<int, object>();
}
这是一个单元测试:
public void Test()
{
var resultMap = new ResultMap();
resultMap.Add(new[] {0}, "a0");
resultMap.Add(new[] {1, 0}, "b0");
resultMap.Add(new[] {1, 1, 0}, "c0");
resultMap.Add(new[] {1, 1, 1}, "c1");
resultMap.Add(new[] {1, 2}, "b2");
resultMap.Add(new[] {2}, "a2");
Debug.Assert("a0" == resultMap.TryGetValue(0));
Debug.Assert("a0" == resultMap.TryGetValue(0, 0));
Debug.Assert("a0" == resultMap.TryGetValue(0, 1));
Debug.Assert("a0" == resultMap.TryGetValue(0, 2));
Debug.Assert("a0" == resultMap.TryGetValue(0, 0, 0));
Debug.Assert("a0" == resultMap.TryGetValue(0, 0, 1));
Debug.Assert("a0" == resultMap.TryGetValue(0, 0, 2));
Debug.Assert("a0" == resultMap.TryGetValue(0, 1, 0));
Debug.Assert("a0" == resultMap.TryGetValue(0, 1, 1));
Debug.Assert("a0" == resultMap.TryGetValue(0, 1, 2));
Debug.Assert("a0" == resultMap.TryGetValue(0, 2, 0));
Debug.Assert("a0" == resultMap.TryGetValue(0, 2, 1));
Debug.Assert("a0" == resultMap.TryGetValue(0, 2, 2));
Debug.Assert(null == resultMap.TryGetValue(1));
Debug.Assert("b0" == resultMap.TryGetValue(1, 0));
Debug.Assert(null == resultMap.TryGetValue(1, 1));
Debug.Assert("b2" == resultMap.TryGetValue(1, 2));
Debug.Assert("b0" == resultMap.TryGetValue(1, 0, 0));
Debug.Assert("b0" == resultMap.TryGetValue(1, 0, 1));
Debug.Assert("b0" == resultMap.TryGetValue(1, 0, 2));
Debug.Assert("c0" == resultMap.TryGetValue(1, 1, 0));
Debug.Assert("c1" == resultMap.TryGetValue(1, 1, 1));
Debug.Assert(null == resultMap.TryGetValue(1, 1, 2));
Debug.Assert("b2" == resultMap.TryGetValue(1, 2, 0));
Debug.Assert("b2" == resultMap.TryGetValue(1, 2, 1));
Debug.Assert("b2" == resultMap.TryGetValue(1, 2, 2));
Debug.Assert("a2" == resultMap.TryGetValue(2));
Debug.Assert("a2" == resultMap.TryGetValue(2, 0));
Debug.Assert("a2" == resultMap.TryGetValue(2, 1));
Debug.Assert("a2" == resultMap.TryGetValue(2, 2));
Debug.Assert("a2" == resultMap.TryGetValue(2, 0, 0));
Debug.Assert("a2" == resultMap.TryGetValue(2, 0, 1));
Debug.Assert("a2" == resultMap.TryGetValue(2, 0, 2));
Debug.Assert("a2" == resultMap.TryGetValue(2, 1, 0));
Debug.Assert("a2" == resultMap.TryGetValue(2, 1, 1));
Debug.Assert("a2" == resultMap.TryGetValue(2, 1, 2));
Debug.Assert("a2" == resultMap.TryGetValue(2, 2, 0));
Debug.Assert("a2" == resultMap.TryGetValue(2, 2, 1));
Debug.Assert("a2" == resultMap.TryGetValue(2, 2, 2));
}
所以这个问题并不像乍一看那么简单。我看到的最重要的是复合模式,所以我们将从一个可以公开我们需要的功能的接口开始:
public interface INode<TParam, TResult>
{
TResult GetValue(TParam[] parameters, int depth);
}
我在int
参数和string
返回值中进行了通用而不是硬编码,以使其从通用MultiKeyLookup
的角度更具可重用性。
然后我们有一个简单的情况,Leaf
节点只返回一个特定的值,而不管参数是什么:
class Leaf<TParam, TResult> : INode<TParam, TResult>
{
private TResult value;
public Leaf(TResult value)
{
this.value = value;
}
public TResult GetValue(TParam[] parameters, int depth)
{
return value;
}
}
然后我们有不那么平凡的情况。正确的Node
类。它接受一些值,然后将每个值映射到INode
对象。这就是奇迹发生的地方。它映射到的INode
可以是只有特定值的叶节点,也可以是另一个节点。然后,当被要求获取一个值时,它只是将输入参数映射到适当的深度,并以递归的方式获取该值的INode
的值:
class Node<TParam, TResult> : INode<TParam, TResult>
{
//private Tuple<TParam, INode<TParam, TResult>>[] values;
private Dictionary<TParam, INode<TParam, TResult>> lookup;
public Node(params Tuple<TParam, INode<TParam, TResult>>[] values)
{
lookup = values.ToDictionary(pair => pair.Item1,
pair => pair.Item2);
}
public TResult GetValue(TParam[] parameters, int depth)
{
return lookup[parameters[depth]].GetValue(parameters, depth + 1);
}
}
所以在这一点上我们可以完成。下面是一个(稍微简化的)映射示例:
var node = new Node<int, string>(
Tuple.Create(0, (INode<int, string>)new Leaf<int, string>("a0")),
Tuple.Create(1, (INode<int, string>)new Node<int, string>(
Tuple.Create(0, (INode<int, string>)new Leaf<int, string>("b0")))));
Console.WriteLine(node.GetValue(new int[] { 0 }, 0)); //prints a0
现在这有点乱。特别是,它有大量的泛型参数规范,我们知道这些规范总是相同的,并且需要将每种INode
类型强制转换为接口类型,以便Tuple
类型正确。
为了方便起见,我创建了一个"builder"类MultiKeyLookup
。它将有一些用于创建叶子和节点的辅助方法,这样就可以为该类指定一次泛型参数。此外,由于Leaf
和Node
都不需要这些构建器,我已经将这两个类都作为MultiKeyLookup
的私有内部类,除了包含这两个类之外,它还具有:
public class MultiKeyLookup<TParam, TResult>
{
public INode<TParam, TResult> CreateLeaf(TResult result)
{
return new Leaf<TParam, TResult>(result);
}
public INode<TParam, TResult> CreateNode(
params Tuple<TParam, INode<TParam, TResult>>[] values)
{
return new Node<TParam, TResult>(values);
}
public INode<TParam, TResult> Root { get; set; }
public TResult GetValue(TParam[] parameters)
{
return Root.GetValue(parameters, 0);
}
//definition of Leaf goes here
//definition of Node goes here
}
使用这个类,我们现在可以写:
var map = new MultiKeyLookup<int, string>();
map.Root = map.CreateNode(
Tuple.Create(0, map.CreateLeaf("a0")),
Tuple.Create(1, map.CreateNode(
Tuple.Create(0, map.CreateLeaf("b0")),
Tuple.Create(1, map.CreateNode(
Tuple.Create(0, map.CreateLeaf("c0")),
Tuple.Create(1, map.CreateLeaf("c1")))),
Tuple.Create(2, map.CreateLeaf("b2")))),
Tuple.Create(2, map.CreateLeaf("a2")));
Console.WriteLine(map.GetValue(new int[] { 0 })); //prints a0
Console.WriteLine(map.GetValue(new int[] { 0, 0, 4 })); //prints a0
Console.WriteLine(map.GetValue(new int[] { 1, 0 })); // prints b0
Console.WriteLine(map.GetValue(new int[] { 1, 1, 0 })); //prints c0
注意,这是您在OP中定义的完整创建,而不是简化的示例。
可以这样使用类:
public class A
{
public string result;
public A(int case)
{
if(case == 0)
{
this.result = "a0";
}
else if(case == 2)
{
this.result = "a2";
}
else
{
return new B(case).result;
}
}
}
public class B
{
public string result;
public B(int case)
{
if(case == 0)
{
this.result = "b0";
}
else if(case == 2)
{
this.result = "b2"
}
else
{
return new C(case).result;
}
}
}
public class C
{
public string result;
public C(int case)
{
if(case == 0)
{
this.result = "C0";
}
else
{
this.result = "c1";
}
}
}
我知道你已经选好了答案,但我想到了一个新想法,我觉得它很酷。使用int键和对象值的嵌套字典如下:
Dictionary<int, object> baseDictionary = new Dictionary<int, object>();
baseDictionary.Add(0, new object[] { "a1" });
baseDictionary.Add(1, new Dictionary<int, object>());
baseDictionary.Add(2, new object[] { "a2" });
Dictionary<int, object> childDictionary = baseDictionary[1] as Dictionary<int, object>;
childDictionary.Add(0, new object[] { "b1" });
childDictionary.Add(1, new Dictionary<int, object>());
childDictionary.Add(2, new object[] { "b2" });
Dictionary<int, object> childTwoDictionary = childDictionary[1] as Dictionary<int, object>;
childTwoDictionary.Add(0, new object[] { "c1" });
childTwoDictionary.Add(1, new object[] { "c2" });
然后访问您想要的记录,您可以使用递归方法与键数组,如下所示:
private object GetResult(int keyIndex, int[] keys, Dictionary<int, object> inputDictionary)
{
Dictionary<int, object> nextDictionary = inputDictionary[keys[keyIndex]] as Dictionary<int, object>;
object result;
if (nextDictionary != null && keyIndex < keys.Length)
{
keyIndex++;
return GetResult(keyIndex, keys, nextDictionary);
}
else if(!string.IsNullOrEmpty(inputDictionary[keys[keyIndex]].ToString()))
{
result = inputDictionary[keys[keyIndex]] as object;
keyIndex++;
return result;
}
return new object[] { "Failed" };
}
并按如下方式调用:
private void simpleButton1_Click(object sender, EventArgs e)
{
int keyIndex = 0;
int[] keys = { 1, 1, 1 };
object result = this.GetResult(keyIndex, keys, this.baseDictionary);
labelControl1.Text = (((object[])(result))[0]).ToString();
}
如果您可以提前知道您的"密钥结构",那么使用Dictionary<string, string>
并生成一个由3部分组成的密钥可能会更便宜:"ABC"…避免了巢并提供了直接查找。
例如,如果您知道a = 1, b = 2和c = 3,则可以将它们连接到字符串"123",这就是字典查找的关键字。这就是HttpContext缓存和。net 4.0 MemoryCache的工作原理。
编辑:如果你不总是有所有3个值,使用string。使用提供值之间分隔符/分隔符的键结构进行格式化。这通常是最佳实践,否则很容易发生键冲突:
private const string _keyFormat = "{0}_{1}_{2}";
private string GenerateKey(object a, object b, object c)
{
return string.Format(_keyFormat, a, b, c);
}