我进行了一些测试,无论是全部命中还是全部未命中,使用TryGetValue总是更快的。那么什么情况下应该使用ContainsKey呢?
我进行了一些测试,无论是全部命中还是全部未命中,使用TryGetValue总是更快的。那么什么情况下应该使用ContainsKey呢?
这取决于您使用这些方法的目的。如果您打开引用源代码,您会看到。
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;
}
public bool ContainsKey(TKey key)
{
return (this.FindEntry(key) >= 0);
}
就像你所看到的,TryGetValue
与ContainsKey
加一个数组查找是一样的。
如果你的逻辑只是检查键是否存在于Dictionary
中,而与此键相关的其他内容(获取键的值)没有任何关系,那么你应该使用ContainsKey
。
如果你想获取特定键的值,TryGetValue
比ContainsKey
更快。
if(dic.ContainsKey(keyValue))
{
dicValue = dic[keyValue]; // here you do more work!
}
Dictionary[key]
的逻辑
public TValue this[TKey key]
{
get {
int i = FindEntry(key);
if (i >= 0) return entries[i].value;
ThrowHelper.ThrowKeyNotFoundException();
return default(TValue);
}
set {
Insert(key, value, false);
}
}
如果你使用带有ContainsKey
的键,并且之后用dic[key]
获取值,那么基本上你将会执行FindEntry
方法和数组查找两次。这样就会增加一次调用FindEntry
的开销。
从概念上讲,这两种方法非常不同。 ContainsKey 仅检查给定的键是否在字典中。 TryGetValue 将尝试返回给定键的值(如果存在于字典中)。根据你想要做什么,两种方法都可以很快。
考虑以下方法,它从字典中返回一个值或返回 string.Empty。
Dictionary<int,string> Dict = new Dictionary<int,string>();
string GetValue1(int key)
{
string outValue;
if (!Dict.TryGetValue(key, out outValue))
outValue = string.Empty;
return outValue;
}
相比之下
string GetValue2(int key)
{
return (Dict.ContainsKey(key))
? Dict[key]
: string.Empty;
}
两者都相对较快,在大多数情况下它们的性能可以忽略不计(请参见下面的单元测试)。如果我们想要挑剔的话,使用TryGetValue需要使用out参数,这比普通参数多出了一些开销。如果未找到该值,则此变量将被设置为类型的默认值,对于字符串来说是null。在上面的示例中,虽然没有使用这个null值,但我们仍然产生了额外的开销。最后,GetValue1需要使用一个本地变量outValue,而GetValue2不需要。
BACON指出,在值被找到的情况下,GetValue2将使用2次查找,这比较昂贵。这是正确的,这也意味着在键未被找到的情况下,GetValue2的性能会更快。因此,如果程序预计大部分查找都未命中,请使用GetValue2。否则,请使用GetValue1。
如果处理ConcurrentDictionary,请使用TryGetValue,因为在多线程环境下对其进行的操作应该是原子的,即一旦您在检查值时,当您尝试访问该值时,该检查可能是不正确的。
下面包含2个单元测试,测试具有不同Key类型(int vs string)的字典中这两种方法的性能。此基准测试仅展示这两种方法之间的差距如何随着上下文的变化而缩小/扩大。
[TestMethod]
public void TestString()
{
int counter = 10000000;
for (var x = 0; x < counter; x++)
DictString.Add(x.ToString(), "hello");
TimedLog("10,000,000 hits TryGet", () =>
{
for (var x = 0; x < counter; x++)
Assert.IsFalse(string.IsNullOrEmpty(GetValue1String(x.ToString())));
}, Console.WriteLine);
TimedLog("10,000,000 hits ContainsKey", () =>
{
for (var x = 0; x < counter; x++)
Assert.IsFalse(string.IsNullOrEmpty(GetValue2String(x.ToString())));
}, Console.WriteLine);
TimedLog("10,000,000 misses TryGet", () =>
{
for (var x = counter; x < counter*2; x++)
Assert.IsTrue(string.IsNullOrEmpty(GetValue1String(x.ToString())));
}, Console.WriteLine);
TimedLog("10,000,000 misses ContainsKey", () =>
{
for (var x = counter; x < counter*2; x++)
Assert.IsTrue(string.IsNullOrEmpty(GetValue2String(x.ToString())));
}, Console.WriteLine);
}
[TestMethod]
public void TestInt()
{
int counter = 10000000;
for (var x = 0; x < counter; x++)
DictInt.Add(x, "hello");
TimedLog("10,000,000 hits TryGet", () =>
{
for (var x = 0; x < counter; x++)
Assert.IsFalse(string.IsNullOrEmpty(GetValue1Int(x)));
}, Console.WriteLine);
TimedLog("10,000,000 hits ContainsKey", () =>
{
for (var x = 0; x < counter; x++)
Assert.IsFalse(string.IsNullOrEmpty(GetValue2Int(x)));
}, Console.WriteLine);
TimedLog("10,000,000 misses TryGet", () =>
{
for (var x = counter; x < counter * 2; x++)
Assert.IsTrue(string.IsNullOrEmpty(GetValue1Int(x)));
}, Console.WriteLine);
TimedLog("10,000,000 misses ContainsKey", () =>
{
for (var x = counter; x < counter * 2; x++)
Assert.IsTrue(string.IsNullOrEmpty(GetValue2Int(x)));
}, Console.WriteLine);
}
public static void TimedLog(string message, Action toPerform, Action<string> logger)
{
var start = DateTime.Now;
if (logger != null)
logger.Invoke(string.Format("{0} Started at {1:G}", message, start));
toPerform.Invoke();
var end = DateTime.Now;
var span = end - start;
if (logger != null)
logger.Invoke(string.Format("{0} Ended at {1} lasting {2:G}", message, end, span));
}
TestInt 的结果
10,000,000 hits TryGet Started at ...
10,000,000 hits TryGet Ended at ... lasting 0:00:00:00.3734136
10,000,000 hits ContainsKey Started at ...
10,000,000 hits ContainsKey Ended at ... lasting 0:00:00:00.4657632
10,000,000 misses TryGet Started at ...
10,000,000 misses TryGet Ended at ... lasting 0:00:00:00.2921058
10,000,000 misses ContainsKey Started at ...
10,000,000 misses ContainsKey Ended at ... lasting 0:00:00:00.2579766
对于命中的情况,ContainsKey 大约比 TryGetValue 慢 25%。
对于未命中的情况,TryGetValue 大约比 ContainsKey 慢 13%。
TestString 的测试结果。
10,000,000 hits TryGet Started at ...
10,000,000 hits TryGet Ended at ... lasting 0:00:00:03.2232018
10,000,000 hits ContainsKey Started at ...
10,000,000 hits ContainsKey Ended at ... lasting 0:00:00:03.6417864
10,000,000 misses TryGet Started at ...
10,000,000 misses TryGet Ended at ... lasting 0:00:00:03.6508206
10,000,000 misses ContainsKey Started at ...
10,000,000 misses ContainsKey Ended at ... lasting 0:00:00:03.4912164
对于命中,ContainsKey比TryGetValue慢约13%
对于未命中,TryGetValue比ContainsKey慢约4.6%
TryGetValue
的文档指出:“此方法结合了ContainsKey方法和Item属性的功能。”虽然相对于GetValue2
,GetValue1
具有本地变量和通过引用传递参数的“开销”,但与在Dict
包含键key
的情况下执行两次查找的GetValue2
相比,这是可以忽略不计的:一次在调用Dict.ContainsKey(key)
时进行查找,另一次在检索Dict[key]
时进行查找。 - Lance U. MatthewsContainsKey
,TryGetValue
只需要执行额外的跳转并写入输出参数。我认为这个答案过于关注微观优化,如避免使用局部变量和输出参数。这个基准测试显示,在1000万次查找(全部未命中)中,TryGetValue
仅比ContainsKey
略慢。除非有特殊情况证明TryGetValue
成为瓶颈,并且测试表明[继续] - Lance U. MatthewsContainsKey
+ Dict[key]
提供了值得的性能提升,我认为一般建议应该根据其意图选择 Dictionary<>
方法:ContainsKey
用于检查不需要值的键是否存在,而 TryGetValue
则用于尝试检索可能不存在的键的值。 - Lance U. Matthews
Dictionary
),这两个函数都调用了一个名为FindEntry()
的函数,只不过TryGetValue
在执行完更多代码后(尽管只有一次赋值!)才结束,因此它不可能更快。https://referencesource.microsoft.com/#mscorlib/system/collections/generic/dictionary.cs,bcd13bb775d408f1 - starlight54