有什么更有效的方法:Dictionary TryGetValue或ContainsKey + Item?


251

从MSDN在Dictionary.TryGetValue方法上的条目:

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

如果未找到键,则value参数将为值类型TValue获取适当的默认值;否则,值为0。例如,对于整数类型,0(零),对于布尔类型,false(假),对于引用类型,null。

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

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

从描述中,尚不清楚它是否比调用ContainsKey然后进行查找更有效或更方便。TryGetValue只是先执行ContainsKey然后再实现Item 的实现,还是实际上比通过一次查找更有效?

换句话说,什么是更有效的(即哪个执行的查询更少):

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

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

Answers:


313

TryGetValue 会更快。

ContainsKey使用与相同的检查TryGetValue,该检查内部引用实际的输入位置。该Item属性实际上具有与几乎相同的代码功能TryGetValue,除了它将引发异常而不返回false。

使用,ContainsKey后跟Item基本上是重复的查找功能,在这种情况下,这是大部分计算。


2
这更加微妙:if(dict.ContainsKey(ikey)) dict[ikey]++; else dict.Add(ikey, 0);。但是我认为TryGetValue由于使用了indexer属性的get set方法,效率仍然更高,不是吗?
蒂姆·施密特

4
您现在也可以实际查看它的.net源代码:referencesource.microsoft.com/#mscorlib/system/collections/… 您可以看到TryGetValue,ContainsKey和this []的全部三个都调用相同的FindEntry方法并执行相同的工作量,只是他们回答问题的方式不同:trygetvalue返回布尔值和值,包含键仅返回true / false,this []返回值或引发异常。
约翰·加德纳

1
@JohnGardner是的,这就是我说的-但是,如果您执行ContainsKey然后获取Item,那么您的工作就是2倍而不是1倍。
Reed Copsey

3
我完全同意:)我只是指出实际的来源现在可以使用。其他答案/等都没有指向实际来源的链接:D
John Gardner

1
稍微偏离主题,如果您在多线程环境中通过IDictionary访问,我将始终使用TryGetValue,因为状态从您调用ContainsKey时起可能会发生变化(不能保证TryGetValue也会在内部正确锁定,但这可能更安全)
克里斯·贝里(Chris Berry)

91

快速基准测试显示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

11
当然,这取决于实际的使用模式。如果您几乎从未失败过查找,那么TryGetValue应该遥遥领先。另外... nitpick ... DateTime不是捕获性能测量的最佳方法。
Ed S.

4
@EdS。您是对的,TryGetValue甚至可以更进一步。我编辑了答案,以包括“所有命中”和“所有未命中”方案。
dasblinkenlight 2012年

2
@Luciano解释你如何使用 Any -像这样:Any(i=>i.Key==key)。在这种情况下,是的,这是对字典的线性搜索的错误。
weston 2012年

13
DateTime.Now只能精确到几毫秒。改用Stopwatch类(在System.Diagnostics内部使用QueryPerformanceCounter可以提供更高的准确性)。它也更容易使用。
阿拉斯泰尔花胶

5
除了Alastair和Ed的注释-DateTime.Now还可以向后退,如果您获得时间更新,例如用户更新计算机时间,越过时区或时区更改(DST,for实例)。尝试在将系统时钟同步到某些无线电服务(例如GPS或移动电话网络)提供的时间的系统上工作。DateTime.Now将遍历所有地方,而DateTime.UtcNow仅解决这些原因之一。只需使用秒表。
antiduh 2014年

51

由于到目前为止,没有一个答案能真正回答这个问题,因此,经过一些研究,这是我可以接受的答案:

如果您对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组合的两倍。


20

谁在乎 :-)

您可能会问,因为TryGetValue使用起来很麻烦-因此用扩展方法将其封装起来。

public static class CollectionUtils
{
    // my original method
    // 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);
    // }


    // EDIT: one of many possible improved versions
    public static TValue GetValueOrDefault<K, V>(this IDictionary<K, V> dictionary, K key)
    {
        // initialized to default value (such as 0 or null depending upon type of TValue)
        TValue value;  

        // attempt to get the value of the key from the dictionary
        dictionary.TryGetValue(key, out value);
        return value;
    }

然后只需致电:

dict.GetValueOrDefault("keyname")

要么

(dict.GetValueOrDefault("keyname") ?? fallbackValue) 

1
@Hüseyin我很困惑,我如何愚蠢地张贴了这篇文章,this但事实证明,我在我的代码库中重复了两次我的方法-一次重复一次,一次不重复this,这就是为什么我从来没有抓住它!感谢修复!
Simon_Weaver

2
TryGetValue如果键不存在,则将默认值分配给out值参数,因此可以简化此操作。
拉斐尔·史密斯

2
简化版本:公共静态TValue GetValueOrDefault <TKey,TValue>(此字典<TKey,TValue> dict,TKey键){TValue ret; dict.TryGetValue(key,out ret); 返回ret }
约书亚

2
在C#7中,这真的很有趣:if(!dic.TryGetValue(key, out value item)) item = dic[key] = new Item();
Shimmy Weitzhandler

1
讽刺的是,真正的源代码已经有一个GetValueOrDefault()例程,但它是隐藏... referencesource.microsoft.com/#mscorlib/system/collections/...
戴文T.克辛

10

你为什么不测试呢?

但我很确定 TryGetValue会更快,因为它只执行一次查找。当然,这不能保证,即不同的实现可能具有不同的性能特征。

我实现字典的方式是通过创建内部 Find函数,该函数查找项目的插槽,然后在此之上构建其余部分。


我认为实现细节不可能改变保证一次执行动作X快于等于一次执行动作X的保证。最好的情况是它们相同,更糟糕的是2X版本需要两倍的时间。
丹·贝查德

9

到目前为止,所有的答案虽然很好,但都没有抓住重点。

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

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

传统上,无论语言,基础架构,操作系统,平台或机器体系结构如何,这种快捷方式(组合搜索和检索)都更加高效。它也更具可读性,因为它明确地表达了您的意图,而不是暗示您的意图(从代码的结构)。

因此,答案(是从一个古老的老旧黑客那里得到的)肯定是“是”(TryGetValue优于ContainsKey和Item [Get]的组合以从字典中检索值)。

如果您认为这听起来很奇怪,请这样想:即使TryGetValue,ContainsKey和Item [Get]的当前实现不会产生任何速度差异,您也可以假定将来的实现(例如.NET v5)会做(TryGetValue会更快)。考虑一下软件的生命周期。

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


2
尽管我100%同意您关于语义的观点,但是仍然值得进行性能测试。您永远都不知道所使用的API何时具有次优的实现,以至于语义正确的事情碰巧变慢了,除非您进行测试。
丹·贝查德

5

制定一个快速的测试程序,使用TryGetValue绝对可以改善字典中的一百万个项目。

结果:

ContainsKey + Item for 1000000 hits:45ms

100万次点击的TryGetValue:26ms

这是测试应用程序:

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

}

