有没有比TryGetValue使用C#词典更好的方法?


19

我发现自己经常在网上查找问题,许多解决方案都包括字典。但是,每当我尝试实现它们时,我的代码都会令人讨厌。例如,每次我想使用一个值时:

int x;
if (dict.TryGetValue("key", out x)) {
    DoSomethingWith(x);
}

这是4行代码,基本上可以执行以下操作: DoSomethingWith(dict["key"])

我听说使用out关键字是一种反模式,因为它会使函数改变其参数。

另外,我发现自己经常需要“反向”字典,在其中翻转键和值。

同样,我经常想遍历字典中的各项,发现自己将键或值转换为列表等以更好地做到这一点。

我觉得几乎总是有一种更好,更优雅的字典使用方式,但是我很茫然。


7
可能存在其他方式,但是我通常在尝试获取值之前首先使用ContainsKey
罗比迪

2
老实说,除了少数例外,如果你知道你的字典键是什么时间提前,有可能一个更好的办法。如果您不知道,通常不应该完全对dict键进行硬编码。词典是用于与半结构化的对象或数据,其中所述字段的至少工作有用大多正交给应用程序。IMO,这些领域越接近相关的业务/领域概念,使用这些概念的词典就越没有帮助。
svidgen

6
@RobbieDee:但是,在执行此操作时必须小心,因为这样做会造成竞争状况。在调用ContainsKey和获取值之间可能会删除键。
whatsisname

12
@whatsisname:在这种情况下,ConcurrentDictionary将更适合。System.Collections.Generic名称空间中的集合不是线程安全的
罗伯特·哈维

4
在50%的时间里,我看到某人使用Dictionary,他们真正想要的是一门新课。对于那些50%(仅)的东西,这是一种设计气味。
BlueRaja-Danny Pflughoeft,

Answers:


23

