什么是更有效的:字典TryGetValue或ContainsKey +项目?

从Dictionary.TryGetValue方法的 MSDN条目:

此方法结合了ContainsKey方法和Item属性的function。

如果未find该键,则value参数将为值typesTValue获取适当的默认值; 例如,整数types为0(零),布尔types为false,参考types为null。

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

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

从描述来看,不清楚它是否比调用ContainsKey然后再查找更有效率或者更方便。 TryGetValue的实现只是调用ContainsKey然后Item或者实际上比单个查找更有效率?

换句话说,什么是更有效的(即哪一个执行较less的查找):

 Dictionary<int,int> dict; //...// int ival; if(dict.ContainsKey(ikey)) { ival = dict[ikey]; } else { ival = default(int); } 

要么

 Dictionary<int,int> dict; //...// int ival; dict.TryGetValue(ikey, out ival); 

注意:我不是在寻找一个基准!

TryGetValue会更快。

ContainsKey使用与TryGetValue相同的检查,它在内部引用实际的入口位置。 Item属性实际上具有与TryGetValue几乎相同的代码function,只不过它会抛出exception而不是返回false。

使用ContainsKey后面的项目基本上复制查找function,这是这种情况下计算的大部分。

一个快速的基准testing显示TryGetValue略有优势:

  static void Main() { var d = new Dictionary<string, string> {{"a", "b"}}; var start = DateTime.Now; for (int i = 0; i != 10000000; i++) { string x; if (!d.TryGetValue("a", out x)) throw new ApplicationException("Oops"); if (d.TryGetValue("b", out x)) throw new ApplicationException("Oops"); } Console.WriteLine(DateTime.Now-start); start = DateTime.Now; for (int i = 0; i != 10000000; i++) { string x; if (d.ContainsKey("a")) { x = d["a"]; } else { x = default(string); } if (d.ContainsKey("b")) { x = d["b"]; } else { x = default(string); } } } 

这产生

 00:00:00.7600000 00:00:01.0610000 

使得ContainsKey + Item访问速度慢40%,假设点击和未命中混合。

此外,当我改变程序总是错过(即总是仰望"b" )这两个版本变得同样快速:

 00:00:00.2850000 00:00:00.2720000 

但是,当我把它“全部命中”的时候, TryGetValue仍然是一个明显的赢家:

 00:00:00.4930000 00:00:00.8110000 

由于到目前为止答案都没有真正回答这个问题,所以在一些研究之后,我发现了一个可以接受的答案:

如果你反编译TryGetValue,你会发现它是这样做的:

 public bool TryGetValue(TKey key, out TValue value) { int index = this.FindEntry(key); if (index >= 0) { value = this.entries[index].value; return true; } value = default(TValue); return false; } 

而ContainsKey方法是:

 public bool ContainsKey(TKey key) { return (this.FindEntry(key) >= 0); } 

所以TryGetValue只是ContainsKey加上一个数组查找,如果该项目是存在的。

资源

似乎TryGetValue几乎是ContainsKey + Item组合的两倍。

谁在乎 :-)