5

在我的机器上,如果有RAM负载,则在RELEASE模式(不是DEBUG)下运行时,如果找到,则ContainsKey等于TryGetValue/ 。try-catchDictionary<>

ContainsKey当只有少量字典条目未找到时,它们的性能远远超过所有它们(在下面的示例中,设置MAXVAL为比ENTRIES丢失某些条目更大的值):

结果:

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

我觉得这里有些鱼腥味。我想知道,由于您从未使用检索到的值,因此优化程序是否正在删除或简化ContainsKey()检查。
丹·贝查德

只是不能。ContainsKey()在编译的DLL中。优化器对ContainsKey()的实际作用一无所知。它可能会引起副作用,因此必须调用它并且不能删节。
AxD

这是假的。事实是,检查.NET代码表明ContainsKey,TryGetValue和this []都调用相同的内部代码,因此,当该条目存在时,TryGetValue的速度比ContainsKey + this []快。
Jim Balter

3

除了设计可以在实际设置中提供准确结果的微基准测试之外,您还可以检查.NET Framework的参考源。

它们都调用了FindEntry(TKey)完成大部分工作且不会记住其结果的方法,因此调用TryGetValue速度几乎是ContainsKey+的两倍Item


的不便界面TryGetValue可以使用扩展方法进行调整

using System.Collections.Generic;

namespace Project.Common.Extensions
{
    public static class DictionaryExtensions
    {
        public static TValue GetValueOrDefault<TKey, TValue>(
            this IDictionary<TKey, TValue> dictionary,
            TKey key,
            TValue defaultValue = default(TValue))
        {
            if (dictionary.TryGetValue(key, out TValue value))
            {
                return value;
            }
            return defaultValue;
        }
    }
}

从C#7.1开始,您可以default(TValue)用plain 替换default类型被推断。

用法:

var dict = new Dictionary<string, string>();
string val = dict.GetValueOrDefault("theKey", "value used if theKey is not found in dict");

它返回null查找失败的引用类型,除非指定了明确的默认值。

var dictObj = new Dictionary<string, object>();
object valObj = dictObj.GetValueOrDefault("nonexistent");
Debug.Assert(valObj == null);

val dictInt = new Dictionary<string, int>();
int valInt = dictInt.GetValueOrDefault("nonexistent");
Debug.Assert(valInt == 0);

请注意,扩展方法的用户无法分辨出不存在的密钥和存在的密钥之间的区别,但是其值为default(T)。
卢卡斯

在现代计算机上,如果快速连续两次调用子例程,那么调用一次子例程所花费的时间不太可能是两次。这是因为CPU和缓存体系结构很可能会缓存与第一个调用关联的许多指令和数据,因此第二个调用将更快地执行。另一方面,几乎可以肯定,两次通话要比一次通话花费更长的时间,因此,如果可能的话,消除第二次通话仍然有优势。
辩论者

2

如果您要尝试从字典中获取值,那么TryGetValue(key,out value)是最佳选择,但是如果您要检查键的存在,是否要进行新插入而不覆盖旧键,并且只有在该范围内,ContainsKey(key)是最佳选择,基准测试才能确认这一点:

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

这是一个真实的示例,我有一个服务,它为创建的每个“商品”关联一个渐进编号,该编号在每次创建新商品时都必须是免费的,如果删除某个商品,则该免费号码变为免费,当然这不是最佳的,因为我有一个静态变量来缓存当前数字,但是如果您结束所有数字,则可以从0重新开始到UInt32.MaxValue

执行测试:
将元素添加到哈希表中...
完成0,5908中–暂停....
将元素添加至字典中。
完成0,2679中-暂停....
查找第一个要插入的空闲数字
First方法:ContainsKey
Done in 0,0561-在字典中增加了1000000的值-暂停....
第二种方法:TryGetValue
Done in 0,0643-在字典中增加了1000001的值-暂停....
测试hashtable
Done in 0, 3015-将值1000000添加到哈希表中-暂停...。
按任意键继续。。

如果你们中的某些人可能在问ContainsKeys是否可以占优势,我什至尝试过使用Contains键反转TryGetValue,结果是一样的。

因此,对我来说,最后要考虑的是,这完全取决于程序的行为方式。

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.