字典(C#或其他语言)只是一个容器,您可以在其中基于键查找值。在许多语言中,可以更正确地将其标识为Map,最常见的实现是HashMap。

要考虑的问题是当密钥不存在时会发生什么。某些语言通过返回nullnil或其他等效值来表现。默默地默认为一个值,而不是通知您不存在值。

不管是好是坏,C#库设计人员想出了一个习惯来应对这种行为。他们认为查找不存在的值的默认行为是引发异常。如果要避免异常,则可以使用Try变体。这与将字符串解析为整数或日期/时间对象所使用的方法相同。本质上,影响是这样的:

T count = int.Parse("12T45"); // throws exception

if (int.TryParse("12T45", out count))
{
    // Does not throw exception
}

然后继续到字典,其索引器将其委托给Get(index)

var myvalue = dict["12345"]; // throws exception
myvalue = dict.Get("12345"); // throws exception

if (dict.TryGet("12345", out myvalue))
{
    // Does not throw exception
}

这只是语言的设计方式。


应该out不鼓励使用变量吗?

C#不是拥有它们的第一种语言,它们在特定情况下有其用途。如果要构建高度并发的系统,则不能out在并发边界使用变量。

在许多方面,如果语言和核心库提供程序支持某个惯用语,我会尝试在我的API中采用这些惯用语。这样一来,API就会以该语言呈现出更加一致的效果。因此,用Ruby编写的方法看起来不会像用C#,C或Python编写的方法。他们每个人都有一种首选的代码构建方式,并且与之一起使用可帮助您的API用户更快地学习它。


地图一般是反模式吗?

他们有自己的目标,但是很多时候,对于您的目标,它们可能是错误的解决方案。特别是如果您有双向映射,则需要。有许多容器和组织数据的方式。您可以使用多种方法,有时在选择该容器之前需要三思。

如果双向映射值列表很短,则可能只需要一个元组列表。或结构列表,您可以在其中轻松找到映射两侧的第一个匹配项。

考虑问题领域,并选择最适合该工作的工具。如果没有,则创建它。


4
“那么,您可能只需要一个元组列表。或者一个结构列表,您可以在其中轻松找到映射两侧的第一个匹配项。” -如果对小集合进行了优化,则应在库中进行优化,而不是在用户代码中加盖戳记。
Blrfl

如果选择元组列表或字典,则这是实现细节。这是了解您的问题领域并使用正确的工具完成工作的问题。显然,存在一个列表,并且存在一个字典。对于通常情况,字典是正确的,但是对于一个或两个应用程序,您可能需要使用列表。
Berin Loritsch

3
是的,但是如果行为仍然相同,则应在库中进行确定。我已经遇到过其他语言的容器实现,如果事先告诉我们条目数量会很少,这些容器实现将切换算法。我同意您的回答,但是一旦弄清楚了,它应该放在一个库中,也许作为SmallBidirectionalMap。
Blrfl

5
“不管是好是坏,C#库设计人员想出了一个习惯来应对这种行为。” –我认为您的想法很糟糕:他们是用惯用语“出现”的,而不是使用现有的广泛使用的惯用语,后者是可选的返回类型。
约尔格W¯¯米塔格

3
@JörgWMittag如果您谈论的是Option<T>,那么我认为这将使该方法在C#2.0中更难使用,因为它没有模式匹配。

21

这里有一些关于哈希表/字典的一般原理的很好的答案。但我想我会谈谈您的代码示例,

int x;
if (dict.TryGetValue("key", out x)) 
{
    DoSomethingWith(x);
}

从C#7(我认为它已经使用大约两年了)开始,可以简化为:

if (dict.TryGetValue("key", out var x))
{
    DoSomethingWith(x);
}

当然可以减少到一行:

if (dict.TryGetValue("key", out var x)) DoSomethingWith(x);

如果您具有默认值(密钥不存在时),则可以变为:

DoSomethingWith(dict.TryGetValue("key", out var x) ? x : defaultValue);

因此,您可以通过使用合理的最新语言添加来获得紧凑形式。


1
很好地调用了v7语法,在允许使用var +1的同时,必须剪掉多余的定义行,这是个不错的选择,它是不错的选择
BrianH

2
另请注意,如果"key"不存在,x则将其初始化为default(TValue)
Peter Duniho,

作为通用扩展,也可能会很好,就像"key".DoSomethingWithThis()
Ross Presser在

我非常喜欢使用getOrElse("key", defaultValue) Null对象的设计仍然是我最喜欢的模式。以这种方式工作,您不必担心TryGetValue返回true还是false。
candied_orange

1
我觉得这个答案值得否认,因为紧凑而编写紧凑的代码是不好的。它会使您的代码更难阅读。另外,如果我没记错的话,TryGetValue它不是原子/线程安全的,因此您可以轻松地执行一项检查是否存在,而另一项则可以获取并操作该值
Marie

12

这既不是代码异味,也不是反模式,因为使用带有out参数的TryGet样式函数是惯用的C#。但是,C#提供了3个选项来使用“词典”,因此应确保针对您的情况使用了正确的选项。我想我知道使用out参数存在问题的谣言从何而来,所以我将在最后解决。

使用C#词典时要使用哪些功能:

  1. 如果确定键在词典中,请使用Item [TKey]属性
  2. 如果密钥通常应该在字典中,但是它不存在/不好/稀有/有问题,则应使用Try ... Catch,以便引发错误,然后可以尝试适当地处理错误
  3. 如果您不确定密钥是否在字典中,请使用带有Get参数的TryGet

为了证明这一点,只需要参考“备注”下的Dictionary TryGetValue文档

此方法结合了ContainsKey方法和Item [TKey]属性的功能。

...

如果您的代码经常尝试访问不在字典中的键,请使用TryGetValue方法。使用此方法比捕获Item [TKey]属性引发的KeyNotFoundException更有效。

此方法接近O(1)操作。

TryGetValue存在的全部原因是为了充当使用ContainsKey和Item [TKey]的更方便的方式,同时避免了必须两次搜索字典-因此假装它不存在并且手动执行它做的两件事是一个很尴尬的事情选择。

实际上,由于这种简单的格言,我很少使用过原始字典:选择最通用的类​​/容器,它可以为您提供所需的功能。字典的设计不是按值而不是按键查找(例如),因此,如果您要这样做,使用替代结构可能更有意义。我想我可能在上一个为期一年的开发项目中曾经使用过Dictionary,这只是因为它很少是我要完成的工作的正确工具。词典当然不是C#工具箱中的瑞士军刀。

没有参数怎么了?

CA1021:避免输出参数

尽管返回值是司空见惯的且经常使用,但正确使用out和ref参数需要中级设计和编码技能。为一般读者设计的图书馆建筑师不应期望用户精通out或ref参数。

我猜您在那儿听到了out参数就像是反模式。与所有规则一样,您应该仔细阅读以了解“为什么”,在这种情况下,甚至还明确提到了Try模式如何不违反规则

实现Try模式的方法(例如System.Int32.TryParse)不会引发此冲突。


我希望整个指南清单能使更多的人阅读。对于那些追随者,这是它的根源。docs.microsoft.com/en-us/visualstudio/code-quality/...
彼得Wone

10

在我看来,C#字典至少缺少两种方法,这些方法在许多情况下都可以用其他语言大量清除代码。第一个是返回Option,它使您可以在Scala中编写如下代码:

dict.get("key").map(doSomethingWith)

第二个是如果找不到密钥,则返回用户指定的默认值:

doSomethingWith(dict.getOrElse("key", "key not found"))

在适当的时候使用某种语言提供的成语要说些什么,例如Try模式,但这并不意味着您只需要使用该语言提供的成语即可。我们是程序员。可以创建新的抽象来使我们的特定情况更容易理解,特别是如果它消除了很多重复的话。如果您经常需要某些东西,例如反向查找或遍历值,请使其成为现实。创建您希望拥有的界面。


我喜欢第二个选项。也许我不得不写一个或两个扩展方法:)
Adam B

