はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】ref構造体(ref struct)の特徴・制約について

はじめに

今回はref構造体(ref struct)について取り上げたいと思います。

ref 修飾子は、構造体型の宣言内で使用できます。 ref struct 型のインスタンスはスタック上に割り当てられます。マネージド ヒープにエスケープすることはできません。

learn.microsoft.com

概要

ref structの最も覚えるべき特徴はインスタンスをスタック上にしか置けないというものです。

// インスタンスをStack上にしか置くことができない
public ref struct RefStruct
{
    public Span<int> Span;
    public int X;
}

public class SampleClass
{
    // SampleClassをインスタンス化するとヒープに置かれるので、以下の書き方はNG
    // public RefStruct _refStruct;
}


それを実現するために具体的には以下のような制約があります。

  • ref struct を配列の要素型にすることはできません。
  • ref struct をクラスまたは非 ref struct のフィールドの宣言型にすることはできません。
  • ref struct ではインターフェイスを実装できません。
  • ref struct を System.ValueType または System.Object にボックス化することはできません。
  • ref struct を型引数にすることはできません。
  • ref struct 変数をラムダ式またはローカル関数でキャプチャすることはできません。
  • ref struct 変数を async メソッド内で使用することはできません。 ただし、同期メソッドでは ref struct 変数を使用できます (Task または Task を返すメソッドなど)。
  • ref struct 変数を反復子内で使用することはできません。
// NGサンプル
public class Program
{
    private static void Main(string[] args)
    {
        // 1. 配列の要素型にできないのでNG
        // var array = new RefStruct[];
        
        // 4. System.ValueTypeまたはSystem.Objectにボックス化することができないのでNG
        // object obj = new RefStruct();

        // 5. 型引数にすることはできないのでNG
        // var sample = new SampleClass<RefStruct>();
        
        // 6. ラムダ式やローカル関数でキャプチャすることはできないのでNG
        var refStruct = new RefStruct();
        var x = () =>
        {
            // var x = refStruct.X;
        };
        
        foreach (var item in EnumerateNumber())
        {
            Console.WriteLine(item);
        }
    }
    
    // 7. asyncメソッド内で使用することはできないのでNG
    private async void HogeAsync()
    {
        // var refStruct = new RefStruct();
    }
    
    // 8. 反復子内で使用することはできないのでNG
    private static IEnumerable<int> EnumerateNumber()
    {
        // Span<int>もref構造体
        Span<int> x = stackalloc int[2];

        x[0] = 1;
        yield return x[0];

        x[1] = 10;
        yield return x[1];
    }
}

// インスタンスをStack上にしか置くことができない
// 3. インターフェイスを実装できないのでNG
public ref struct RefStruct // : IDisposable
{
    public Span<int> Span;
    public int X;
}

public struct SampleStruct
{
    // 2. クラスまたは非 ref structのフィールドの宣言型にできないのでNG
    // public RefStruct RefStruct;
}

public class SampleClass<T> { }

読み取り専用にする

readonly ref structと書くことで読み取り専用にすることができます。

public class Program
{
    private static void Main(string[] args)
    {
        var refStruct = new RefStruct();

        // readonlyでないので値を更新できる
        refStruct.X = 10;

        var span = Enumerable.Range(0, 10).ToArray().AsSpan();
        var readonlyRefStruct = new ReadonlyRefStruct(span, 10);

        // readonlyなのでNG
        // readonlyRefStruct.X = 10;
        
        // ちなみにこれは大丈夫 (Spanは参照と長さを持つ構造体)
        Console.WriteLine(readonlyRefStruct.Span[0]);     // 0
        span[0] = 100;
        Console.WriteLine(readonlyRefStruct.Span[0]);     // 100
    }
}

// インスタンスをStack上にしか置くことができない
public ref struct RefStruct
{
    public Span<int> Span;
    public int X;
}

// 読み取り専用のref構造体
public readonly ref struct ReadonlyRefStruct
{
    public readonly Span<int> Span;
    public readonly int X;

    public ReadonlyRefStruct(Span<int> span, int x)
    {
        Span = span;
        X = x;
    }
}

補足

Span<T>ref構造体です。

public readonly ref struct Span<T>
{
    /// <summary>A byref or a native ptr.</summary>
    internal readonly ref T _reference;
    /// <summary>The number of elements this Span contains.</summary>
    private readonly int _length;

    /// <summary>
    /// Creates a new span over the entirety of the target array.
    /// </summary>
    /// <param name="array">The target array.</param>
    /// <remarks>Returns default when <paramref name="array"/> is null.</remarks>
    /// <exception cref="System.ArrayTypeMismatchException">Thrown when <paramref name="array"/> is covariant and array's type is not exactly T[].</exception>
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Span(T[]? array)
    {
        if (array == null)
        {
            this = default;
            return; // returns default
        }
        if (!typeof(T).IsValueType && array.GetType() != typeof(T[]))
            ThrowHelper.ThrowArrayTypeMismatchException();

        _reference = ref MemoryMarshal.GetArrayDataReference(array);
        _length = array.Length;
    }

// 以下省略...