【问题标题】:Should I check whether particular key is present in Dictionary before accessing it?我应该在访问之前检查字典中是否存在特定的键吗?
【发布时间】:2010-11-12 13:52:58
【问题描述】:

我是否应该检查字典中是否存在特定键如果我确定在我到达访问它的代码时它会被添加到字典中?

有两种方法可以访问字典中的值

  1. 检查 ContainsKey 方法。如果它返回 true,那么我使用字典对象的索引器 [key] 访问。

  1. TryGetValue 将返回 true 或 false 以及通过 out 参数返回值。

(如果我想获得价值,第二名的表现会比第一名更好。Benchmark。)

但是,如果我确定访问全局字典的函数肯定有密钥,那么我应该仍然使用 TryGetValue 检查还是不检查我应该使用 indexer[]。

或者我不应该假设并且总是检查?

【问题讨论】:

    标签: c# .net dictionary


    【解决方案1】:

    答案当然是“这一切都取决于情况”。您需要平衡字典中缺少密钥的风险(对于访问数据有限的小型系统,您可以依赖完成的顺序,对于较大的系统,对于较大的系统,对于访问相同的多个程序员来说较大)数据,尤其是具有读/写/删除访问权限的情况,其中涉及线程且无法保证顺序,或者数据源自外部且读取可能失败)以及风险的影响(安全关键系统、商业版本或企业将使用的系统)依赖于与娱乐、一次性工作和/或仅供您使用的东西相比)以及对速度、大小和懒惰的任何要求。

    如果我要建立一个控制铁路信号的系统,我希望能够避免所有可能和不可能的错误,并且避免错误处理等错误(墨菲第二定律:“什么不能出错会出错”。)如果我为了好玩而把东西放在一起,即使大小和速度不是问题,我也会对这样的东西更加放松——我会想要找到有趣的东西。

    当然,有时这本身就是有趣的东西。

    【讨论】:

      【解决方案2】:

      我个人会检查密钥是否在那里,无论你是否确定它,有些人可能会说这个检查是多余的,字典会抛出一个你可以捕获的异常,但恕我直言,你不应该依赖该异常,您应该检查自己,然后抛出您自己的异常,这意味着某事或带有成功标志和原因的结果对象......失败机制实际上取决于实现。

      【讨论】:

        【解决方案3】:

        我认为 Marc 和 Jon (像往常一样)已经很成熟了。由于您还在问题中提到了性能,因此可能值得考虑如何锁定字典。

        简单的lock 序列化所有读取访问,如果读取频繁且写入相对较少,这可能是不可取的。在这种情况下,使用ReaderWriterLockSlim 可能会更好。缺点是代码稍微复杂一些,写入速度稍慢。

        【讨论】:

          【解决方案4】:

          总是检查。永远不要把话说绝了。我假设您的应用程序对性能没有那么重要,您将不得不节省检查时间。

          提示:如果您决定不检查,至少使用 Debug.Assert( dict.ContainsKey( key ) );这只会在调试模式下编译,您的发布版本不会包含它。这样你至少可以在调试时进行检查。

          仍然:如果可能,请检查一下 :-)

          编辑:这里有一些误解。我所说的“总是检查”不仅是指在某处使用 if。正确处理异常也包含在其中。所以,更准确地说:永远不要把任何事情视为理所当然,期待意外。通过 ContainsKey 检查或处理潜在的异常,但如果不包含该元素,请执行一些操作。

          【讨论】:

          • 我不同意这一点。如果密钥本来就存在而不存在,那么正确的行为是抛出异常,是吗?这正是索引器所做的——那么为什么不使用这种行为呢?此外,我不热衷于改变调试和发布版本之间的行为......这对我来说听起来像是一个问题的秘诀。
          • 是的,调试检查并不是最佳实践。但总比不检查要好。如果数据应该在字典中,则抛出异常肯定是正确的方法,但它可能不是字典的内部异常,而是更适合实际问题的异常(例如 InvalidOperationException)。
          • 我同意乔恩的观点,你只检查它何时可能存在或不存在,如果你确定它应该在那里,那么正确的路径是允许抛出异常并相应地处理它.
          • 最初的问题没有说明处理异常。我明白这只是访问索引,然后希望最好。通过“始终检查”,我还包括捕捉异常和适当地采取行动。我将编辑帖子并完善我的陈述:为意外做好准备。
          • 您绝对不应该捕捉异常 - 使用 TryGetValue 而不是使用索引器访问并捕捉异常。但是,当键应该存在时,与显式测试和抛出不同的异常相比,使用索引器可以显着提高可读性。
          【解决方案5】:

          如果您知道字典通常包含密钥,则无需在访问之前检查它。

          如果出现问题并且字典不包含它应该包含的项目,您可以让字典抛出异常。首先检查密钥的唯一原因是如果您想自己处理此问题情况而不会出现异常。然而,让字典抛出异常并捕获它是处理这种情况的一种完全有效的方法。

          【讨论】:

            【解决方案6】:

            从性能的角度来看,有两种思路。

            1) 尽可能避免异常,因为异常代价高昂 - 即在尝试从字典中检索特定键之前检查它是否存在。如果有可能它可能不存在,我认为更好的方法。这将防止相当常见的异常。

            2) 如果您确信该项目 99% 的时间都存在,那么在访问它之前不要检查它是否存在。有 1% 的时间它不存在,会抛出异常,但您通过不检查为其他 99% 的时间节省了时间。

            我的意思是,如果有一个明确的,就为大多数人优化。如果某个项目的存在存在任何真正程度的不确定性,请在检索之前进行检查。

            【讨论】:

            • 我认为这根本不应该与性能有关。它应该与正确性有关:如果 预期 键在正确的情况下存在,即键不存在是一个错误,那么抛出异常是失败的正确方法。否则,您应该使用异常来检测这种情况 - 处理带有异常的非异常情况是个坏主意,IMO。
            • 我想更多的是从字典中不存在的情况下不应该导致失败,例如应该使用默认值,或者如果它不存在则应该添加它。它应该存在于字典中的情况,但如果它不存在则不是真正的错误 - 即假设字典是数据库中某些数据的缓存,例如数据库不可用并且缓存无法填充,但您不希望这导致系统故障。很难找到一个真实世界的例子,所以希望我有某种意义!
            【解决方案7】:

            TryGetValue 与按键索引的代码相同,只是前者返回一个默认值(对于out 参数),后者抛出异常。使用TryGetValue,您将获得一致的检查,绝对不会造成性能损失。

            编辑:正如乔恩所说,如果你知道它总是有密钥,那么你可以索引它并让它抛出适当的异常。但是,如果您可以通过自己抛出详细的消息来提供更好的上下文信息,那就更好了。

            【讨论】:

              【解决方案8】:

              如果键应该存在,则使用索引器 - 如果它不存在,它将引发适当的异常,如果键的缺失表示错误,这是正确的行为。

              如果密钥不存在是有效的,请改用TryGetValue 并做出相应的反应。

              (同时应用 Marc 关于安全访问共享字典的建议。)

              【讨论】:

              • 缺少密钥通常表示另一个问题。如果您知道问题所在并且可以提供更有用的消息,那么您应该考虑抛出自己的异常,因为 KeyNotFoundException 附带的消息有时很难缩小范围。
              • 如果 KeyNotFoundException 也提供了密钥,那肯定会很有用——但除此之外,通常只有堆栈跟踪对我来说就足够了。你会走多远:检查每一个可能的空引用(不仅仅是参数,还有你内部期望非空的东西)是否为空,并在每一个上抛出一个自定义异常?在内部不一致的情况下,框架的某些部分总会抛出异常:如果你试图预测其中的每一个,代码最终更多的是关于错误情况而不是取得进展。
              【解决方案9】:

              如果字典是全局的(静态/共享),您应该同步对它的访问(这很重要;否则您可能会损坏它)。

              即使您的线程只是读取数据,它也需要尊重可能正在编辑它的其他线程的锁。

              但是;如果您确定该项目在那里,则索引器应该没问题:

              Foo foo;
              lock(syncLock) {
                  foo = data[key];
              }
              // use foo...
              

              否则,一个有用的模式是检查和添加同一个锁:

              Foo foo;
              lock(syncLock) {
                  if(!data.TryGetValue(key, out foo)) {
                      foo = new Foo(key);
                      data.Add(key, foo);
                  }
              }
              // use foo...
              

              这里我们只添加不存在的项目...但在同一个锁内

              【讨论】:

              • 很好,但我认为这不是问题所在?我没有看到任何对并发的引用? :)
              • "全球字典" - 让我按照给定的路径。如果我的理解不正确,OP 可以忽略此回复;-p
              • 为什么我们在从字典中读取数据时需要加锁?是否由于字典扩展/在某些限制后重新散列?
              • @Jadoon 因为它不是线程安全的,即使对于阅读也是如此。相比之下,Hashtable 对任意数量的读取器和最多一个写入器都是安全的,因此更容易同时使用
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2014-02-16
              • 2022-06-14
              • 1970-01-01
              • 1970-01-01
              • 2015-12-27
              • 2015-10-30
              相关资源
              最近更新 更多