potisanのプログラミングメモ

趣味のプログラマーがプログラミング関係で気になったことや調べたことをいつでも忘れられるようにメモするブログです。はてなブログ無料版なので記事の上の方はたぶん広告です。記事中にも広告挿入されるみたいです。

C# 絶対サイズ指定の構造体は全体コピーされる

C# 9.0ではStructLayout属性のSize引数を指定した構造体は通常のコピーでも全部(Sizeバイト分)コピーされます。P/Invoke以外ではメンバー変数のカバー範囲だけコピーされるかと思っていましたが、カバー範囲外もコピーされていました。Win32 APIVARIANTPROPVARIANTのような詳細な定義が面倒な構造体を使う場合は便利な仕様です。

模式図:C# 9.0では構造体の■も□もコピーされる(コピー1でありコピー2ではない)。
○コピー1:■■■□□→■■■□□
×コピー2:■■■□□→■■■
→:代入によるコピー操作
■:構造体のメンバー変数のカバー範囲
□メンバー変数のカバー範囲外

以下のコードで上記を確認できます。StructSize5は先頭3バイトだけをメンバー変数に持つ絶対サイズ5バイト(StructLayout.Size == 5)の構造体です。コピー前の構造体で5バイト目に書き込んだ値がコピー後の構造体でも保持されていたことから、構造体の絶対サイズ分(全体)がコピーされることが分かります。

トップレベステートメント&readonly構造体(C# 9) サンプルはreadonly structですが、structとしても挙動は変わりません。

using System;
using System.Runtime.InteropServices;

var struct1 = new StructSize5(new byte[] { 0, 1, 2, 3, 4 });
// 値セマンティクスによりインスタンスがコピーされる。
var struct2 = struct1;
var copied = struct2.ReadBytes()[4] == 4;
Console.WriteLine(
    "メンバー変数の範囲外はコピーされま{0}",
    copied ? "した。" : "せんでした。");

[StructLayout(LayoutKind.Sequential, Size = 5)]
readonly struct StructSize5
{
    // 確認用の先頭3バイト
    public readonly byte Byte0;
    public readonly byte Byte1;
    public readonly byte Byte2;

    public StructSize5(byte[] bytes)
    {
        (Byte0, Byte1, Byte2) = (0, 0, 0);
        if (bytes.Length > Marshal.SizeOf(this))
            throw new ArgumentOutOfRangeException(nameof(bytes));
        NativeMethods.RtlCopyMemory(ref this, bytes, (uint)bytes.Length);
    }

    public byte[] ReadBytes()
    {
        var bytes = new byte[Marshal.SizeOf(this)];
        NativeMethods.RtlCopyMemory(bytes, this, (uint)bytes.Length);
        return bytes;
    }

    private static class NativeMethods
    {
        [DllImport("ntdll.dll")]
        public static extern void RtlCopyMemory([Out] byte[] Destination, in StructSize5 Source, uint Length);
        [DllImport("ntdll.dll")]
        public static extern void RtlCopyMemory(ref StructSize5 Destination, [In] byte[] Source, uint Length);
    }
}

Main関数(C# 8)

using System;
using System.Runtime.InteropServices;

static class Program
{
    static void Main()
    {
        var struct1 = new StructSize5(new byte[] { 0, 1, 2, 3, 4 });
        // 値セマンティクスによりインスタンスがコピーされる。
        var struct2 = struct1;
        var copied = struct2.ReadBytes()[4] == 4;
        Console.WriteLine(
            "メンバー変数の範囲外はコピーされま{0}",
            copied ? "した。" : "せんでした。");
    }

    [StructLayout(LayoutKind.Sequential, Size = 5)]
    struct StructSize5
    {
        // 確認用の先頭3バイト
        public readonly byte Byte0;
        public readonly byte Byte1;
        public readonly byte Byte2;

        public StructSize5(byte[] bytes)
        {
            (Byte0, Byte1, Byte2) = (0, 0, 0);
            if (bytes.Length > Marshal.SizeOf(this))
                throw new ArgumentOutOfRangeException(nameof(bytes));
            NativeMethods.RtlCopyMemory(ref this, bytes, (uint)bytes.Length);
        }

        public byte[] ReadBytes()
        {
            var bytes = new byte[Marshal.SizeOf(this)];
            NativeMethods.RtlCopyMemory(bytes, this, (uint)bytes.Length);
            return bytes;
        }

        private static class NativeMethods
        {
            [DllImport("ntdll.dll")]
            public static extern void RtlCopyMemory([Out] byte[] Destination, in StructSize5 Source, uint Length);
            [DllImport("ntdll.dll")]
            public static extern void RtlCopyMemory(ref StructSize5 Destination, [In] byte[] Source, uint Length);
        }
    }
}

参考