はじめに
UnsafeAccessor
を活用して List<T>
の内部配列を取れないものかと思ったのですが、ジェネリックな型を含む場合は使えないようでした。実行時 System.BadImageFormatException
(Invalid usage of UnsafeAccessorAttribute.) になります。
追記: ↓ の方法でやりたいことはできるようです。
List<int> list = [1, 2, 3];
var span = System.Runtime.InteropServices.CollectionsMarshal.AsSpan(list);
UnsafeAccessor とは
- ドキュメント https://learn.microsoft.com/ja-jp/dotnet/api/system.runtime.compilerservices.unsafeaccessorattribute?view=net-8.0
- .NET 8.0 で追加された新機能
- リフレクションなしでプライベートメンバにアクセスできる
- AoT でリフレクションを避けたい需要から追加された機能
using System.Runtime.CompilerServices;
class TestClass
{
private int _value;
public int Value { get => this._value; set => this._value = value; }
}
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_value")]
extern static ref int _value(TestClass value);
var test = new TestClass();
_value(test) = 1;
List<T>
の内部配列を取りたい
List<T>
はよく使う型なのですが、Span<T>
を取りたいときに配列のコピー(ToArray()
)を作らないといけないもどかしさがあります。内部配列を取得できればこれを省略できそうです。
extern
メソッドでは、ジェネリック型を指定するとうまくいかないようです。
それもそのはずで、ジェネリック型は実行時に型を決定しますが、extern
はコンパイル時に型を決定します(うろ覚え)。
ジェネリック型でない、具体的な型(例: int
)を与えるとうまくいきます。
class A<T>
{
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")]
internal static extern ref T[] _items(List<T> list);
}
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")]
internal static extern ref int[] _items(List<int> list);
var list = new List<int>();
A<int>._items(list); // こちらは例外
_items(list); // こちらはいける
おわりに
List<T>
の内部配列は取れないこともないですが、型ごとに取得メソッドを定義する必要があり、あまり実用的ではなさそうです。
おまけ UnsafeAccessor
のパフォーマンス比較
テストコード
using System.Linq.Expressions;
using System.Runtime.CompilerServices;
class TestClass
{
private int _value;
public int Value { get => this._value; set => this._value = value; }
}
[UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_value")]
extern static ref int _value(TestClass value);
static Action AssignProperty()
{
var test = new TestClass();
return () => test.Value++;
}
static Action AssignUnsafeAccessor()
{
var test = new TestClass();
return () => _value(test)++;
}
static Action AssignExpression()
{
var test = new TestClass();
var info = test.GetType().GetField("_value", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
Func<TestClass, int> getter;
{
var p = Expression.Parameter(typeof(TestClass));
getter = Expression.Lambda<Func<TestClass, int>>(Expression.Field(p, info), p).Compile();
}
Action<TestClass, int> setter;
{
var p1 = Expression.Parameter(typeof(TestClass));
var p2 = Expression.Parameter(typeof(int));
setter = Expression.Lambda<Action<TestClass, int>>(Expression.Assign(Expression.Field(p1, info), p2), p1, p2).Compile();
}
return () => setter(test, getter(test) + 1);
}
static Action AssignReflection()
{
var test = new TestClass();
var info = test.GetType().GetField("_value", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)!;
return () =>
{
int value = (int)info.GetValue(test)!;
info.SetValue(test, value + 1);
};
}
Test | Score | % | CG0 |
---|---|---|---|
AssignProperty | 2,566,308 | 100.0% | 0 |
AssignUnsafeAccessor | 2,557,564 | 99.7% | 0 |
AssignExpression | 2,578,716 | 100.5% | 0 |
AssignReflection | 857,693 | 33.4% | 4 |
-
UnsafeAccessor
はプロパティと遜色ないパフォーマンスです - 式木もプロパティと遜色ないパフォーマンスです。これは意外でした。初期化処理分を計測していないため、実際はもう少しパフォーマンスは落ちると思います
- リフレクションはややパフォーマンスは落ちますが、昔に比べると一桁くらい良くなってる気がします。BOX 化の影響でガベージが発生します
実行環境: Windows11 x64 .NET Runtime 8.0.0
Score は高いほどパフォーマンスがよいです。
GC0 はガベージコレクション回数を表します(少ないほどパフォーマンスがよい)。