koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

List<T>の内部の配列の参照を取得する

はじめに

C#List<T> はいわゆる可変長配列のことであり,内部的には配列を持っている. 要素追加の度に残り容量を確認し,不足するようであればより大きな配列を確保し,そこに既存の要素をコピーした上で新しい要素を入れるという実装になっている.

List<T> のインデックスアクセサ自体はとても単純なものであり,おそらくインライン展開されるため,速度面で気になることはほぼない.

しかし,P/Invoke等でどうしても List<T> の内部の配列が欲しい場合もある. 本記事では List<T> の内部の配列の参照を得る方法を紹介する.

リフレクション

一番愚直にリフレクションを用いる方法である. 当然のことだが遅いため,何度も用いる場合には向かない.

using System;
using System.Collections.Generic;
using System.Reflection;


/// <summary>
/// Provides some utility methods of <see cref="List{T}"/>.
/// </summary>
public static class ListUtils
{
    /// <summary>
    /// Get internal array of <see cref="List{T}"/>.
    /// </summary>
    public static T[] GetArray<T>(List<T> list)
    {
        return (T[])(typeof(List<T>).GetField(
                "_items",
                BindingFlags.GetField
                    | BindingFlags.NonPublic
                    | BindingFlags.Instance)
                ?? throw new ArgumentException("FieldInfo not found: System.Collections.Generic.List<T>._items"))
            .GetValue(list);
    }
}

List<T> の内部の配列のメンバ名は _items であり,シリアライズのこともあるため,メンバ名変更は禁止というコメントが見られたため,近い将来で名前変更されることはまずないと考えていいと思われる.

IL生成

ILを生成し,デリゲートとしてキャッシュすることで,リフレクションの高速化を図る. 2回目以降はリフレクションを用いるより速くなるはずで,何度も List<T> の内部の配列を得たい場合はこの方法が良い.

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;



/// <summary>
/// Provides some utility methods of <see cref="List{T}"/>.
/// </summary>
public static class ListUtils
{
    /// <summary>
    /// Get internal array of <see cref="List{T}"/>.
    /// </summary>
    public static T[] GetArray<T>(List<T> list)
    {
        return ListUtils<T>.GetArray(list);
    }
}


/// <summary>
/// Provides some utility methods of <see cref="List{T}"/>.
/// </summary>
public static class ListUtils<T>
{
    /// <summary>
    /// Cache of delegate of <see cref="CreateGetArrayFunc"/>.
    /// </summary>
    private static Func<List<T>, T[]>? _getArray;

    /// <summary>
    /// Get internal array of <see cref="List{T}"/>.
    /// </summary>
    /// <param name="list">Target list.</param>
    /// <returns>Internal array of <see cref="List{T}"/>.</returns>
    public static T[] GetArray(List<T> list)
    {
        return (_getArray ??= CreateGetArrayFunc())(list);
    }

    /// <summary>
    /// Create method which gets internal array of <see cref="List{T}"/>.
    /// </summary>
    /// <typeparam name="T">Element type of <see cref="List{T}"/>.</typeparam>
    private static Func<List<T>, T[]> CreateGetArrayFunc()
    {
        var dynMethod = new DynamicMethod(
            "GetListArray",
            typeof(T[]),
            new [] { typeof(List<T>) },
            true);

        var ilGen = dynMethod.GetILGenerator();
        ilGen.Emit(OpCodes.Ldarg_0);
        ilGen.Emit(
            OpCodes.Ldfld,
            typeof(List<T>).GetField(
                "_items",
                BindingFlags.GetField
                    | BindingFlags.NonPublic
                    | BindingFlags.Instance)
                ?? throw new ArgumentException("FieldInfo not found: System.Collections.Generic.List<T>._items"));
        ilGen.Emit(OpCodes.Ret);

        return (Func<List<T>, T[]>)dynMethod.CreateDelegate(typeof(Func<List<T>, T[]>));
    }
}

List<T> という型引数を持つクラスを対象にするので,Static Type Cachingという手法を適用している. コンパイル時に型が解決するため, Type 型をキー,デリゲートを値にした Dictionary を用いるよりも高速である.

ジェネリック版の ListUtils型推論により型引数の指定を省略するためだけにある(呼び出し側で,例えば ListUtils.GetArray<int>(list) ではなく, ListUtils.GetArray(list) と書くだけでよい). ジェネリック版のみだと, ListUtils<int>.GetArray(list) のように書かなければならなくなり,面倒である.

式木

単にフィールドにアクセスするだけなので,IL版でも十分に保守性は高い(3命令しかないので)が,どうしても式木でやりたい場合は下記のようになる. 最終的にはIL生成していると思うが,生成までにやっていることは多いため,IL版より初回呼び出しは時間がかかるのではないだろうか?

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;


/// <summary>
/// Provides some utility methods of <see cref="List{T}"/>.
/// </summary>
public static class ListUtils
{
    /// <summary>
    /// Get internal array of <see cref="List{T}"/>.
    /// </summary>
    public static T[] GetArray<T>(List<T> list)
    {
        return ListUtils<T>.GetArray(list);
    }
}


/// <summary>
/// Provides some utility methods of <see cref="List{T}"/>.
/// </summary>
public static class ListUtils<T>
{
    /// <summary>
    /// Cache of delegate of <see cref="CreateGetArrayFunc"/>.
    /// </summary>
    private static Func<List<T>, T[]>? _getArray;

    /// <summary>
    /// Get internal array of <see cref="List{T}"/>.
    /// </summary>
    /// <param name="list">Target list.</param>
    /// <returns>Internal array of <see cref="List{T}"/>.</returns>
    public static T[] GetArray(List<T> list)
    {
        return (_getArray ??= CreateGetArrayFunc())(list);
    }

    /// <summary>
    /// Create method which gets internal array of <see cref="List{T}"/>.
    /// </summary>
    /// <typeparam name="T">Element type of <see cref="List{T}"/>.</typeparam>
    private static Func<List<T>, T[]> CreateGetArrayFunc()
    {
        // Arguments.
        var pList = Expression.Parameter(typeof(List<T>), "list");

        return Expression.Lambda<Func<List<T>, T[]>>(
            Expression.Field(
                pList,
                typeof(List<T>).GetField(
                    "_items",
                    BindingFlags.GetField
                        | BindingFlags.NonPublic
                        | BindingFlags.Instance)
                    ?? throw new ArgumentException("FieldInfo not found: System.Collections.Generic.List<T>._items")),
            "GetListArray",
            new []
            {
                pList
            }).Compile();
    }
}