C# readonly structとは?使い方・メリット・注意点をサンプルコードで解説

はじめに

C#で値型を設計していると、structにすべきか、classにすべきか、さらにreadonly structにすべきかで迷うことがあります。特に、座標、金額、ID、数量、範囲、サイズなどの「小さな値」を表す型では、値の変更を防ぎながら効率よく扱える設計が重要です。

readonly structは、C#のstructreadonly修飾子を付けることで、その構造体が不変であることを表現する機能です。C# 7.2で導入され、inパラメータやref readonlyなどとあわせて、値型を安全かつ効率的に扱うための仕組みとして追加されました。Microsoft Learn

この記事では、C#のreadonly structとは何か、通常のstructclassとの違い、基本的な書き方、メリット、注意点、実務での使いどころまで、サンプルコードを使ってわかりやすく解説します。

1. C# readonly structとは?基本概念をわかりやすく解説

1-1. readonly structの定義

readonly structとは、インスタンスの状態を変更しないことをコンパイラに伝えるための構造体です。

基本的には、次のように書きます。

C#
public readonly struct Point
{
public readonly int X;
public readonly int Y;

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

readonly structを使うと、「この構造体は作成後に値を変更しない」という意図を型定義の時点で明確にできます。C#の仕様上、readonly structの各インスタンスフィールドはreadonlyである必要があり、インスタンスメンバーから状態を書き換えることも制限されます。Microsoft Learn

1-2. 通常のstructとの違い

通常のstructは、値型でありながら内部状態を変更できます。

C#
public struct Counter
{
public int Value;

public void Increment()
{
Value++;
}
}

この例では、IncrementメソッドによってValueを変更できます。

一方、readonly structでは次のような変更はできません。

C#
public readonly struct Counter
{
public readonly int Value;

public Counter(int value)
{
Value = value;
}

// コンパイルエラー
// public void Increment()
// {
// Value++;
// }
}

つまり、通常のstructは「値型だが可変にできる」のに対し、readonly structは「値型かつ不変であることを強制する」ための仕組みです。

1-3. classとの違い

classは参照型、structreadonly structは値型です。

C#
public class UserClass
{
public string Name { get; set; } = "";
}

public readonly struct UserId
{
public int Value { get; }

public UserId(int value)
{
Value = value;
}
}

classの変数にはオブジェクトへの参照が入ります。別の変数へ代入しても、基本的には同じオブジェクトを参照します。

一方、structreadonly structは値そのものとして扱われます。代入や引数渡しではコピーが発生することがあります。そのため、小さな値を表す場合に向いています。

readonly structは、classのように参照を共有するのではなく、「値そのもの」を安全に扱いたい場合に便利です。

1-4. readonlyフィールド・readonlyプロパティとの関係

readonly structreadonlyフィールドは似ていますが、意味する範囲が違います。

C#
public struct Sample
{
public readonly int Value;

public Sample(int value)
{
Value = value;
}
}

この場合、Valueフィールドは読み取り専用ですが、構造体全体が不変であるとは限りません。別の可変フィールドを追加できるからです。

C#
public struct Sample
{
public readonly int Value;
public int MutableValue;

public Sample(int value, int mutableValue)
{
Value = value;
MutableValue = mutableValue;
}
}

一方、readonly structでは、構造体全体に対して「インスタンス状態を変更しない」という制約がかかります。

C#
public readonly struct Sample
{
public int Value { get; }

public Sample(int value)
{
Value = value;
}
}

実務では、readonly structではフィールドを直接公開するより、getのみのプロパティを使うほうが読みやすく、安全な設計にしやすいです。

1-5. readonly structが導入された背景

readonly structが導入された背景には、値型の安全性とパフォーマンスがあります。

C#では、structは値型としてコピーされることがあります。特に、読み取り専用の文脈で可変なstructのメソッドを呼び出すと、意図しない変更を防ぐために防御的コピーが発生する場合があります。

readonly structを使うと、その型が状態を変更しないことをコンパイラに伝えられます。これにより、不変性を明確にしつつ、余計なコピーを減らせる可能性があります。C#のバージョン履歴でも、readonly structは構造体が不変であり、メンバーメソッドへinパラメータとして渡すべきことを示す機能として説明されています。Microsoft Learn

2. readonly structの基本的な書き方

2-1. readonly structの構文

readonly structの基本構文は次のとおりです。

C#
public readonly struct TypeName
{
public int Value { get; }

public TypeName(int value)
{
Value = value;
}
}

通常のstructの前にreadonlyを付けるだけです。

C#
public readonly struct ProductId
{
public int Value { get; }

public ProductId(int value)
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}

Value = value;
}
}

