哪种更有效:Dictionary TryGetValue还是ContainsKey+Item?

来自MSDN在字典。TryGetValue方法上的条目:

这个方法结合了ContainsKey方法和

如果没有找到键,则value参数得到适当的值 值类型为TValue的默认值;例如,0(零)为

.整数类型,布尔类型为false,引用类型为null 如果您的代码经常尝试访问,请使用TryGetValue方法 不在字典中的键。使用这种方法比较多 比捕获Item抛出的KeyNotFoundException更有效 财产。< / p >

这个方法接近于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);

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

251674 次浏览

你为什么不测试一下呢?

但我非常确定TryGetValue更快,因为它只做一次查找。当然这并不能保证,即不同的实现可能有不同的性能特征。

我实现字典的方法是创建一个内部Find函数,该函数为一个条目查找插槽,然后在此基础上构建其余部分。

TryGetValue将更快。

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

使用ContainsKey后跟Item基本上重复了查找功能,这是本例中的大部分计算。

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

制作一个快速测试程序,使用字典中包含100万个项的TryGetValue显然有很大的改进。

结果:

ContainsKey + Item为1000000次:45ms

1000000次的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);


}

由于到目前为止没有一个答案真正回答了这个问题,下面是我在一些研究后找到的一个可以接受的答案:

如果你反编译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加上数组查找,如果项目是存在的。

< a href = " http://blog.lab49.com/archives/840 " > < / >来源

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

如果你试图从字典中获取值,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);
}
}
}

这是一个真实的例子,我有一个服务,对于每个创建的“Item”,它关联一个累进的数字,这个数字,每次你创建一个新项目,必须找到空闲的,如果你删除一个Item,空闲的数字变成空闲的,当然这不是优化的,因为我有一个静态的变量,缓存当前的数字,但如果你结束所有的数字,你可以从0到UInt32重新开始。MaxValue < br >

< br > < p >测试执行: 将元素添加到哈希表中 完成在0,5908 -暂停....
向字典中添加元素 完成在0,2679 -暂停....
找到插入
的第一个空闲数 第一种方法:ContainsKey
在0,0561中完成——在字典中添加值1000000——暂停....
第二种方法:TryGetValue
在0,0643完成——在字典中添加值1000001——暂停....
测试hashtable < br > 完成在0,3015 -添加值1000000到哈希表-暂停....
按任意键继续。< br > < / p >

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

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

在我的机器上,有RAM负载,当以RELEASE模式(不是DEBUG)运行时,如果Dictionary<>中的所有条目都找到了,ContainsKey等于TryGetValue/try-catch

到目前为止,当只有几个字典条目没有找到时,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();
}
}
}

谁在乎呢? -)

你可能会问,因为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)

到目前为止,所有的答案虽然不错,但都忽略了一个关键问题。

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

因此,询问调用这样的方法是否更快通常是不正确的,除非速度是正式接口定义的一部分(在本例中并非如此)。

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

所以答案(来自一个灰色的老黑客)肯定是“是的”(TryGetValue比ContainsKey和Item [Get]的组合更可取,以从字典中检索值)。

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

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

除了设计一个能够在实际环境中给出准确结果的微基准测试外,您还可以查看. net Framework的参考源代码。

它们都调用FindEntry(TKey)方法,该方法完成大部分工作,并且不记住其结果,因此调用__ABC1的速度几乎是__ABC2 + 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替换default(TValue)推断类型

用法:

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


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