你可能会问,因为TryGetValue是一个痛苦的使用 – 所以这样抽象。

 public static class CollectionUtils { public static V GetValueOrDefault<K, V>(this Dictionary<K, V> dic, K key) { V ret; bool found = dic.TryGetValue(key, out ret); if (found) { return ret; } return default(V); } 

然后,只需拨打:

 dict.GetValueOrDefault("keyname"); 

你为什么不testing它?

但我很确定TryGetValue更快,因为它只做一个查询。 当然,这是不能保证的,即不同的实现可能会有不同的性能特点。

我实现一个字典的方式是创build一个内部Findfunction,find一个项目的插槽,然后build立其余的顶部。

到目前为止,所有的答案虽然好,但却错过了一个重要的点。

进入API类(例如,.NET框架)的方法构成接口定义(不是C#或VB接口,而是计算机科学意义上的接口)的一部分。

因此,除非速度是正式接口定义的一部分(在这种情况下不是这样),否则调用这种方法是否更快是不正确的。

传统上,这种快捷方式(结合search和检索)无论语言,基础结构,操作系统,平台还是机器架构都更为高效。 它也更具可读性,因为它明确地expression了你的意图,而不是暗示它(来自你的代码结构)。

所以答案(从灰白的老黑客)肯定是“是”(TryGetValue比ContainsKey和Item [Get]的组合更可取的字典值)。

如果你觉得这听起来很奇怪,可以这样想:即使TryGetValue,ContainsKey和Item [Get]的当前实现没有产生任何速度差异,你可以认为未来的实现(例如.NET v5)会做(TryGetValue会更快)。 想想你的软件的生命周期。

顺便说一下,有趣的是,典型的现代接口定义技术仍然很less提供正式定义时序约束的手段。 也许.NET v5?

制作一个快速testing程序,使用TryGetValue和字典中的一百万个项目肯定会有所改进。

结果:

包含1000000个点击次数的键+项目:45毫秒

TryGetValue 1000000点击次数:26ms

这是testing应用程序:

 static void Main(string[] args) { const int size = 1000000; var dict = new Dictionary<int, string>(); for (int i = 0; i < size; i++) { dict.Add(i, i.ToString()); } var sw = new Stopwatch(); string result; sw.Start(); for (int i = 0; i < size; i++) { if (dict.ContainsKey(i)) result = dict[i]; } sw.Stop(); Console.WriteLine("ContainsKey + Item for {0} hits: {1}ms", size, sw.ElapsedMilliseconds); sw.Reset(); sw.Start(); for (int i = 0; i < size; i++) { dict.TryGetValue(i, out result); } sw.Stop(); Console.WriteLine("TryGetValue for {0} hits: {1}ms", size, sw.ElapsedMilliseconds); } 

在运行RELEASE模式(不是DEBUG)的情况下,在我的机器上装载大量RAM,如果findDictionary<>中的所有条目,则ContainsKey等于TryGetValue / try-catch

当只有几个字典条目没有find的时候, ContainsKey性能远胜于所有的字典条目(在我的例子中,将MAXVAL为比ENTRIES更大的值,以使某些条目没有find):

结果:

 Finished evaluation .... Time distribution: Size: 000010: TryGetValue: 53,24%, ContainsKey: 1,74%, try-catch: 45,01% - Total: 2.006,00 Size: 000020: TryGetValue: 37,66%, ContainsKey: 0,53%, try-catch: 61,81% - Total: 2.443,00 Size: 000040: TryGetValue: 22,02%, ContainsKey: 0,73%, try-catch: 77,25% - Total: 7.147,00 Size: 000080: TryGetValue: 31,46%, ContainsKey: 0,42%, try-catch: 68,12% - Total: 17.793,00 Size: 000160: TryGetValue: 33,66%, ContainsKey: 0,37%, try-catch: 65,97% - Total: 36.840,00 Size: 000320: TryGetValue: 34,53%, ContainsKey: 0,39%, try-catch: 65,09% - Total: 71.059,00 Size: 000640: TryGetValue: 32,91%, ContainsKey: 0,32%, try-catch: 66,77% - Total: 141.789,00 Size: 001280: TryGetValue: 39,02%, ContainsKey: 0,35%, try-catch: 60,64% - Total: 244.657,00 Size: 002560: TryGetValue: 35,48%, ContainsKey: 0,19%, try-catch: 64,33% - Total: 420.121,00 Size: 005120: TryGetValue: 43,41%, ContainsKey: 0,24%, try-catch: 56,34% - Total: 625.969,00 Size: 010240: TryGetValue: 29,64%, ContainsKey: 0,61%, try-catch: 69,75% - Total: 1.197.242,00 Size: 020480: TryGetValue: 35,14%, ContainsKey: 0,53%, try-catch: 64,33% - Total: 2.405.821,00 Size: 040960: TryGetValue: 37,28%, ContainsKey: 0,24%, try-catch: 62,48% - Total: 4.200.839,00 Size: 081920: TryGetValue: 29,68%, ContainsKey: 0,54%, try-catch: 69,77% - Total: 8.980.230,00 

这是我的代码:

  using System; using System.Collections.Generic; using System.Diagnostics; namespace ConsoleApplication1 { class Program { static void Main(string[] args) { const int ENTRIES = 10000, MAXVAL = 15000, TRIALS = 100000, MULTIPLIER = 2; Dictionary<int, int> values = new Dictionary<int, int>(); Random r = new Random(); int[] lookups = new int[TRIALS]; int val; List<Tuple<long, long, long>> durations = new List<Tuple<long, long, long>>(8); for (int i = 0;i < ENTRIES;++i) try { values.Add(r.Next(MAXVAL), r.Next()); } catch { --i; } for (int i = 0;i < TRIALS;++i) lookups[i] = r.Next(MAXVAL); Stopwatch sw = new Stopwatch(); ConsoleColor bu = Console.ForegroundColor; for (int size = 10;size <= TRIALS;size *= MULTIPLIER) { long a, b, c; Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("Loop size: {0}", size); Console.ForegroundColor = bu; // --------------------------------------------------------------------- sw.Start(); for (int i = 0;i < size;++i) values.TryGetValue(lookups[i], out val); sw.Stop(); Console.WriteLine("TryGetValue: {0}", a = sw.ElapsedTicks); // --------------------------------------------------------------------- sw.Restart(); for (int i = 0;i < size;++i) val = values.ContainsKey(lookups[i]) ? values[lookups[i]] : default(int); sw.Stop(); Console.WriteLine("ContainsKey: {0}", b = sw.ElapsedTicks); // --------------------------------------------------------------------- sw.Restart(); for (int i = 0;i < size;++i) try { val = values[lookups[i]]; } catch { } sw.Stop(); Console.WriteLine("try-catch: {0}", c = sw.ElapsedTicks); // --------------------------------------------------------------------- Console.WriteLine(); durations.Add(new Tuple<long, long, long>(a, b, c)); } Console.ForegroundColor = ConsoleColor.Yellow; Console.WriteLine("Finished evaluation .... Time distribution:"); Console.ForegroundColor = bu; val = 10; foreach (Tuple<long, long, long> d in durations) { long sum = d.Item1 + d.Item2 + d.Item3; Console.WriteLine("Size: {0:D6}:", val); Console.WriteLine("TryGetValue: {0:P2}, ContainsKey: {1:P2}, try-catch: {2:P2} - Total: {3:N}", (decimal)d.Item1 / sum, (decimal)d.Item2 / sum, (decimal)d.Item3 / sum, sum); val *= MULTIPLIER; } Console.WriteLine(); } } } 

如果试图从字典中获取值,则TryGetValue(key,out值)是最好的select,但是如果您正在检查密钥的存在,对于新的插入,而不覆盖旧的密钥,只有在这个范围内,ContainsKey(key)才是最好的select,benchmark可以证实这一点:

 using System; using System.Threading; using System.Diagnostics; using System.Collections.Generic; using System.Collections; namespace benchmark { class Program { public static Random m_Rand = new Random(); public static Dictionary<int, int> testdict = new Dictionary<int, int>(); public static Hashtable testhash = new Hashtable(); public static void Main(string[] args) { Console.WriteLine("Adding elements into hashtable..."); Stopwatch watch = Stopwatch.StartNew(); for(int i=0; i<1000000; i++) testhash[i]=m_Rand.Next(); watch.Stop(); Console.WriteLine("Done in {0:F4} -- pause....", watch.Elapsed.TotalSeconds); Thread.Sleep(4000); Console.WriteLine("Adding elements into dictionary..."); watch = Stopwatch.StartNew(); for(int i=0; i<1000000; i++) testdict[i]=m_Rand.Next(); watch.Stop(); Console.WriteLine("Done in {0:F4} -- pause....", watch.Elapsed.TotalSeconds); Thread.Sleep(4000); Console.WriteLine("Finding the first free number for insertion"); Console.WriteLine("First method: ContainsKey"); watch = Stopwatch.StartNew(); int intero=0; while (testdict.ContainsKey(intero)) { intero++; } testdict.Add(intero, m_Rand.Next()); watch.Stop(); Console.WriteLine("Done in {0:F4} -- added value {1} in dictionary -- pause....", watch.Elapsed.TotalSeconds, intero); Thread.Sleep(4000); Console.WriteLine("Second method: TryGetValue"); watch = Stopwatch.StartNew(); intero=0; int result=0; while(testdict.TryGetValue(intero, out result)) { intero++; } testdict.Add(intero, m_Rand.Next()); watch.Stop(); Console.WriteLine("Done in {0:F4} -- added value {1} in dictionary -- pause....", watch.Elapsed.TotalSeconds, intero); Thread.Sleep(4000); Console.WriteLine("Test hashtable"); watch = Stopwatch.StartNew(); intero=0; while(testhash.Contains(intero)) { intero++; } testhash.Add(intero, m_Rand.Next()); watch.Stop(); Console.WriteLine("Done in {0:F4} -- added value {1} into hashtable -- pause....", watch.Elapsed.TotalSeconds, intero); Console.Write("Press any key to continue . . . "); Console.ReadKey(true); } } } 

这是一个真实的例子,我有一个服务,创build每个“项目”,它关联一个累进号码,这个号码,每次你创build一个新的项目,必须find自由,如果你删除一个项目,免费号码变成免费的,当然这不是最优化的,因为我有一个静态的varcaching当前的数字,但万一你结束了所有的数字,你可以重新从0开始到UInt32.MaxValue

testing执行:
将元素添加到散列表…
完成在0,5908 – 暂停….
将元素添加到字典中…
完成在0,2679 – 暂停….
find第一个可用的插入号码
第一种方法:ContainsKey
完成在0,0561 – 在字典中增加值1000000 – 暂停….
第二种方法:TryGetValue
在0.0643完成 – 在字典中增加值1000001 – 暂停….
testing散列表
完成在03015 – 增加值1000000到哈希表 – 暂停….
按任意键继续 。 。

如果你们中的一些人可能会问ContainsKeys是否有优势,我甚至试着用Contains键反转TryGetValue,结果是一样的。

所以,对于我来说,最后的考虑,这一切都取决于程序的行为方式。

真的,他们应该服务于两个不同的目标。

  • 当你想确定密钥是否被设置时, ContainsKey应该被使用,但是你不关心这个实际是什么。
  • TryGetValue应该在你需要物品的时候使用。