C# PR

【C#】ContainsKey VS TryGetValue!!速いのはTryGetValue

アイキャッチ(c_sharp)
記事内に商品プロモーションを含む場合があります

こんにちは!
なかむぅです。

DictionaryクラスのメソッドでKeyが存在するかチェックできる

  • ContainsKey(TKey key)
  • TryGetValue(TKey key, out TValue value)

といった2つのメソッドがでてきて

どっちがええんや

となったので違いを調べてみました。

ContainsKeyとTryGetValueってなに?

という方は以下の記事を参考にしてください。

アイキャッチ(c_sharp)
【C#】ContainsKeyはKeyの存在チェックです【Dictionary】こんにちは! なかむぅです。 C#のコードを読んでいるとContainsKey(TKey key)というものがでてきて ...
アイキャッチ(c_sharp)
【C#】TryGetValueはKeyの存在チェックしたついでに値を取得こんにちは! なかむぅです。 C#のコードを読んでいるとTryGetValue(TKey key, out TValue val...

ContainsKeyとTryGetValueの書き方

書き方は第二引数があるかないか。


if (dictionary.ContainsKey("hoge"))
{
	Console.WriteLine($"「hoge」のKeyには「{dictionary["hoge"]}」が入っています");
}
if (dictionary.TryGetValue("hoge", out string value))
{
	Console.WriteLine($"「hoge」のKeyには「{value}」が入っています");
}

TryGetValueは一度Keyを渡せばoutの変数に値が入るので、値の取得が楽ちんです。

ContainsKeyはKeyを指定して値を取得しないといけないので、その分のコストがかかります。

ついでに中身も見てみたのですが、どちらもFindEntryを呼び出してチェックしていました。

ContainsKeyは演算子で比較してそのまま返していますが、TryGetValueoutで渡ってきた引数に値を入れてから返しています。

dictionary.cs

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

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

実際のFindEntryの中身はこちら。

dictionary.cs

private int FindEntry(TKey key)
{
	if (key == null)
	{
		ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
	}

	if (buckets != null)
	{
		int hashCode = comparer.GetHashCode(key) & 0x7FFFFFFF;
		for (int i = buckets[hashCode % buckets.Length]; i >= 0; i = entries[i].next)
		{
			if (entries[i].hashCode == hashCode && comparer.Equals(entries[i].key, key)) return i;
		}
	}
	return -1;
}

書き方についてはTryGetValueのほうがスッキリ書けて良いかなと思います。

存在しないKeyを取得しようとしたとき

存在しないKeyを取得しようとしたときは以下のような結果になります。

  • ContainsKeyは例外が発生
  • TryGetValueはvalueが型のdefault値が返ってくる

var dictionary = new Dictionary<string, string>();
if (!dictionary.ContainsKey("hoge"))
{
	try
	{
		Console.WriteLine($"ContainsKey:{dictionary["hoge"]}");
	}
	catch
	{
		// 例外が発生する
		Console.WriteLine($"ContainsKeyは例外");
	}
}
if (!dictionary.TryGetValue("hoge", out string value))
{
	if (value is null)
	{
		// string型のデフォルト値nullが返ってきている
		Console.WriteLine($"TryGetValueのvalueはnull");
	}
}
結果

ContainsKeyは例外
TryGetValueの valueは null

中身を見てみると、TryGetValueは例外が発生しないようにoutで渡ってきた引数にdefaultを入れて返しているからでした。

dictionary.cs

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

TryGetValuedefault値のまま行ってしまいますが、ContainsKeyは例外が発生するので、どこに問題があるかがわかりやすくなると思います。

ContainsKeyとTryGetValueの速度

ContainsKeyTryGetValueでどちらが速いか測定してみました。

Countプロパティ分のDictionaryを作って、Countプロパティ分のListにセットしていく処理にしています。

回数は1000回と10000回で行いました。