このように、IDやコードのような小さな値を型として表現する場合に使いやすいです。

2-2. フィールドをreadonlyにする必要性

readonly structでインスタンスフィールドを定義する場合、そのフィールドもreadonlyにする必要があります。

C#
public readonly struct Point
{
public readonly int X;
public readonly int Y;

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

次のように、readonlyではないフィールドを定義するとコンパイルエラーになります。

C#
public readonly struct BadPoint
{
// コンパイルエラー
// public int X;

public readonly int Y;

public BadPoint(int x, int y)
{
// X = x;
Y = y;
}
}

readonly structは構造体全体の不変性を表すため、内部に変更可能なインスタンスフィールドを持てません。

2-3. プロパティのみで定義する例

実務では、フィールドを直接公開するより、プロパティで定義するほうが一般的です。

C#
public readonly struct Size
{
public int Width { get; }
public int Height { get; }

public Size(int width, int height)
{
if (width < 0)
{
throw new ArgumentOutOfRangeException(nameof(width));
}

if (height < 0)
{
throw new ArgumentOutOfRangeException(nameof(height));
}

Width = width;
Height = height;
}
}

getのみの自動プロパティにすると、外部から値を変更できません。

C#
var size = new Size(100, 50);

// コンパイルエラー
// size.Width = 200;

readonly structでは、このように「作成時に値を決め、その後は変更しない」という設計が基本になります。

2-4. コンストラクタの書き方

readonly structでは、コンストラクタで必要な値をすべて初期化します。

C#
public readonly struct Temperature
{
public double Celsius { get; }

public Temperature(double celsius)
{
Celsius = celsius;
}
}

バリデーションを入れることもできます。

C#
public readonly struct Quantity
{
public int Value { get; }

public Quantity(int value)
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value), "数量は0以上である必要があります。");
}

Value = value;
}
}

値オブジェクトとして使う場合は、コンストラクタで不正な値を防ぐことが重要です。

2-5. メソッドを含むreadonly structの例

readonly structにはメソッドも定義できます。ただし、メソッド内でインスタンスの状態を変更することはできません。

C#
public readonly struct RectangleSize
{
public int Width { get; }
public int Height { get; }

public RectangleSize(int width, int height)
{
Width = width;
Height = height;
}

public int GetArea()
{
return Width * Height;
}

public bool IsSquare()
{
return Width == Height;
}
}

この例では、GetAreaIsSquareは値を計算して返すだけで、WidthHeightを変更しません。readonly structのメソッドは、このように「状態を変えずに結果を返す」設計に向いています。

3. readonly structの使い方をサンプルコードで解説

3-1. 座標を表すreadonly structのサンプル

座標は、readonly structで表現しやすい代表例です。

C#
public readonly struct Point2D
{
public double X { get; }
public double Y { get; }

public Point2D(double x, double y)
{
X = x;
Y = y;
}

public double DistanceFromOrigin()
{
return Math.Sqrt(X * X + Y * Y);
}
}

使い方は次のとおりです。

C#
var point = new Point2D(3, 4);

Console.WriteLine(point.X);
Console.WriteLine(point.Y);
Console.WriteLine(point.DistanceFromOrigin()); // 5

座標は「XとYの組み合わせそのもの」が意味を持つ値です。作成後にXだけを変更するより、新しい座標を作るほうが自然な場合が多いため、readonly structと相性が良いです。

3-2. 金額や数量を表す値オブジェクトのサンプル

金額もreadonly structで表現しやすい値です。

C#
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }

public Money(decimal amount, string currency)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount), "金額は0以上である必要があります。");
}

if (string.IsNullOrWhiteSpace(currency))
{
throw new ArgumentException("通貨コードは必須です。", nameof(currency));
}

Amount = amount;
Currency = currency;
}

public Money Add(Money other)
{
if (Currency != other.Currency)
{
throw new InvalidOperationException("異なる通貨の金額は加算できません。");
}

return new Money(Amount + other.Amount, Currency);
}
}

使い方は次のとおりです。

C#
var price = new Money(1200m, "JPY");
var tax = new Money(120m, "JPY");

var total = price.Add(tax);

Console.WriteLine($"{total.Amount} {total.Currency}");

Addメソッドでは現在のMoneyを変更せず、新しいMoneyを返しています。これがreadonly structらしい設計です。

3-3. メソッドで値を計算するサンプル

readonly structは、値を保持するだけでなく、その値に関する計算をメソッドとして持たせることもできます。