2
从正面看,c#扩展方法意味着您可以自己实现这些
jk。

5

这是4行代码,基本上可以执行以下操作: DoSomethingWith(dict["key"])

我同意这是不雅之举。在这种情况下,我喜欢使用一种机制,其中的值是struct类型:

public static V? TryGetValue<K, V>(
      this Dictionary<K, V> dict, K key) where V : struct => 
  dict.TryGetValue(key, out V v)) ? new V?(v) : new V?();

现在,我们有了TryGetValue返回的新版本int?。然后,我们可以做类似的技巧来扩展T?

public static void DoIt<T>(
      this T? item, Action<T> action) where T : struct
{
  if (item != null) action(item.GetValueOrDefault());
}

现在放在一起:

dict.TryGetValue("key").DoIt(DoSomethingWith);

我们只剩下一个清晰的陈述。

我听说使用out关键字是一种反模式,因为它会使函数改变其参数。

我的措辞不太强烈,并说尽可能避免变异是个好主意。

我发现自己经常需要一本“反向”词典,在其中翻转键和值。

然后实现或获取双向字典。它们很容易编写,或者Internet上有很多实现。这里有很多实现,例如:

/programming/268321/bidirectional-1-to-1-dictionary-in-c-sharp

同样,我经常想遍历字典中的各项,发现自己将键或值转换为列表等以更好地做到这一点。

当然可以。

我觉得几乎总是有一种更好,更优雅的字典使用方式,但是我很茫然。

问问自己“假设我有一个课而不是Dictionary实现我想做的确切操作;那课是什么样?” 然后,一旦您回答了这个问题,就实现该类。您是计算机程序员。通过编写计算机程序解决您的问题!


谢谢。您认为您可以对动作方法的实际作用进行更多的解释吗?我是新手。
亚当·B

