🔬

[C#]CollectionsMarshal の解説

2022/01/07に公開

.NET 5 で System.Runtime.InteropServices.CollectionsMarshal というクラスが導入されました。

List<T>Dictionary<TKey, TValue> の少し低レベルなところを触らせてくれます。

List<T>

AsSpan

導入時期: .NET 5

List<T> の内部配列を参照する Span<T> を取り出します。

List<T>.Add の前後で使ったりすると危ういので、一時的な用途に限定して使いましょう。

using System.Runtime.InteropServices;

var list = Enumerable.Range(0, 10).ToList();
var span = CollectionsMarshal.AsSpan(list);
span[^1] = -1;
Console.WriteLine(string.Join(", ", list));
// 0, 1, 2, 3, 4, 5, 6, 7, 8, -1

list.Add(100); // 内部配列が再確保される
span[0] = -2;  // この span は list の内部配列の参照ではなくなっている
Console.WriteLine(string.Join(", ", list));
// 0, 1, 2, 3, 4, 5, 6, 7, 8, -1, 100

Dictionary<TKey, TValue>

GetValueRefOrAddDefault

導入時期: .NET 6

Dictionary<TKey, TValue>から参照を取り出します。存在しないキーを指定した場合は default で初期化されます。

メソッドの戻り値で値への参照を返し、キーが存在するかを out 引数で返します。

値の参照を返す都合で、Dictionary<TKey, TValue>.TryGetValue ではキーが存在するかをメソッドの戻り値で返し、値は out 引数で返す仕様と異なるのに注意。

こちらも値を追加する前後で使ったりすると危ういので、一時的な用途に限定して使いましょう。

using System.Runtime.InteropServices;

var dic = new Dictionary<string, int> {
    { "foo", 1 },
};
bool exists;
ref var foo = ref CollectionsMarshal.GetValueRefOrAddDefault(dic, "foo", out exists);
System.Diagnostics.Debug.Assert(exists);
ref var bar = ref CollectionsMarshal.GetValueRefOrAddDefault(dic, "bar", out exists);
System.Diagnostics.Debug.Assert(!exists);

foo++;
bar++;
Console.WriteLine(string.Join(", ", dic));
// [foo, 2], [bar, 1]

dic.Add("baz", -1);
foo++;
Console.WriteLine(dic["foo"]);
// 3

dic.Add("foobar", -1); // ここで再確保される
foo++; // foo は dic の内部の参照ではない
Console.WriteLine(dic["foo"]);
// 3

GetValueRefOrNullRef

導入時期: .NET 6

Dictionary<TKey, TValue>から参照を取り出します。存在しないキーを指定した場合は null 参照になります。

null 参照に触れると例外が投げられるので、Unsafe.IsNullRef を使ってチェックしてあげましょう。

using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

var dic = new Dictionary<string, int> {
    { "foo", 1 },
};
ref var foo = ref CollectionsMarshal.GetValueRefOrNullRef(dic, "foo");
System.Diagnostics.Debug.Assert(!Unsafe.IsNullRef(ref foo));
ref var bar = ref CollectionsMarshal.GetValueRefOrNullRef(dic, "bar");
System.Diagnostics.Debug.Assert(Unsafe.IsNullRef(ref bar));

foo++;
Console.WriteLine(string.Join(", ", dic));
// [foo, 2]

try
{
    bar++;
}
catch (NullReferenceException e)
{
    Console.WriteLine(e.Message);
    // Object reference not set to an instance of an object.
}
GitHubで編集を提案

Discussion