C#
public readonly struct OrderLine
{
public Money UnitPrice { get; }
public int Quantity { get; }

public OrderLine(Money unitPrice, int quantity)
{
if (quantity <= 0)
{
throw new ArgumentOutOfRangeException(nameof(quantity));
}

UnitPrice = unitPrice;
Quantity = quantity;
}

public Money GetSubtotal()
{
return new Money(UnitPrice.Amount * Quantity, UnitPrice.Currency);
}
}

使い方は次のとおりです。

C#
var unitPrice = new Money(500m, "JPY");
var line = new OrderLine(unitPrice, 3);

var subtotal = line.GetSubtotal();

Console.WriteLine($"{subtotal.Amount} {subtotal.Currency}"); // 1500 JPY

値と、その値に関係する処理を同じ型にまとめることで、コードの見通しが良くなります。

3-4. in引数と組み合わせるサンプル

readonly structは、inパラメータと組み合わせることがあります。inは、引数を参照渡ししつつ、呼び出し先で変更できないようにする修飾子です。C# 7.2ではreadonly structとあわせてinパラメータも追加されました。Microsoft Learn+1

C#
public readonly struct Vector2D
{
public double X { get; }
public double Y { get; }

public Vector2D(double x, double y)
{
X = x;
Y = y;
}
}

public static class VectorCalculator
{
public static double Dot(in Vector2D a, in Vector2D b)
{
return a.X * b.X + a.Y * b.Y;
}
}

使い方は次のとおりです。

C#
var a = new Vector2D(1, 2);
var b = new Vector2D(3, 4);

var result = VectorCalculator.Dot(in a, in b);

Console.WriteLine(result); // 11

inを使うと、値型のコピーを避けられる可能性があります。ただし、小さなstructでは効果が小さい場合もあるため、パフォーマンス目的で使う場合は計測して判断することが大切です。

3-5. 実務で使いやすい設計例

実務では、プリミティブ型をそのまま使うより、意味のある型で包むと安全性が上がります。

たとえば、商品IDをintのまま扱うと、ユーザーIDや注文IDと取り違える可能性があります。

C#
public readonly struct ProductId
{
public int Value { get; }

public ProductId(int value)
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}

Value = value;
}

public override string ToString()
{
return Value.ToString();
}
}

利用例です。

C#
public class Product
{
public ProductId Id { get; }
public string Name { get; }

public Product(ProductId id, string name)
{
Id = id;
Name = name;
}
}

ProductIdを専用の型にすることで、単なるintよりも意味が明確になります。readonly structにすれば、IDの値が途中で変わらないことも保証しやすくなります。

4. readonly structを使うメリット

4-1. 不変な値型を表現できる

readonly structの最大のメリットは、不変な値型を表現できることです。

C#
public readonly struct EmailAddress
{
public string Value { get; }

public EmailAddress(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("メールアドレスは必須です。", nameof(value));
}

Value = value;
}
}

作成後に値を変えられないため、オブジェクトの状態を追いやすくなります。

不変な型は、バグの原因になりやすい「どこかで値が変わっていた」という問題を減らせます。

4-2. 意図しない値の変更を防げる

通常のstructでは、うっかり状態を変更するメソッドを追加できてしまいます。

C#
public struct MutablePoint
{
public int X { get; set; }
public int Y { get; set; }

public void MoveRight()
{
X++;
}
}

一方、readonly structではこのような変更ができません。

C#
public readonly struct ImmutablePoint
{
public int X { get; }
public int Y { get; }

public ImmutablePoint(int x, int y)
{
X = x;
Y = y;
}

public ImmutablePoint MoveRight()
{
return new ImmutablePoint(X + 1, Y);
}
}

値を変更したい場合は、既存の値を書き換えるのではなく、新しい値を返します。この設計により、処理の副作用を減らせます。

4-3. 防御的コピーを減らせる可能性がある

C#では、読み取り専用の文脈で可変なstructのメンバーを呼び出すと、コンパイラが防御的コピーを作ることがあります。これは、呼び出したメソッドが状態を変更する可能性があるためです。

readonly structであれば、型全体が状態を変更しないことを示せます。そのため、コンパイラが余計なコピーを避けやすくなるケースがあります。

C#
public readonly struct Measurement
{
public double Value { get; }

public Measurement(double value)
{
Value = value;
}

public double ToMeters()
{
return Value;
}
}

特に、頻繁に呼び出される小さな値型では、防御的コピーを減らせる可能性がある点がメリットになります。

4-4. パフォーマンス改善につながるケース

readonly structは、次のようなケースでパフォーマンス改善につながる可能性があります。

小さな値型を大量に扱う場合、inパラメータと組み合わせる場合、防御的コピーが問題になる場合、ライブラリやゲーム、数値計算などで値型のコピーコストを意識する場合です。