private int Count { get; } = 1000; // セットするKeyの数
public void CheckContainsKeyVsTryGetValue()
{
	// ContainsKeyとTryGetValueの測定を入れ替えると結果が変わってしまう
	CheckContainsKey(GetDictionary()); //①
	CheckTryGetValue(GetDictionary()); //②
}

private Dictionary<int, int> GetDictionary()
{
	var dictionary = new Dictionary<int, int>();
	for (int i = 0; i < Count; i++)
	{
		dictionary.Add(i, i);
	}
	return dictionary;
}

private void CheckContainsKey(Dictionary<int, int> dictionary)
{
	var list = new List<int>(Count);
	var sw = new Stopwatch();
	sw.Start();

	for (int i = 0; i < Count; i++)
	{
		if (dictionary.ContainsKey(i)) list.Add(dictionary[i]);
	}

	sw.Stop();
	Console.WriteLine($"ContainsKey :{sw.Elapsed} ms");
}

private void CheckTryGetValue(Dictionary<int, int> dictionary)
{
	var list = new List<int>(Count);
	var sw = new Stopwatch();
	sw.Start();

	for (int i = 0; i < Count; i++)
	{
		if (dictionary.TryGetValue(i, out int value)) list.Add(value);
	}

	sw.Stop();

	Console.WriteLine($"TryGetValue :{sw.Elapsed} ms");
}

ちなみに、上記のソースコードだとContainsKeyTryGetValueの測定する順番を入れ替えたときに結果が変わってしまったので、それぞれコメントアウトして1つずつ測定してます。

同時に行うと、後から測定した方が速くなってしまうみたいです。

結果:ContainsKeyから先に測定したとき

ContainsKey : 00:00:00.0000448 ms
TryGetValue : 00:00:00.0000212 ms
結果:TryGetValueから先に測定したとき

TryGetValue : 00:00:00.0000359 ms
ContainsKey : 00:00:00.0000288 ms

なので、以下の手順で測定しました。

  • ②のCheckTryGetValueをコメントアウトして①のCheckContainsKeyを実行
  • ①のCheckContainsKeyをコメントアウトして②のCheckTryGetValueを実行

結果、TryGetValueのほうが速かったです。

結果:1000回

ContainsKey : 00:00:00.0000450 ms
TryGetValue : 00:00:00.0000354 ms
結果:10000回

ContainsKey : 00:00:00.0002018 ms
TryGetValue : 00:00:00.0001414 ms

速度はTryGetValueのほうが速いですね。


私の会社で一緒に働きませんか?

私が働く会社では、一緒に働いてくれるエンジニアを募集しています♪

こんな人がオススメ
  • 長く働いていても年収が全然上がらない…一気に年収を上げたい
  • 残業をしたくてしているわけではないので、残業代はしっかりと出して欲しい
  • やりたいことに挑戦させてもらえない…自分がしてみたいことに挑戦したい
  • 通勤時間が長すぎるから、できるだけ短くしたい
  • もっと勉強したいから、書籍代や資格の受験料など負担してくれるところに入りたい
  • 現場に駆り出されてからは放置プレイ…相談できる人も先輩も居ないので、自分の状況をちゃんと理解してくれるところで働きたい

上記内容に1つでも当てはまる場合は、ぜひお声がけください。
私のサイトから応募していただいた方にはお好きなギフト券5000円分プレゼントさせていただきます(条件あり)。

詳細ページ


まとめ:結論

ContainsKey TryGetValue
記述量 ×
不具合の検知 ×
速度 ×

結論、TryGetValueを使用するのが良さそうですね。

否定演算子を使ってdefault値が返ってきたときの処理をしっかりと書けば、Keyがセットされていないときの対処もできると思います。


if (dictionary.TryGetValue(i, out int value))
{
	// default値が返ってきた時の処理
}

他に比較する要素が見当たらないので、もし他にもあればコメントください。

以上、 ContainsKeyTryGetValueについてでした!

いやここは違うだろ

と思った方はコメントいただけると嬉しいです。

最後までお読みいただきありがとうございました!

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です