1
@AdamB动作是一个对象,它表示调用void方法的能力。如您所见,在调用方,我们传递一个void方法的名称,该方法的参数列表与操作的通用类型参数匹配。在调用方,该动作将像其他方法一样被调用。
埃里克·利珀特

1
@AdamB:您可以认为它与Action<T>非常相似interface IAction<T> { void Invoke(T t); },但是对于该接口的“实现”以及如何Invoke调用该方法具有非常宽松的规则。如果您想了解更多信息,请学习C#中的“代理”,然后了解lambda表达式。
埃里克·利珀特

好吧,我想我明白了。因此,该操作让您放入一个方法作为DoIt()的参数。并调用该方法。
亚当·B

@AdamB:完全正确。这是“具有高阶功能的功能编程”的示例。大多数函数都将数据作为参数。高阶函数将函数作为参数,然后对这些函数进行处理。当您学习更多C#时,您会发现LINQ完全由高阶函数实现。
埃里克·利珀特

4

TryGetValue()仅当您不知道“键”是否作为字典中的键存在时,才需要使用该构造,否则DoSomethingWith(dict["key"])将完全有效。

一种“减少脏污”的方法可能是ContainsKey()用作检查。


6
“一种“不那么脏”的方法可能是使用ContainsKey()作为检查。我不同意。尽管不是最理想的TryGetValue情况,至少它使得很难忘记处理空箱。理想情况下,我希望这只会返回Optional。
亚历山大-恢复莫妮卡

1
ContainsKey多线程应用程序的方法存在一个潜在问题,即检查时间到使用时间(TOCTOU)漏洞。如果其他某个线程删除了对ContainsKey和的调用之间的键,该GetValue怎么办?
David Hammen

2
@JAD可选内容。您可以拥有Optional<Optional<T>>
亚历山大-恢复莫妮卡

2
@DavidHammen要清楚,您需要TryGetValue在上ConcurrentDictionary。常规词典不会同步
亚历山大-莫妮卡(Monica)恢复

2
@JAD不,(Java的可选类型很糟糕,不要让我入门)。我的发言更加抽象
亚历山大-莫妮卡(Monica)恢复

2

其他答案包含重点,因此在这里我不再赘述,而是将重点放在这一部分上,到目前为止,这部分似乎基本上被忽略了:

同样,我经常想遍历字典中的各项,发现自己将键或值转换为列表等以更好地做到这一点。

实际上,迭代字典很容易,因为它确实实现了IEnumerable

var dict = new Dictionary<int, string>();

foreach ( var item in dict ) {
    Console.WriteLine("{0} => {1}", item.Key, item.Value);
}

如果您更喜欢Linq,那也可以:

dict.Select(x=>x.Value).WhateverElseYouWant();

总而言之,我不认为字典是一种反模式-它们只是具有特定用途的特定工具。

此外,有关更多细节,请签出SortedDictionary(使用RB树以获得更可预测的性能)并SortedList(这也是一个易混淆的命名词典,它牺牲了插入速度来提高查找速度,但是如果您盯着固定的,排序集)。我已经在那里更换的情况下,DictionarySortedDictionary导致一个数量级更快地执行(但它可能发生轮的其他方式太)。


1

如果您觉得使用字典很尴尬,则可能不是解决问题的正确选择。字典很棒,但是就像一个评论者所注意到的那样,字典经常被用作本来应该是一类的东西的快捷方式。或者字典本身可能是核心存储方法,但是它周围应该有一个包装器类,以提供所需的服务方法。

关于Dict [key]与TryGet的说法很多。我经常使用KeyValuePair在字典上进行迭代。显然,这是一个鲜为人知的构造。

字典的主要好处是,随着项目数量的增加,与其他集合相比,它确实非常快。如果您必须检查某个键是否存在很多,您可能要问自己,使用字典是否合适。作为客户,您通常应该知道您所放置的内容,从而可以安全地查询什么。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.