C#
public static double Calculate(in Vector2D value)
{
return Math.Sqrt(value.X * value.X + value.Y * value.Y);
}

ただし、readonly structにすれば必ず高速になるわけではありません。小さすぎるstructでは通常の値渡しのほうが単純で速い場合もあります。パフォーマンス目的で導入するなら、ベンチマークを取って判断することが重要です。

4-5. コードの安全性と可読性が高まる

readonly structを使うと、その型が不変であることが宣言から読み取れます。

C#
public readonly struct CustomerId
{
public int Value { get; }

public CustomerId(int value)
{
Value = value;
}
}

このコードを見た開発者は、CustomerIdが途中で変更されない値であると理解できます。

これは可読性の向上にもつながります。仕様やコメントで説明しなくても、型定義そのものが設計意図を表してくれるからです。

5. readonly structの注意点・デメリット

5-1. すべてのインスタンスフィールドをreadonlyにする必要がある

readonly structでは、すべてのインスタンスフィールドをreadonlyにする必要があります。

C#
public readonly struct Sample
{
public readonly int Value;

public Sample(int value)
{
Value = value;
}
}

次のようなコードはコンパイルエラーになります。

C#
public readonly struct BadSample
{
// コンパイルエラー
// public int Value;
}

フィールドを直接使う場合は、必ずreadonlyを付けましょう。

ただし、実務では次のようにgetのみのプロパティを使うほうが自然です。

C#
public readonly struct Sample
{
public int Value { get; }

public Sample(int value)
{
Value = value;
}
}

5-2. mutableな参照型フィールドを持つ場合の注意

readonly structであっても、参照型オブジェクトの中身まで自動的に不変になるわけではありません。

C#
public readonly struct Tags
{
public List<string> Values { get; }

public Tags(List<string> values)
{
Values = values;
}
}

この場合、Valuesプロパティ自体を別のリストに差し替えることはできません。しかし、リストの中身は変更できます。

C#
var list = new List<string> { "CSharp" };
var tags = new Tags(list);

tags.Values.Add("readonly struct");

これはreadonly structの不変性を壊す原因になります。

対策として、外部から受け取ったコレクションをコピーしたり、IReadOnlyList<T>や不変コレクションを使ったりします。

C#
public readonly struct SafeTags
{
public IReadOnlyList<string> Values { get; }

public SafeTags(IEnumerable<string> values)
{
Values = values.ToArray();
}
}

readonlyが保証するのは、あくまでフィールドやプロパティの参照先を変更しないことです。参照先オブジェクトの内部状態までは自動では守れません。

5-3. 大きすぎるstructは逆にパフォーマンスが悪くなる

structは値型であるため、代入や引数渡しでコピーされることがあります。

小さなstructなら問題になりにくいですが、大きなstructを頻繁にコピーすると、逆にパフォーマンスが悪くなる場合があります。

C#
public readonly struct LargeData
{
public long A { get; }
public long B { get; }
public long C { get; }
public long D { get; }
public long E { get; }
public long F { get; }

public LargeData(long a, long b, long c, long d, long e, long f)
{
A = a;
B = b;
C = c;
D = d;
E = e;
F = f;
}
}

このような大きめの構造体を多用する場合は、classのほうが適している可能性があります。

readonly structは「不変だから常に良い」のではなく、「小さく、値として扱いたいデータ」に向いています。

5-4. メソッド内で状態を変更できない

readonly structでは、メソッド内でインスタンスの状態を変更できません。

C#
public readonly struct Counter
{
public int Value { get; }

public Counter(int value)
{
Value = value;
}

// コンパイルエラー
// public void Increment()
// {
// Value++;
// }
}

値を変えたい場合は、新しいインスタンスを返します。

C#
public readonly struct Counter
{
public int Value { get; }

public Counter(int value)
{
Value = value;
}

public Counter Increment()
{
return new Counter(Value + 1);
}
}

可変状態を前提とする処理には、readonly structは向きません。

5-5. default値への対応に注意する

structは、コンストラクタを呼ばずにdefault値を作れる点に注意が必要です。

C#
Money money = default;

この場合、decimalAmount0stringCurrencynullになります。

C#
Console.WriteLine(money.Amount);   // 0
Console.WriteLine(money.Currency); // null

コンストラクタでCurrencyの必須チェックをしていても、defaultではそのチェックを通りません。

対策として、defaultでも安全に扱える設計にするか、使用時に検証します。

C#
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }

public bool IsValid => !string.IsNullOrWhiteSpace(Currency);

public Money(decimal amount, string currency)
{
if (amount < 0)
{
throw new ArgumentOutOfRangeException(nameof(amount));
}

if (string.IsNullOrWhiteSpace(currency))
{
throw new ArgumentException("通貨コードは必須です。", nameof(currency));
}

Amount = amount;
Currency = currency;
}
}

