C# の struct(構造体)は、スタックに積まれる値型の軽量クラスです。小さなデータのまとまりを表現するために設計されており、クラスと同様にメソッド・プロパティ・インターフェース実装を持てる一方、継承は持ちません。
本記事では、OOP の観点で「軽量クラス型」として struct を捉えつつ、C# の各バージョンで追加された機能を順に解説します。
struct とは — OOP 視点での「軽量クラス型」
class との対比
| 特徴 | struct |
class |
|---|---|---|
| メモリ配置 | スタック(または値としてインライン) | ヒープ |
| コピーのセマンティクス | 値コピー(コピーが渡る) | 参照コピー(参照が渡る) |
| デフォルト値 | 全フィールドがゼロ初期化 | null |
| 継承 | 不可(インターフェースは実装可能) | 可能 |
null の可能性 |
なし(Nullable でない限り) | あり |
| GC の負荷 | ない(スタック上で完結する場合) | あり |
いつ struct を選ぶか
Microsoft のガイドラインでは、以下をすべて満たす場合に struct が適切とされています。
- インスタンスのサイズが 16 バイト以下程度
- 不変(immutable) であることが多い
- コレクション要素やローカル変数として頻繁に使われる
- ボックス化が不要な文脈で使われる
Vector2・Color・DateTime・Guid などがその典型例です。
基本的な定義
struct Point
{
public int X;
public int Y;
}
フィールドにアクセスするだけであれば、これで動きます。
Point p;
p.X = 3;
p.Y = 4;
Console.WriteLine($"({p.X}, {p.Y})"); // (3, 4)
メソッド・プロパティ・インターフェースを持てる
struct はクラスと同様にメンバーを定義できます。
struct Point
{
public int X { get; set; }
public int Y { get; set; }
public double Distance() => Math.Sqrt(X * X + Y * Y);
public override string ToString() => $"({X}, {Y})";
}
Point p = new Point { X = 3, Y = 4 };
Console.WriteLine(p.Distance()); // 5
Console.WriteLine(p); // (3, 4)
インターフェースも実装できます。
interface ITranslatable
{
void Translate(int dx, int dy);
}
struct Point : ITranslatable
{
public int X { get; set; }
public int Y { get; set; }
public void Translate(int dx, int dy)
{
X += dx;
Y += dy;
}
}
Constructors(コンストラクタ)— C# 1.0 / 10 / 11
C# 1.0 〜 9 :パラメーターなしコンストラクタは定義不可
初期の C# では、struct にパラメーターなしのコンストラクタを定義することはできませんでした。new Point() と書いた場合はコンパイラが暗黙に全フィールドをゼロ初期化するだけです。
パラメーターありのコンストラクタは定義できましたが、すべてのフィールドを初期化しなければコンパイルエラーになります。
struct Point
{
public int X;
public int Y;
// C# 1.0 〜 9 でも可:パラメーターありコンストラクタ
public Point(int x, int y)
{
X = x;
Y = y; // すべてのフィールドを初期化しないとエラー
}
}
C# 10 :パラメーターなしカスタムコンストラクタが解禁
struct Counter
{
public int Value;
// C# 10 から定義可能
public Counter()
{
Value = 1; // 好きな初期値を設定できる
}
}
var c = new Counter();
Console.WriteLine(c.Value); // 1
ただし default(Counter) はコンストラクタを呼ばず、依然としてゼロ初期化されるため注意が必要です。
Counter d = default;
Console.WriteLine(d.Value); // 0(コンストラクタは呼ばれない)
C# 11 :required メンバーと合わせた利用
C# 11 で導入された required 修飾子を struct のプロパティに付けると、オブジェクト初期化子での指定を強制できます。
struct Config
{
public required string Host { get; init; }
public required int Port { get; init; }
}
// コンパイルエラーになる(required メンバーが未設定)
// Config c = new Config();
// OK
Config c = new Config { Host = "localhost", Port = 8080 };
Field Initializers(フィールド初期化子)— C# 10
C# 10 以前は、struct のフィールド宣言で初期値を指定することはできませんでした。C# 10 からはクラスと同様に宣言と同時に初期値を書けます。
// C# 10 以降
struct Gauge
{
public int Min = 0; // フィールド初期化子
public int Max = 100; // フィールド初期化子
public int Value = 50; // フィールド初期化子
}
var g = new Gauge();
Console.WriteLine(g.Value); // 50
ただし、フィールド初期化子はカスタムコンストラクタを経由したときのみ実行されます。default キーワードや new Gauge() をコンストラクタが定義されていない場合に使うと、フィールド初期化子は無視されてゼロ初期化になります。
Gauge d = default;
Console.WriteLine(d.Value); // 0(初期化子は実行されない)
パラメーターなしカスタムコンストラクタを定義すると、初期化子が確実に実行されます。
struct Gauge
{
public int Min = 0;
public int Max = 100;
public int Value = 50;
public Gauge() { } // パラメーターなしコンストラクタを明示
}
var g = new Gauge();
Console.WriteLine(g.Value); // 50(初期化子が実行される)
readonly struct(読み取り専用構造体)— C# 7.2
readonly 修飾子を struct に付けると、すべてのフィールドとプロパティが読み取り専用になります。インスタンスが作られた後の状態変更が防止され、真に不変な値型を定義できます。
readonly struct ImmutablePoint
{
public int X { get; }
public int Y { get; }
public ImmutablePoint(int x, int y)
{
X = x;
Y = y;
}
public ImmutablePoint Translate(int dx, int dy)
=> new ImmutablePoint(X + dx, Y + dy);
}
readonly struct の利点はパフォーマンス面にも現れます。通常の struct を in 引数(後述)や ref readonly で渡すとき、コンパイラは防御コピー(defensive copy)を生成することがあります。readonly struct はこれを抑制できます。
// readonly struct ならコンパイラが防御コピーを生成しない
void Print(in ImmutablePoint p)
{
Console.WriteLine($"({p.X}, {p.Y})");
}
readonly members(読み取り専用メンバー)— C# 8.0
struct 全体を readonly にせず、特定のメンバーだけを readonly にしたい場合は C# 8.0 の readonly メンバーが使えます。
struct Rectangle
{
public int Width;
public int Height;
// このメソッドは this を変更しないことをコンパイラに伝える
public readonly int Area() => Width * Height;
// このプロパティも読み取り専用
public readonly string Description => $"{Width}x{Height}";
}
readonly メンバーの中で this を変更しようとするとコンパイルエラーになります。
struct Rectangle
{
public int Width;
public int Height;
public readonly void Reset()
{
Width = 0; // エラー:readonly メンバーで this のフィールドを変更できない
}
}
この機能により、readonly struct にできないが一部のメソッドだけ副作用がない、という状況をコンパイラに正確に伝えられます。防御コピーの抑制も部分的に得られます。
ref struct — C# 7.2
ref struct はスタック専用の構造体です。ヒープに配置できないという制約を持つ代わりに、スタック上のメモリを直接参照する ref フィールドを持てるようになります。
ref struct SpanLike
{
private ref int _first;
// ...
}
代表例:Span<T>
Span<T> と ReadOnlySpan<T> は ref struct として実装されており、配列・文字列・スタック上のメモリなどをコピーなしに表現できます。
int[] array = { 1, 2, 3, 4, 5 };
Span<int> span = array.AsSpan(1, 3); // index 1〜3 のスライス
foreach (int v in span)
Console.Write($"{v} "); // 2 3 4
ref struct の制約
ref struct はスタックに留まる必要があるため、以下の制約があります。
| 制約 | 理由 |
|---|---|
| ヒープへの配置不可(クラスフィールドにできない) | GC に管理されると移動の恐れがある |
| ボックス化不可 | object への変換でヒープに移る |
interface の実装不可(C# 13 以前) |
インターフェース越しに使うとボックス化が起きる |
async メソッドのローカル変数に使用不可 |
async ステートマシンはヒープに生成される |
lambda・delegate のキャプチャ不可 |
クロージャーはヒープに生成される |
class Container
{
// コンパイルエラー:ref struct はフィールドにできない
// Span<int> _span;
}
Disposal ref struct(ref struct の IDisposable)— C# 8.0 / 13
C# 8.0 :インターフェースなしで Dispose できる
ref struct は IDisposable インターフェースを実装できません(ボックス化が必要なため)。しかし C# 8.0 から、Dispose() メソッドを持っているだけで using ステートメントが使えるようになりました。
ref struct RentedBuffer
{
private int[] _buffer;
private readonly int _length;
public RentedBuffer(int length)
{
_buffer = System.Buffers.ArrayPool<int>.Shared.Rent(length);
_length = length;
}
public Span<int> AsSpan() => _buffer.AsSpan(0, _length);
// IDisposable は実装できないが、Dispose() メソッドは定義できる
public void Dispose()
{
if (_buffer is not null)
{
System.Buffers.ArrayPool<int>.Shared.Return(_buffer);
_buffer = null!;
}
}
}
Dispose() メソッドを持つ ref struct は using で使用できます。
using var buf = new RentedBuffer(256);
Span<int> span = buf.AsSpan();
// ... span を操作 ...
// ブロックを抜けると自動で Dispose() が呼ばれる
C# 13 :ref struct がインターフェースを実装可能に
C# 13(.NET 9)では ref struct の制約が緩和され、インターフェースを実装できるようになりました。ただしボックス化は依然として禁止されており、interface 型の変数やジェネリック型パラメーターへの代入は制限されます。
// C# 13
ref struct MySpan<T> : IDisposable
{
// ...
public void Dispose()
{
// クリーンアップ処理
}
}
また、C# 13 では allows ref struct 制約をジェネリック型パラメーターに付与することで、ref struct をジェネリックメソッドに渡せるようになりました。
// T に ref struct を渡せる
void Process<T>(T value) where T : allows ref struct, IDisposable
{
using (value)
{
// ...
}
}
まとめ
| 機能 | 導入バージョン | 要点 |
|---|---|---|
基本的な struct |
C# 1.0 | 値型・スタック配置・値コピーセマンティクス |
| パラメーターありコンストラクタ | C# 1.0 | すべてのフィールドを初期化する必要がある |
readonly struct |
C# 7.2 | すべてのフィールド・プロパティが読み取り専用 |
ref struct |
C# 7.2 | スタック専用・ref フィールドを持てる |
using 対応 Dispose() |
C# 8.0 | IDisposable なしで using が使える |
readonly メンバー |
C# 8.0 | 個々のメソッド・プロパティを readonly に |
| パラメーターなしカスタムコンストラクタ | C# 10 | new Struct() で任意の初期値を設定できる |
| フィールド初期化子 | C# 10 | 宣言時に初期値を書ける |
required メンバー |
C# 11 | オブジェクト初期化子での設定を強制できる |
ref struct がインターフェースを実装可能 |
C# 13 | ボックス化禁止の制約は維持 |
struct はクラスの代替として安易に使うのではなく、小さくて不変に近いデータを高頻度に扱う場面で適切に選択することが重要です。ref struct と Span<T> を活用すれば、アロケーションゼロのパフォーマンスクリティカルなコードも記述できます。