readonly structの基本

文法 難易度: ★★☆☆☆(初級)

前提知識

先にこちらを読んでおくと、この記事がすらすら読めます。

readonly structは、インスタンスを生成した後はすべてのフィールドとプロパティが変更できないことを、コンパイラが保証するstructだ。

なぜ必要か

structは値型で、代入や引数渡しのたびに値がコピーされる(「構造体(struct)の基本」の通り)。ただしstruct自体は既定では可変で、プロパティやフィールドを自由に書き換えられる。コピーされる性質と可変な中身が組み合わさると、どのコピーに変更が反映されたのか分からなくなる事故が起きやすい。readonly structは「そもそも書き換えられない」という制約を型に付け、この事故を未然に防ぐ。

定義してみる

var point = new Point(1, 2);
Console.WriteLine($"X={point.X}, Y={point.Y}");

public readonly struct Point
{
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    public int X { get; }
    public int Y { get; }
}
X=1, Y=2

structreadonlyを付けると、XYのようなプロパティはすべてget専用にする必要があり、値はコンストラクタで一度設定した後は書き換えられない。通常のstructならpublic int X { get; set; }のようにsetを書けるが、readonly structの中で同じことをすると次のようにコンパイルエラーになる。

// NG: readonly structにsetプロパティを混在させる
public readonly struct Point
{
    public int X { get; set; }
    public int Y { get; }
}
error CS8341: 読み取り専用の構造体に含まれる自動実装インスタンスのプロパティは、読み取り専用である必要があります。

最低限の理解

⚠️ 可変なstructをinで渡しても変更が反映されない

readonlyを付けていない可変なstructinで渡し、その中で状態を変えるメソッドを呼び出しても、コンパイルエラーにはならない。だが実際に変更されるのは呼び出す直前に作られた防御的コピーで、呼び出し元にも呼び出したメソッドの中の変数にも反映されない。

// NG: 可変なCounterをinで渡し、Incrementで変更したつもりになる
void ShowAfterIncrement(in Counter counter)
{
    counter.Increment();
    Console.WriteLine(counter.Value);
}

var counter = new Counter { Value = 0 };
ShowAfterIncrement(in counter);

public struct Counter
{
    public int Value;
    public void Increment() => Value++;
}
0
// OK: 状態を変えず、新しい値を作って返す
var counter = new Counter(0);
counter = counter.Increment();
Console.WriteLine(counter.Value);

public readonly struct Counter
{
    public Counter(int value) => Value = value;
    public int Value { get; }
    public Counter Increment() => new Counter(Value + 1);
}
1

課題

自分が扱う値(金額、日時範囲、座標など)を1つ選び、readonly structとして定義してみる。コンストラクタで値を設定し、inパラメータを取るメソッドに渡して、防御的コピーが作られないことを確認する。

次に読む