readonly structを値オブジェクトとして使う場合、default状態をどう扱うかは必ず考えておきましょう。

5-6. boxingが発生するケースに注意する

structは値型ですが、objectやインターフェース型として扱うとboxingが発生する場合があります。

C#
public readonly struct ProductId
{
public int Value { get; }

public ProductId(int value)
{
Value = value;
}
}

ProductId id = new ProductId(1);

// boxingが発生する
object obj = id;

また、インターフェースを通して呼び出す場合にもboxingが問題になることがあります。

C#
public interface IPrintable
{
string Print();
}

public readonly struct Code : IPrintable
{
public string Value { get; }

public Code(string value)
{
Value = value;
}

public string Print()
{
return Value;
}
}

IPrintable printable = new Code("A001"); // boxingが発生する可能性がある

パフォーマンスを重視する場合は、boxingが発生していないかも確認しましょう。

6. readonly structと関連機能の違い

6-1. readonly structとreadonly memberの違い

readonly structは、構造体全体を読み取り専用にする機能です。

一方、readonly memberは、通常のstructの中で特定のメンバーだけが状態を変更しないことを示す機能です。C#のreadonlyキーワードは、構造体型定義では構造体が不変であることを、インスタンスメンバー宣言ではそのメンバーが構造体の状態を変更しないことを意味します。Microsoft Learn

C#
public struct Point
{
public int X { get; set; }
public int Y { get; set; }

public readonly double DistanceFromOrigin()
{
return Math.Sqrt(X * X + Y * Y);
}

public void MoveRight()
{
X++;
}
}

この例では、DistanceFromOriginは状態を変更しませんが、MoveRightは状態を変更します。

型全体を不変にしたいならreadonly struct、一部のメンバーだけを読み取り専用にしたいならreadonly memberを使います。

6-2. readonly structとrecord structの違い

record structは、値の等価性やToStringなど、データ中心の型に便利な機能をコンパイラが自動生成してくれる構造体です。Microsoftのドキュメントでも、record structは値型を宣言する構文として説明されています。Microsoft Learn

C#
public record struct ProductCode(string Value);

record structは簡潔に書けますが、通常はプロパティが変更可能です。

C#
var code = new ProductCode("A001");
code.Value = "B002";

不変性を重視するなら、readonly record structを検討します。

6-3. readonly record structとの違い

readonly record structは、readonly structrecord structの特徴を組み合わせたものです。

C#
public readonly record struct ProductCode(string Value);

このように書くと、値型であり、不変性を持ち、さらに値の等価性やToStringなどのレコード機能も利用できます。公式ドキュメントでは、record structreadonly record structの両方を定義できると説明されています。Microsoft Learn

たとえば、次のように値の比較が自然にできます。

C#
var code1 = new ProductCode("A001");
var code2 = new ProductCode("A001");

Console.WriteLine(code1 == code2); // True

EqualsGetHashCodeを自分で書くのが面倒な場合は、readonly record structが便利です。

6-4. ref structとの違い

ref structは、スタック上でのみ扱う必要がある特殊な構造体です。代表例にはSpan<T>ReadOnlySpan<T>があります。ref structは通常のstructより制約が多く、配列要素や通常のクラスフィールドとして扱えないなどの制限があります。Microsoft Learn

C#
public ref struct BufferView
{
public Span<byte> Buffer;

public BufferView(Span<byte> buffer)
{
Buffer = buffer;
}
}

readonly structは「不変な値型」を表すためのものです。

ref structは「参照の安全性やスタック上での扱い」を制御するためのものです。

また、両方を組み合わせることもできます。

C#
public readonly ref struct ReadOnlyBufferView
{
public ReadOnlySpan<byte> Buffer { get; }

public ReadOnlyBufferView(ReadOnlySpan<byte> buffer)
{
Buffer = buffer;
}
}

ただし、ref structは制約が多いため、通常の値オブジェクトにはreadonly structreadonly record structを使うほうが扱いやすいです。

6-5. inパラメータとの関係

inパラメータは、引数を参照渡ししつつ、呼び出し先で変更できないようにする機能です。

C#
public static double Length(in Vector2D vector)
{
return Math.Sqrt(vector.X * vector.X + vector.Y * vector.Y);
}

readonly structinは相性が良いです。readonly structは変更されないことが明確で、inはコピーを避けながら読み取り専用で渡すことを表現できます。

ただし、inを使えば必ず速くなるわけではありません。小さな値型では値渡しのほうが単純な場合もあります。

