以下のようなケースについて考えます。
Dictionary<string, object> dict = new();
dict.TryAdd("A", new());
dict.TryAdd("B", new());
dict.TryAdd("C", new());
意図通り動作するが…
TryAdd(key, new())
するとは辞書に追加されない場合もオブジェクトがインスタンス化されてるよね? っていうお話です。
実行環境: https://dotnetfiddle.net/テストコード
using System;
using System.Collections.Generic;
using System.Threading;
public class Program
{
public class Test
{
static int counter = 0;
public Test()
{
Console.WriteLine("Test ctor: " + counter++);
}
}
public static void Main()
{
var dict = new Dictionary<string, Test>();
dict.TryAdd("A", new());
dict.TryAdd("A", new());
dict.TryAdd("A", new());
// TryAdd での new() を避けるには・・・
if (!dict.ContainsKey("A"))
{
dict.Add("A", new());
}
Console.WriteLine("count: " + dict.Count);
var ilock = new Test();
Interlocked.CompareExchange(ref ilock, new(), null);
Interlocked.CompareExchange(ref ilock, new(), null);
Interlocked.CompareExchange(ref ilock, new(), null);
}
}
テスト結果は以下の通り。new() の数だけオブジェクトが作られて TryAdd
CompareExchange
に渡され、そして不要なら破棄されます。
条件が整った時のみ式を実行する特別扱いされたメソッドではないという事ですね。
Test ctor: 0
Test ctor: 1
Test ctor: 2
count: 1
Test ctor: 3
Test ctor: 4
Test ctor: 5
Test ctor: 6
コンパイラー側で良い感じにしてくれても良くね? と思うも勝手に式の優先順位変えるのはあり得ないか。辞書に限っては foreach の配列特別扱いと同様に
ContainsKey + Add
へ展開してくれても良さそうな気はする。
GitHub を見て安心感を得る
誰でもつい書いちゃうよね? という安心感を得るために公式のソースを狙い撃ちして探します。
検索ワード(検索結果への直リンクだと何故か一件も引っかからない)
language:C# /\.TryAdd\(.*,.*new.*\(\)\)/ AND (/namespace.*Microsoft/ OR /namespace.*System/)
--
オブジェクトを作るだけ作って即時破棄するようなコードパスは少ないですが、無くはないです。良かった!
Interlocked
は?
こっちも同志が見つかります 😊
language:C# /Interlocked\.CompareExchange\(ref.*,.*new.*\(\),.*null\)/ AND (/namespace.*Microsoft/ OR /namespace.*System/)
おまけ
調べたときにちょいちょい見かけた Interlocked.CompareExchange
を使ってダブルチェックを行うワンライナー。
return obj ?? Interlocked.CompareExchange(ref obj, new(), null) ?? obj;
new()
部分を遅延実行するような書き方は思いつかなかったし公式ソースにも見当たらなかったから多分無理?
👇 ダブルチェック
--
ConcurrentDictionary
や Interlocked.CompareExchange
はしょうがない(?)かもですが、普通の Dictionary
での TryAdd + new() は神経質になるほどではないが気に留めておいて次からは気を付けましょう、という感じでしょうか。
以上です。お疲れ様でした。