【问题标题】:c# Nested dictionary of different depthsc#嵌套不同深度的字典
【发布时间】:2013-06-04 19:01:22
【问题描述】:

基本上我需要的是不同 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 建议,但我不是特别熟悉。

【问题讨论】:

    标签: c# dictionary switch-statement nested control-structure


    【解决方案1】:

    鉴于您正在寻找键的部分匹配,您将无法使用单个字典完成此操作。原因如下:

    假设你有某种“规则”类。我们称之为“钥匙”。你可以这样实例化它:

    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) 的哈希码。您也无法通过使用任何类型的基/派生类型来解决此问题。

    最好的选择可能是开设自己的课程。应该这样做:

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

    【讨论】:

    • 我想看看我是否不能把一些真实的数据放进去,并确保它按我想的那样工作,但我认为它是。谢谢!
    • 这基本上是我的想法,但我认为将它抽象为一棵树更有意义。要查找值,请从根开始。任何时候你在一个非叶子节点你消费另一个int 并选择相应的分支。在叶节点,您只需输出它具有的任何值。插入新的“规则”以相同的方式遍历树,但只要缺少一个分支,您就附加一个非叶节点(并且终端状态应该是一个非叶节点,您正在向其添加叶节点)。现在代码的结构方式是相同的,但并没有强调它只是一棵树。不过,+1
    【解决方案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 中定义的完整创建,而不是简化的示例。

    【讨论】:

    • 非常好的解决方案,感谢您的解释!据我所知(我可能是错的)和 Michael Gunter 的解决方案都做同样的事情,但你的声明有点冗长。
    【解决方案3】:

    也许使用类似这样的类:

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

    【讨论】:

    • 条目多于使之可行
    • 对不起,我有最好的错误答案:)
    【解决方案4】:

    我知道你已经选择了一个答案,但我想出了一个新想法,我觉得这很酷。使用 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();
    }
    

    【讨论】:

      【解决方案5】:

      如果您可以提前知道您的“密钥结构”,使用Dictionary&lt;string, string&gt; 并生成一个由 3 个部分串联而成的密钥可能会更便宜:“ABC”...避免嵌套并提供直接查找。

      例如,如果您知道 a = 1、b = 2 和 c = 3,则可以将它们连接成一个字符串“123”,这就是字典查找的关键。这就是 HttpContext Caching 和 .NET 4.0 MemoryCache 的工作原理。

      编辑:

      如果您并非始终拥有全部 3 个值,请使用带有键结构的 string.Format,该键结构在值之间提供分隔符/分隔符。无论如何,这通常是最佳实践,否则您很容易发生键冲突:

      private const string _keyFormat = "{0}_{1}_{2}";
      
      private string GenerateKey(object a, object b, object c)
      {
          return string.Format(_keyFormat, a, b, c);
      }
      

      【讨论】:

      • 考虑在键中使用分隔符,这样当您使用字母表中的两位数引用时,您会避免一些混乱,例如:2||10||3 a = 2; b = 10; c = 3.
      • @DavidH 问题是他没有比较相等的值。他想要拥有0 的密钥并传入0_0_0 并使其匹配,或者传入0_1_3 并使其匹配。它不会,你设置它的方式。使用查找的想法很棒,我也一直在使用它们;但鉴于此问题的限制,目前它并不适用。
      • @DavidH 所以这意味着如果第二个位置有 n 个可能的值,第三个位置有 m 个可能的值,他需要存储 nm 个键,所有这些都以 0 作为第一个值,都指向相同的结果。那里使用了 *lot 过多的存储空间,并且在创建字典时还需要做一些额外的工作。最重要的是,如果对于解决方案无法起作用的三个位置中的每一个位置都没有已知的、有限数量的值。
      • 也许使用自定义的IEqualityComparer&lt;T&gt; 让字典将 0_0_0 和 0_1_3 匹配到同一个字典键
      • @Christian 来试试吧。这是不可能(有效地)做的,因为任何匹配的值的哈希码都需要相同,所以你最终会得到基本上所有具有相同哈希的东西,这意味着你所有的字典都变成了链表,你的高效搜索已经都下地狱了。
      猜你喜欢
      • 2011-03-15
      • 1970-01-01
      • 1970-01-01
      • 2021-12-29
      • 2020-06-07
      • 1970-01-01
      • 1970-01-01
      • 2022-10-16
      相关资源
      最近更新 更多