7. readonly structを使うべきケース・使わないほうがよいケース

7-1. 小さく不変な値を表す場合

readonly structは、小さく不変な値を表す場合に向いています。

たとえば、次のような型です。

C#
public readonly struct UserId
{
public int Value { get; }

public UserId(int value)
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}

Value = value;
}
}

ID、座標、サイズ、金額、数量、割合、温度など、「値そのもの」に意味がある場合はreadonly structの候補になります。

7-2. 値オブジェクトを表現したい場合

ドメイン駆動設計などで使われる値オブジェクトも、readonly structと相性が良いことがあります。

C#
public readonly struct PostalCode
{
public string Value { get; }

public PostalCode(string value)
{
if (string.IsNullOrWhiteSpace(value))
{
throw new ArgumentException("郵便番号は必須です。", nameof(value));
}

Value = value;
}

public override string ToString()
{
return Value;
}
}

値オブジェクトは、同じ値であれば同じものとして扱う考え方です。readonly structにすると、作成後に値が変わらないことを型で表現できます。

ただし、値の等価性を重視する場合は、readonly record structも有力な選択肢です。

7-3. パフォーマンスを意識するライブラリで使う場合

ライブラリやフレームワークの内部で、小さな値型を大量に扱う場合にもreadonly structは役立つことがあります。

C#
public readonly struct Range
{
public int Start { get; }
public int Length { get; }

public Range(int start, int length)
{
Start = start;
Length = length;
}

public int End => Start + Length;
}

読み取り専用であることをコンパイラに伝えられるため、防御的コピーの削減やAPI設計の明確化につながります。

7-4. 可変状態が必要な場合は避ける

状態を頻繁に変更する必要がある場合、readonly structは向きません。

C#
public struct MutableCounter
{
public int Value { get; private set; }

public void Increment()
{
Value++;
}
}

このようなカウンターやバッファ、内部状態を更新する処理では、通常のstructclassのほうが自然です。

readonly structを無理に使うと、毎回新しいインスタンスを作る必要があり、かえってコードが読みにくくなることがあります。

7-5. 大きなデータ構造には向かない理由

大きなデータ構造をstructにすると、コピーコストが問題になる場合があります。

C#
public readonly struct ReportData
{
public string Title { get; }
public string Body { get; }
public string Author { get; }
public DateTime CreatedAt { get; }
public DateTime UpdatedAt { get; }
public string Category { get; }

public ReportData(
string title,
string body,
string author,
DateTime createdAt,
DateTime updatedAt,
string category)
{
Title = title;
Body = body;
Author = author;
CreatedAt = createdAt;
UpdatedAt = updatedAt;
Category = category;
}
}

このように多くの情報を持つ型は、classrecord classのほうが適している場合があります。

readonly structは、小さくまとまった値を表すために使うのが基本です。

8. readonly structのベストプラクティス

8-1. 小さくシンプルに設計する

readonly structは、小さくシンプルに設計しましょう。

良い例です。

C#
public readonly struct Percentage
{
public double Value { get; }

public Percentage(double value)
{
if (value < 0 || value > 100)
{
throw new ArgumentOutOfRangeException(nameof(value));
}

Value = value;
}
}

避けたい例です。

C#
public readonly struct LargeBusinessObject
{
public string Name { get; }
public string Description { get; }
public string Address { get; }
public string PhoneNumber { get; }
public string Email { get; }
public DateTime CreatedAt { get; }

public LargeBusinessObject(
string name,
string description,
string address,
string phoneNumber,
string email,
DateTime createdAt)
{
Name = name;
Description = description;
Address = address;
PhoneNumber = phoneNumber;
Email = email;
CreatedAt = createdAt;
}
}

大きく複雑な型は、値型ではなく参照型として設計したほうが扱いやすいことが多いです。

8-2. フィールドではなくプロパティ中心で定義する

C#では、公開フィールドよりプロパティを使うほうが一般的です。

C#
public readonly struct Score
{
public int Value { get; }

public Score(int value)
{
if (value < 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}

Value = value;
}
}

プロパティにしておくと、後からロジックを追加したり、APIの見た目を整えたりしやすくなります。

公開フィールドでもreadonly structは作れますが、実務ではgetのみのプロパティを中心に設計することをおすすめします。

8-3. 不変性を壊す参照型の扱いに注意する

readonly structの中に参照型を持つ場合は、参照先の変更に注意しましょう。

悪い例です。

C#
public readonly struct Options
{
public Dictionary<string, string> Values { get; }

public Options(Dictionary<string, string> values)
{
Values = values;
}
}

この設計では、外部から辞書の中身を変更できます。

改善例です。

C#
public readonly struct Options
{
public IReadOnlyDictionary<string, string> Values { get; }

public Options(IDictionary<string, string> values)
{
Values = new Dictionary<string, string>(values);
}
}

さらに厳密な不変性が必要なら、不変コレクションの利用も検討しましょう。

8-4. Equals・GetHashCodeを適切に実装する

readonly structを値オブジェクトとして使うなら、等価性の扱いが重要です。

C#
public readonly struct ProductId : IEquatable<ProductId>
{
public int Value { get; }

public ProductId(int value)
{
Value = value;
}

public bool Equals(ProductId other)
{
return Value == other.Value;
}

public override bool Equals(object? obj)
{
return obj is ProductId other && Equals(other);
}

public override int GetHashCode()
{
return Value.GetHashCode();
}

public static bool operator ==(ProductId left, ProductId right)
{
return left.Equals(right);
}

public static bool operator !=(ProductId left, ProductId right)
{
return !left.Equals(right);
}
}

DictionaryのキーやHashSetの要素として使う場合、EqualsGetHashCodeの実装は特に重要です。

8-5. record structの利用も検討する

等価性やToStringなどを自動生成したい場合は、readonly record structを使うと簡潔です。

C#
public readonly record struct ProductId(int Value);

この1行で、値の等価性やToStringなどが利用できます。

C#
var id1 = new ProductId(1);
var id2 = new ProductId(1);

Console.WriteLine(id1 == id2); // True
Console.WriteLine(id1); // ProductId { Value = 1 }

ただし、コンストラクタで細かいバリデーションを入れたい場合は、通常のreadonly structで明示的に書くほうがわかりやすい場合もあります。

8-6. パフォーマンス改善目的なら計測して判断する

readonly structはパフォーマンス改善に役立つ可能性がありますが、必ず速くなるわけではありません。

次のような観点で判断しましょう。

structのサイズは小さいか、頻繁にコピーされているか、inパラメータで効果があるか、boxingが発生していないか、防御的コピーが問題になっているか。

パフォーマンス改善を目的にする場合は、BenchmarkDotNetなどを使って計測するのがおすすめです。

設計上の不変性を表したいだけなら、まずは読みやすさと安全性を優先しましょう。

9. readonly structでよくあるエラーと対処法

9-1. フィールドがreadonlyでない場合のエラー

readonly structで最もよくあるエラーは、フィールドにreadonlyを付け忘れることです。

C#
public readonly struct BadPoint
{
// コンパイルエラー
// public int X;

public readonly int Y;

public BadPoint(int x, int y)
{
// X = x;
Y = y;
}
}

対処法は、フィールドにreadonlyを付けることです。

C#
public readonly struct GoodPoint
{
public readonly int X;
public readonly int Y;

public GoodPoint(int x, int y)
{
X = x;
Y = y;
}
}

または、プロパティに変更します。

C#
public readonly struct GoodPoint
{
public int X { get; }
public int Y { get; }

public GoodPoint(int x, int y)
{
X = x;
Y = y;
}
}

9-2. メソッド内で値を変更しようとした場合のエラー

readonly structでは、メソッド内でインスタンス状態を変更できません。

C#
public readonly struct BadCounter
{
public int Value { get; }

public BadCounter(int value)
{
Value = value;
}

// コンパイルエラー
// public void Increment()
// {
// Value++;
// }
}

対処法は、変更後の新しいインスタンスを返すことです。

C#
public readonly struct Counter
{
public int Value { get; }

public Counter(int value)
{
Value = value;
}

public Counter Increment()
{
return new Counter(Value + 1);
}
}

readonly structでは、「自分自身を変更する」のではなく、「変更後の値を新しく作る」と考えましょう。

9-3. コンストラクタで初期化漏れがある場合

readonly structのプロパティやフィールドは、コンストラクタで正しく初期化する必要があります。

C#
public readonly struct BadSize
{
public int Width { get; }
public int Height { get; }

// コンパイルエラー
// public BadSize(int width)
// {
// Width = width;
// }
}

Heightが初期化されていないためエラーになります。

正しい実装は次のとおりです。

C#
public readonly struct Size
{
public int Width { get; }
public int Height { get; }

public Size(int width, int height)
{
Width = width;
Height = height;
}
}

必要な値は、コンストラクタで漏れなく設定しましょう。

9-4. mutableなプロパティを定義してしまう例

readonly structでは、変更可能な自動プロパティを定義しようとすると問題になります。

C#
public readonly struct BadUserId
{
// 不変性に反する設計
// public int Value { get; set; }
}

setを持つプロパティは、値を変更できる設計になってしまいます。

対処法は、getのみのプロパティにすることです。

C#
public readonly struct UserId
{
public int Value { get; }

public UserId(int value)
{
Value = value;
}
}

初期化後に変更しない値として扱うなら、setは不要です。

9-5. エラーを避ける正しい実装例

readonly structの正しい実装例をまとめると、次のようになります。

C#
public readonly struct OrderId : IEquatable<OrderId>
{
public int Value { get; }

public OrderId(int value)
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}

Value = value;
}

public bool Equals(OrderId other)
{
return Value == other.Value;
}

public override bool Equals(object? obj)
{
return obj is OrderId other && Equals(other);
}

public override int GetHashCode()
{
return Value.GetHashCode();
}

public override string ToString()
{
return Value.ToString();
}

public static bool operator ==(OrderId left, OrderId right)
{
return left.Equals(right);
}

public static bool operator !=(OrderId left, OrderId right)
{
return !left.Equals(right);
}
}

この例では、getのみのプロパティ、コンストラクタでのバリデーション、等価性の実装、ToStringの実装を含めています。

値オブジェクトとして使うreadonly structでは、このような形を基本にすると実務でも扱いやすくなります。

10. readonly structに関するFAQ

10-1. readonly structはいつから使える?

readonly structはC# 7.2で導入されました。C# 7.2では、readonly structのほか、inパラメータやref readonly戻り値、ref structなど、値型や参照渡しに関する機能が追加されています。Microsoft Learn

古いプロジェクトで使えない場合は、C#の言語バージョンを確認しましょう。

10-2. readonly structはパフォーマンスが必ず良くなる?

必ず良くなるわけではありません。

readonly structは、防御的コピーを減らせる可能性がありますが、型のサイズ、呼び出し回数、JIT最適化、inパラメータの使い方などによって結果は変わります。

パフォーマンス目的で導入する場合は、実際に計測することが大切です。

設計上の目的としては、「不変な値型であることを明確にする」ことが主なメリットです。

10-3. structはすべてreadonlyにすべき?

すべてのstructreadonly structにする必要はありません。

readonly structに向いているのは、小さく、不変で、値そのものを表す型です。

一方、内部状態を変更する必要がある型、大きなデータを持つ型、参照型として共有したほうが自然な型には向きません。

structにするか、readonly structにするか、classにするかは、データの意味と使い方に応じて判断しましょう。

10-4. readonly structとrecord structはどちらを使うべき?

不変性を明確にしつつ、自分で細かく実装したい場合はreadonly structが向いています。

C#
public readonly struct UserId
{
public int Value { get; }

public UserId(int value)
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value));
}

Value = value;
}
}

一方、値の等価性やToStringなどを簡潔に使いたい場合はreadonly record structが便利です。

C#
public readonly record struct UserId(int Value);

ただし、バリデーションや独自ロジックを丁寧に書きたい場合は、通常のreadonly structのほうが意図を表現しやすいことがあります。

10-5. readonly structで参照型プロパティを持ってもよい?

参照型プロパティを持つこと自体は可能です。

C#
public readonly struct UserName
{
public string Value { get; }

public UserName(string value)
{
Value = value;
}
}

stringは不変なので、このような使い方は問題になりにくいです。

注意が必要なのは、List<T>Dictionary<TKey, TValue>のように中身を変更できる参照型です。

C#
public readonly struct BadValues
{
public List<int> Items { get; }

public BadValues(List<int> items)
{
Items = items;
}
}

この場合、Itemsの参照自体は変更できなくても、リストの中身は変更できます。

参照型プロパティを持つ場合は、その型が不変かどうか、外部から変更されないかを確認しましょう。

まとめ

readonly structは、C#で不変な値型を表現するための便利な機能です。

通常のstructと違い、readonly structではインスタンス状態の変更が制限されます。そのため、座標、金額、数量、ID、サイズ、範囲など、小さく意味のある値を安全に扱いたい場合に向いています。

主なメリットは、不変性を型で表現できること、意図しない値の変更を防げること、防御的コピーを減らせる可能性があること、コードの意図が読みやすくなることです。

一方で、すべてのインスタンスフィールドをreadonlyにする必要があること、参照型フィールドの中身までは自動で不変にならないこと、大きすぎるstructではコピーコストが問題になること、default値への対応が必要なことには注意が必要です。

実務では、次のような方針で使うとよいでしょう。

小さく不変な値を表すならreadonly struct、値の等価性や簡潔な記述を重視するならreadonly record struct、可変状態や大きなデータを扱うならclassを検討します。

readonly structは、正しく使えばC#のコードをより安全で読みやすくできます。特に、値オブジェクトやパフォーマンスを意識するライブラリ設計では、有力な選択肢になります。