C#の構造体とは?クラスとの違い・使い方・注意点を初心者向けに徹底解説
はじめに
C#の構造体は、関連するデータをひとまとめにして扱うための仕組みです。intやdoubleのような値型と同じカテゴリに属し、座標、サイズ、日付範囲、色、金額など、「小さな値のまとまり」を表すときによく使われます。
初心者のうちは「クラスと何が違うのか」「いつ構造体を使えばよいのか」「structとclassはどう使い分けるのか」で迷いやすいでしょう。C#では、構造体は値型、クラスは参照型という大きな違いがあります。この違いを理解すると、代入時の挙動、メソッドに渡したときの動き、nullの扱い、パフォーマンス面の注意点が見えてきます。
この記事では、C#の構造体とは何か、クラスとの違い、基本的な使い方、サンプルコード、設計時の注意点まで、初心者にもわかりやすく解説します。
1. C#の構造体とは?
C#の構造体とは、structキーワードを使って定義するデータ型です。複数の値を1つのまとまりとして扱うことができ、フィールド、プロパティ、メソッド、コンストラクターなどを持てます。
MicrosoftのC#リファレンスでも、構造体は「データと関連する機能をカプセル化できる値型」と説明されています。つまり、構造体は単なる変数の集まりではなく、必要に応じて処理もまとめられるデータ型です。
1-1. 構造体は値型のデータ型
C#の型は、大きく「値型」と「参照型」に分けられます。構造体は値型です。
値型の変数は、基本的にその値そのものを保持します。一方、クラスのような参照型の変数は、実体そのものではなく、実体が存在する場所への参照を保持します。MicrosoftのC#ドキュメントでも、値型の変数はその型のインスタンスを含み、代入や引数渡しでは値がコピーされると説明されています。
たとえば、次のような構造体を考えます。
C#struct Point
{
public int X;
public int Y;
}
このPointは、X座標とY座標を1つにまとめた値型のデータ型です。
1-2. 構造体で表現できるデータの例
構造体は、次のような「小さく、ひとまとまりの値」を表すのに向いています。
座標を表すPoint、幅と高さを表すSize、範囲を表すRange、RGBの色を表すColor、金額を表すMoney、日付の期間を表すDateRangeなどです。
たとえば、座標をクラスではなく構造体で表すと、「この座標値そのもの」を扱っていることが明確になります。
C#struct Point
{
public int X;
public int Y;
}
このように、構造体は「値として自然に扱いたいデータ」に適しています。
1-3. 構造体を使う目的
構造体を使う主な目的は、関連する複数の値を1つの型として整理することです。
たとえば、座標を表すためにint xとint yを別々に扱うと、引数や戻り値が増えてコードが読みにくくなります。
C#void Move(int x, int y)
{
Console.WriteLine($"{x}, {y}");
}
構造体を使えば、座標という意味を持つ1つの値として扱えます。
C#void Move(Point point)
{
Console.WriteLine($"{point.X}, {point.Y}");
}
このように、構造体を使うとコードの意味が明確になり、関連する値をまとめて管理しやすくなります。
1-4. クラスではなく構造体を学ぶべき理由
C#ではクラスを使う場面が多いですが、構造体を理解しておくことも重要です。なぜなら、C#の基本型であるint、double、bool、DateTimeなども値型であり、構造体と同じような性質を持っているからです。
構造体を学ぶと、次のようなC#の重要な仕組みを理解しやすくなります。
値型と参照型の違い、代入時のコピー、メソッド引数でのコピー、nullの扱い、ボックス化、パフォーマンスへの影響などです。
つまり、構造体は単に「classの別バージョン」ではなく、C#の型システムを理解するための重要なテーマです。
2. C#の構造体とクラスの違い
構造体とクラスは、どちらもフィールド、プロパティ、メソッド、コンストラクターを持てます。しかし、内部的な扱いは大きく異なります。
最も重要な違いは、構造体は値型、クラスは参照型であることです。
2-1. 値型と参照型の違い
構造体は値型です。値型の変数は、値そのものを保持します。
クラスは参照型です。参照型の変数は、オブジェクト本体ではなく、オブジェクトを指す参照を保持します。
C#struct PointStruct
{
public int X;
public int Y;
}
class PointClass
{
public int X;
public int Y;
}
この2つは見た目が似ていますが、代入したときの動きが異なります。
構造体を代入すると値がコピーされます。クラスを代入すると参照がコピーされ、同じオブジェクトを指します。
2-2. メモリ上の扱いの違い
構造体は値型なので、変数が直接データを保持します。C#の仕様でも、構造体型の変数は構造体のデータを直接含み、クラス型の変数はデータへの参照を含むと説明されています。
ただし、「構造体は必ずスタックに置かれる」と覚えるのは正確ではありません。構造体がクラスのフィールドになっている場合や、配列に格納されている場合、ボックス化された場合など、実際の配置は状況によって変わります。
初心者のうちは、次のように理解するとよいでしょう。
構造体は値そのものを扱う型、クラスは参照を通じてオブジェクトを扱う型です。
2-3. 代入時・引数渡し時の挙動の違い
構造体は代入時にコピーされます。
C#PointStruct a = new PointStruct { X = 10, Y = 20 };
PointStruct b = a;
b.X = 99;
Console.WriteLine(a.X); // 10
Console.WriteLine(b.X); // 99
b = aで値がコピーされるため、b.Xを変更してもa.Xは変わりません。
一方、クラスでは参照がコピーされます。
C#PointClass a = new PointClass { X = 10, Y = 20 };
PointClass b = a;
b.X = 99;
Console.WriteLine(a.X); // 99
Console.WriteLine(b.X); // 99
aとbは同じオブジェクトを参照しているため、b.Xを変更するとa.Xも変わったように見えます。
2-4. 継承できるかどうかの違い
クラスは、ほかのクラスを継承できます。
C#class Animal
{
}
class Dog : Animal
{
}
一方、構造体はほかのクラスや構造体を継承できません。構造体は暗黙的にSystem.ValueTypeを継承しますが、開発者が任意の型を継承させることはできません。
ただし、構造体はインターフェースを実装できます。
C#interface IPrintable
{
void Print();
}
struct Point : IPrintable
{
public int X;
public int Y;
public void Print()
{
Console.WriteLine($"{X}, {Y}");
}
}
2-5. nullを扱えるかどうかの違い
通常の構造体はnullにできません。
C#Point point = null; // コンパイルエラー
構造体でnullを扱いたい場合は、Nullable型を使います。
C#Point? point = null;
Point?はNullable<Point>の省略記法です。未設定を表したい場合は、構造体そのものをnullにするのではなく、Nullable型を使います。
一方、クラスは参照型なので、通常はnullを代入できます。
C#PointClass point = null;
2-6. 構造体とクラスの違いを比較表で整理
| 比較項目 | 構造体 | クラス |
|---|---|---|
| 型の分類 | 値型 | 参照型 |
| 定義キーワード | struct | class |
| 代入時の挙動 | 値がコピーされる | 参照がコピーされる |
| 引数渡し | 基本的にコピーされる | 参照が渡される |
| null | 通常は不可。T?で可能 | 可能 |
| 継承 | 不可 | 可能 |
| インターフェース実装 | 可能 | 可能 |
| 向いている用途 | 小さな値のまとまり | 状態や振る舞いを持つ大きなオブジェクト |
| 例 | 座標、色、範囲、金額 | ユーザー、注文、サービス、画面 |
構造体とクラスは見た目が似ていても、設計上の意味は異なります。構造体は「値」を表すもの、クラスは「オブジェクト」を表すものとして考えると理解しやすくなります。
3. C#の構造体の基本的な使い方
ここからは、C#で構造体を定義して使う基本を見ていきましょう。
3-1. structキーワードで構造体を定義する
構造体はstructキーワードで定義します。
C#struct Point
{
}
これでPointという名前の構造体を定義できます。実際には、この中にフィールドやプロパティ、メソッドなどを追加して使います。
3-2. フィールドを定義する
フィールドは、構造体が保持するデータです。
C#struct Point
{
public int X;
public int Y;
}
この例では、XとYという2つのフィールドを持つPoint構造体を定義しています。
使用例は次のとおりです。
C#Point point;
point.X = 10;
point.Y = 20;
Console.WriteLine(point.X);
Console.WriteLine(point.Y);
ただし、実務ではフィールドを直接公開するより、プロパティを使う設計が一般的です。
3-3. プロパティを定義する
プロパティを使うと、データの取得や設定を安全に管理できます。
C#struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
使用例は次のとおりです。
C#Point point = new Point();
point.X = 10;
point.Y = 20;
Console.WriteLine($"{point.X}, {point.Y}");
プロパティにすることで、あとから入力チェックや読み取り専用化などの設計変更もしやすくなります。
3-4. メソッドを定義する
構造体にはメソッドも定義できます。
C#struct Point
{
public int X { get; set; }
public int Y { get; set; }
public void Print()
{
Console.WriteLine($"{X}, {Y}");
}
}
使用例です。
C#Point point = new Point { X = 10, Y = 20 };
point.Print(); // 10, 20
構造体はデータだけでなく、そのデータに関連する処理もまとめられます。
3-5. コンストラクターを定義する
構造体にもコンストラクターを定義できます。
C#struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
使用例です。
C#Point point = new Point(10, 20);
Console.WriteLine(point.X); // 10
Console.WriteLine(point.Y); // 20
コンストラクターを使うと、構造体を作成するときに必要な値をまとめて設定できます。
3-6. 構造体のインスタンスを作成する
構造体のインスタンスは、newを使って作成できます。
C#Point point = new Point();
コンストラクターがある場合は、引数を渡して初期化できます。
C#Point point = new Point(10, 20);
また、オブジェクト初期化子も使えます。
C#Point point = new Point
{
X = 10,
Y = 20
};
ただし、読み取り専用プロパティだけを持つ構造体では、オブジェクト初期化子ではなくコンストラクターを使うのが基本です。
3-7. 構造体の値を取得・変更する
プロパティを持つ構造体の値は、次のように取得・変更できます。
C#Point point = new Point { X = 10, Y = 20 };
Console.WriteLine(point.X); // 10
point.X = 30;
Console.WriteLine(point.X); // 30
ただし、構造体はコピーされる性質を持つため、可変にすると意図しない挙動の原因になることがあります。実務では、変更可能な構造体よりも、不変の構造体として設計するほうが安全です。
4. 構造体のサンプルコードで理解する
ここでは、実際のサンプルコードを使ってC#の構造体を理解していきます。
4-1. 座標を表す構造体の例
座標は、構造体の代表的な例です。
C#struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
使用例です。
C#Point point = new Point(10, 20);
Console.WriteLine(point.X); // 10
Console.WriteLine(point.Y); // 20
座標は「XとYの組み合わせで1つの値」と考えられるため、構造体に向いています。
4-2. 商品情報を表す構造体の例
商品情報も、小さなデータであれば構造体として表せます。
C#struct Product
{
public string Name { get; }
public int Price { get; }
public Product(string name, int price)
{
Name = name;
Price = price;
}
}
使用例です。
C#Product product = new Product("ノート", 150);
Console.WriteLine(product.Name); // ノート
Console.WriteLine(product.Price); // 150
ただし、商品に在庫管理、カテゴリ、レビュー、割引計算、状態変更など多くの振る舞いが加わる場合は、クラスのほうが適しています。
4-3. メソッドを持つ構造体の例
構造体には、値に関連する処理をメソッドとして持たせることができます。
C#struct Rectangle
{
public int Width { get; }
public int Height { get; }
public Rectangle(int width, int height)
{
Width = width;
Height = height;
}
public int GetArea()
{
return Width * Height;
}
}
使用例です。
C#Rectangle rect = new Rectangle(5, 4);
Console.WriteLine(rect.GetArea()); // 20
このように、構造体にメソッドを書くことで、データと処理を自然にまとめられます。
4-4. コンストラクターを使った初期化の例
コンストラクターを使うと、作成時に正しい状態にできます。
C#struct Money
{
public int Amount { get; }
public string Currency { get; }
public Money(int amount, string currency)
{
Amount = amount;
Currency = currency;
}
public override string ToString()
{
return $"{Amount} {Currency}";
}
}
使用例です。
C#Money price = new Money(1000, "JPY");
Console.WriteLine(price); // 1000 JPY
金額のように「数値と通貨コードがセットで意味を持つ」データは、構造体で表現しやすい例です。
4-5. 構造体を配列やListで扱う例
構造体は、配列やList<T>にも格納できます。
C#Point[] points =
{
new Point(0, 0),
new Point(10, 20),
new Point(30, 40)
};
foreach (Point point in points)
{
Console.WriteLine($"{point.X}, {point.Y}");
}
List<T>で扱う場合は次のように書けます。
C#List<Point> points = new List<Point>();
points.Add(new Point(0, 0));
points.Add(new Point(10, 20));
foreach (Point point in points)
{
Console.WriteLine($"{point.X}, {point.Y}");
}
大量の小さなデータを扱う場合、構造体は選択肢になります。ただし、サイズが大きい構造体を大量にコピーすると、かえってパフォーマンスが悪くなる場合があります。
5. C#の構造体で押さえるべき重要な特徴
C#の構造体を正しく使うには、いくつかの特徴を理解しておく必要があります。
5-1. 構造体はコピーされる
構造体の最も重要な特徴は、代入時や引数渡し時にコピーされることです。
C#Point a = new Point(10, 20);
Point b = a;
b = new Point(99, 20);
Console.WriteLine(a.X); // 10
Console.WriteLine(b.X); // 99
aとbは別々の値です。これはクラスとは大きく異なります。
5-2. 構造体は既定値を持つ
構造体には既定値があります。defaultを使うと、各フィールドが既定値で初期化された構造体を作成できます。
C#Point point = default;
Console.WriteLine(point.X); // 0
Console.WriteLine(point.Y); // 0
intの既定値は0、boolの既定値はfalse、参照型フィールドの既定値はnullです。
構造体は既定値を持つため、コンストラクターを通さずに初期状態が作られる場合があります。この点は設計時に注意が必要です。
5-3. 構造体は継承できない
構造体は、ほかのクラスや構造体を継承できません。
C#struct BaseStruct
{
}
// struct ChildStruct : BaseStruct // エラー
// {
// }
継承を使って共通処理をまとめたい場合は、構造体ではなくクラスを使うのが基本です。
5-4. 構造体はインターフェースを実装できる
構造体は継承できませんが、インターフェースは実装できます。
C#interface IDisplayable
{
void Display();
}
struct Point : IDisplayable
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
public void Display()
{
Console.WriteLine($"{X}, {Y}");
}
}
インターフェースを使えば、構造体にも共通の振る舞いを持たせられます。
5-5. 構造体にもコンストラクター・プロパティ・メソッドを書ける
構造体は、フィールドだけの単純なデータ型ではありません。
次のように、コンストラクター、プロパティ、メソッドをまとめて書けます。
C#struct Size
{
public int Width { get; }
public int Height { get; }
public Size(int width, int height)
{
Width = width;
Height = height;
}
public int Area()
{
return Width * Height;
}
}
構造体を使う場合でも、クラスと同じように読みやすく設計することが大切です。
5-6. readonly structとは
readonly structは、構造体全体を読み取り専用として扱うための仕組みです。
C#readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
readonly structにすると、構造体の状態を後から変更しない設計であることを明示できます。
不変の値を表す構造体では、readonly structを使うと安全性と意図が伝わりやすくなります。
5-7. record structとは
record structは、値の比較や表示に便利な機能を持つ構造体です。C#のレコード型では、record class、record struct、readonly record structを定義できます。公式ドキュメントでも、record修飾子を持つ型は、同じ型で同じ値を格納している場合に等しいと説明されています。
例を見てみましょう。
C#public readonly record struct Point(int X, int Y);
このように書くだけで、XとYを持つ構造体を簡潔に定義できます。
C#var a = new Point(10, 20);
var b = new Point(10, 20);
Console.WriteLine(a == b); // True
Console.WriteLine(a); // Point { X = 10, Y = 20 }
record structは、値の比較やToStringの出力を簡単にしたい場合に便利です。
6. 構造体を使うべき場面
構造体は便利ですが、すべての場面で使うべきものではありません。ここでは、構造体が向いている場面を整理します。
6-1. 小さなデータのまとまりを表したい場合
構造体は、小さなデータのまとまりに向いています。
たとえば、座標、サイズ、範囲、色、金額などです。
C#readonly struct Size
{
public int Width { get; }
public int Height { get; }
public Size(int width, int height)
{
Width = width;
Height = height;
}
}
このように、少数の値だけで意味が完結するデータは構造体にしやすいです。
6-2. 値そのものとして扱いたい場合
構造体は、参照ではなく値そのものとして扱いたい場合に向いています。
たとえば、座標(10, 20)は「どこかに存在するオブジェクト」というより、「Xが10、Yが20の値」です。
C#Point p1 = new Point(10, 20);
Point p2 = p1;
このとき、p2はp1と同じ値を持つ別のデータとして扱われます。値として独立しているほうが自然な場合、構造体が適しています。
6-3. 不変のデータを表したい場合
構造体は、不変のデータと相性が良いです。
C#readonly struct Money
{
public int Amount { get; }
public string Currency { get; }
public Money(int amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
不変にしておけば、コピーされたあとに値が変更されて混乱するリスクを減らせます。
構造体を設計するときは、できるだけ変更できない形にするのがおすすめです。
6-4. 大量に生成される軽量データを扱う場合
大量の小さなデータを扱う場合、構造体が有効なことがあります。
たとえば、ゲーム開発で座標やベクトルを大量に扱う場合、構造体が使われることがあります。
C#readonly struct Vector2
{
public float X { get; }
public float Y { get; }
public Vector2(float x, float y)
{
X = x;
Y = y;
}
}
ただし、構造体はコピーされるため、サイズが大きいとコピーコストが増えます。「大量に生成されるから構造体が必ず速い」とは限りません。
6-5. 座標・日付・範囲・色などを表す場合
構造体に向いている代表例は、次のようなデータです。
座標、サイズ、ベクトル、日付、時間、範囲、色、金額、IDなどです。
これらは、オブジェクトとして共有するよりも、値として独立して扱うほうが自然です。
C#public readonly record struct DateRange(DateTime Start, DateTime End);
このように、値として意味を持つデータには構造体を検討できます。
7. 構造体を使わないほうがよい場面
構造体は便利ですが、使いどころを間違えるとバグやパフォーマンス低下の原因になります。ここでは、構造体を避けたほうがよい場面を解説します。
7-1. データサイズが大きい場合
構造体はコピーされるため、データサイズが大きい場合には向いていません。
C#struct LargeData
{
public int A;
public int B;
public int C;
public int D;
public int E;
public int F;
public int G;
public int H;
}
このような大きな構造体を何度も代入したり、メソッドに渡したりすると、そのたびにコピーコストが発生します。
データが大きい場合は、クラスを使うほうが適していることが多いです。
7-2. 状態を頻繁に変更する場合
構造体は、頻繁に状態を変更するデータにはあまり向いていません。
C#struct Counter
{
public int Value { get; set; }
public void Increment()
{
Value++;
}
}
このような可変の構造体は、コピーされた値を変更しているのか、元の値を変更しているのかがわかりにくくなることがあります。
状態を頻繁に変更する場合は、クラスを使うほうが自然です。
7-3. 継承を使いたい場合
構造体は継承できません。
共通の基底クラスを作って機能を拡張したい場合や、ポリモーフィズムを使いたい場合は、クラスを選ぶべきです。
C#class Animal
{
public virtual void Speak()
{
}
}
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("ワン");
}
}
このような設計は構造体ではできません。
7-4. nullで未設定を表したい場合
構造体は通常nullにできません。
未設定をnullで表したい場合は、クラスのほうが自然なことがあります。
ただし、構造体でもNullable型を使えばnullを扱えます。
C#Point? point = null;
とはいえ、頻繁にnull状態を扱う設計なら、クラスを使うほうがわかりやすい場合があります。
7-5. 参照共有したい場合
同じデータを複数の場所から共有して変更したい場合、構造体は向いていません。
構造体はコピーされるため、ある変数を変更しても別の変数には反映されません。
C#Point a = new Point(10, 20);
Point b = a;
// bを変更してもaは変わらない
同じオブジェクトを共有したい場合は、クラスを使うべきです。
C#PointClass a = new PointClass { X = 10, Y = 20 };
PointClass b = a;
// bを変更するとaから見ても変更される
7-6. 判断に迷ったらクラスを選ぶべき理由
構造体とクラスで迷った場合は、基本的にはクラスを選ぶのが安全です。
Microsoftの設計ガイドラインでも、型を構造体にするかクラスにするかは重要な設計判断であり、値型と参照型の違いを理解することが重要だとされています。
構造体は便利ですが、コピー、既定値、ボックス化、可変性など、初心者がつまずきやすい特徴があります。明確に「小さな値のまとまり」と判断できる場合に構造体を選び、それ以外はクラスを選ぶとよいでしょう。
8. C#の構造体で初心者がつまずきやすい注意点
構造体は、見た目はクラスに似ていますが、値型ならではの注意点があります。
8-1. 代入すると別の値としてコピーされる
構造体は代入するとコピーされます。
C#Point a = new Point(10, 20);
Point b = a;
b = new Point(99, 20);
Console.WriteLine(a.X); // 10
Console.WriteLine(b.X); // 99
クラスの感覚で使うと、「変更が反映されない」と感じることがあります。しかし、構造体ではこの挙動が正しい動きです。
8-2. メソッドの引数に渡すとコピーされる
構造体をメソッドに渡すと、通常は値がコピーされます。
C#void Move(Point point)
{
point = new Point(100, 200);
}
Point p = new Point(10, 20);
Move(p);
Console.WriteLine(p.X); // 10
Moveメソッドの中でpointを変更しても、元のpは変わりません。
元の値を変更したい場合は、refを使う必要があります。
C#void Move(ref Point point)
{
point = new Point(100, 200);
}
Point p = new Point(10, 20);
Move(ref p);
Console.WriteLine(p.X); // 100
ただし、refはコードの理解を難しくすることがあるため、必要な場合に限定して使いましょう。
8-3. プロパティ経由の変更で意図しない挙動になることがある
構造体をプロパティ経由で扱う場合、コピーが発生して意図した変更ができないことがあります。
たとえば、次のようなクラスを考えます。
C#class Player
{
public Point Position { get; set; }
}
このとき、次のようなコードはエラーになる場合があります。
C#// player.Position.X = 10; // 変更できない場合がある
なぜなら、Positionプロパティから取得した構造体がコピーとして扱われるためです。
このような場合は、新しい値を作ってプロパティに再代入します。
C#player.Position = new Point(10, player.Position.Y);
構造体をプロパティとして持つ場合は、「中身を部分的に変更する」のではなく、「新しい値に置き換える」と考えると安全です。
8-4. 既定値による初期化に注意する
構造体はdefaultで初期化できます。
C#Money money = default;
Console.WriteLine(money.Amount); // 0
Console.WriteLine(money.Currency); // null
このように、コンストラクターで意図した初期化をしなくても値が作られる場合があります。
たとえば、Currencyが必須の設計でも、defaultではnullになってしまいます。構造体を設計するときは、既定値でも問題が起きにくいようにすることが重要です。
8-5. 可変な構造体はバグの原因になりやすい
変更可能な構造体は、コピーとの組み合わせでバグの原因になりやすいです。
C#struct MutablePoint
{
public int X { get; set; }
public int Y { get; set; }
}
このような構造体は一見便利ですが、代入や引数渡しでコピーされたあとに変更されると、どの値が変更されたのか追いにくくなります。
実務では、次のような不変の構造体を優先するのがおすすめです。
C#readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
8-6. ボックス化によるパフォーマンス低下に注意する
構造体は値型ですが、object型やインターフェース型として扱うと、ボックス化が発生することがあります。
C#int number = 10;
object obj = number; // ボックス化
構造体でも同じように、値型を参照型のように扱うための変換が起こる場合があります。
ボックス化が大量に発生すると、メモリ割り当てや処理コストが増える可能性があります。通常のアプリケーションでは過度に気にしすぎる必要はありませんが、パフォーマンスが重要な場面では注意が必要です。
9. 構造体とパフォーマンスの関係
構造体は「クラスより速い」と言われることがありますが、常に正しいわけではありません。
9-1. 構造体は必ず速いわけではない
構造体は値型なので、ヒープ割り当てを避けられる場合があります。そのため、小さなデータを大量に扱う場面では効率的なことがあります。
しかし、構造体はコピーされます。構造体のサイズが大きい場合、代入や引数渡しのたびにコピーコストが発生します。
つまり、構造体は「小さく、単純で、値として扱える」場合に効果を発揮しやすい型です。
9-2. コピーコストが大きくなるケース
次のような構造体は、コピーコストが大きくなる可能性があります。
C#struct LargeStruct
{
public long A;
public long B;
public long C;
public long D;
public long E;
public long F;
}
このような構造体を頻繁にメソッドへ渡すと、そのたびにデータがコピーされます。
C#void Process(LargeStruct data)
{
// dataはコピーされる
}
小さな構造体では問題になりにくいですが、大きな構造体では注意が必要です。
9-3. readonly structで不要なコピーを減らす
readonly structを使うと、構造体が変更されないことを明示できます。
C#readonly struct Vector2
{
public float X { get; }
public float Y { get; }
public Vector2(float x, float y)
{
X = x;
Y = y;
}
public float Length()
{
return MathF.Sqrt(X * X + Y * Y);
}
}
不変であることが明確になるため、安全に扱いやすくなります。また、特定の場面では不要な防御的コピーを避ける助けにもなります。
9-4. in引数・ref引数を使う場面
大きな構造体をコピーしたくない場合、in引数やref引数を使うことがあります。
inは、読み取り専用の参照渡しです。
C#void PrintLength(in Vector2 vector)
{
Console.WriteLine(vector.Length());
}
refは、呼び出し先で元の値を変更できる参照渡しです。
C#void Reset(ref Point point)
{
point = new Point(0, 0);
}
ただし、inやrefを多用するとコードが複雑になります。パフォーマンス上の必要性がある場合に使うのが基本です。
9-5. パフォーマンスより可読性を優先すべきケース
初心者が構造体を使うときは、まず可読性と正しさを優先しましょう。
パフォーマンスを意識しすぎて構造体、ref、in、ボックス化回避などを多用すると、コードが読みにくくなります。
多くのアプリケーションでは、構造体かクラスかによる小さな性能差より、設計のわかりやすさのほうが重要です。
まずは次の基準で判断するとよいでしょう。
小さな値のまとまりなら構造体、状態や振る舞いを持つ大きなオブジェクトならクラスです。
10. 構造体の実践的な設計ポイント
構造体を安全に使うには、設計方針が重要です。
10-1. 小さくシンプルに設計する
構造体は、小さくシンプルに設計しましょう。
C#readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
このように、少数の値だけを持つ構造体は扱いやすいです。
逆に、フィールドが多く、複雑な状態を持つ型はクラスにするほうが自然です。
10-2. できるだけ不変にする
構造体は、できるだけ不変にしましょう。
C#readonly struct Money
{
public int Amount { get; }
public string Currency { get; }
public Money(int amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
不変にすることで、コピーされた構造体があとから変更されて混乱するリスクを減らせます。
値を変更したい場合は、既存の値を変更するのではなく、新しい値を作る考え方が適しています。
10-3. EqualsとGetHashCodeを適切に扱う
構造体を比較したり、DictionaryやHashSetで使ったりする場合は、EqualsとGetHashCodeを意識する必要があります。
C#readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
public override bool Equals(object? obj)
{
return obj is Point other &&
X == other.X &&
Y == other.Y;
}
public override int GetHashCode()
{
return HashCode.Combine(X, Y);
}
}
値として等しいかどうかを正しく判定したい場合は、等価性の扱いを明確にしておきましょう。
簡潔に書きたい場合は、record structを使う選択肢もあります。
C#public readonly record struct Point(int X, int Y);
10-4. ToStringを実装してデバッグしやすくする
構造体では、ToStringを実装しておくとデバッグ時に便利です。
C#readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
public override string ToString()
{
return $"({X}, {Y})";
}
}
使用例です。
C#Point point = new Point(10, 20);
Console.WriteLine(point); // (10, 20)
値の中身がわかりやすく表示されるため、ログ出力やデバッグがしやすくなります。
10-5. クラスとの使い分けルールを決める
実務では、構造体とクラスの使い分けルールを決めておくと迷いにくくなります。
たとえば、次のような基準です。
小さく単純な値なら構造体、値として等価性を判断したいなら構造体、不変にできるなら構造体、状態変更が多いならクラス、継承を使いたいならクラス、参照共有したいならクラス、判断に迷うならクラスです。
このようなルールを持っておくと、構造体を使うべき場面と避けるべき場面を判断しやすくなります。
11. C#の構造体に関するよくある質問
最後に、C#の構造体について初心者が疑問に思いやすいポイントをQ&A形式で整理します。
11-1. 構造体とクラスはどちらを使えばよい?
迷った場合は、まずクラスを選ぶのが安全です。
構造体は、小さく単純で、不変にでき、値そのものとして扱いたいデータに向いています。
たとえば、座標、サイズ、範囲、色、金額などは構造体に向いています。
一方、ユーザー、注文、商品、サービス、画面、設定管理など、状態や振る舞いが複雑なものはクラスに向いています。
11-2. 構造体はnewしなくても使える?
構造体は、状況によってはnewなしで変数を宣言できます。
C#Point point;
point.X = 10;
point.Y = 20;
ただし、ローカル変数として使う場合は、すべてのフィールドが初期化されるまで利用できません。
通常は、わかりやすさのためにnewやコンストラクターを使って初期化するのがおすすめです。
C#Point point = new Point(10, 20);
また、既定値で初期化したい場合はdefaultも使えます。
C#Point point = default;
11-3. 構造体にコンストラクターは書ける?
はい、構造体にはコンストラクターを書けます。
C#struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
コンストラクターを使うことで、構造体を作成するときに必要な値をまとめて設定できます。
C#Point point = new Point(10, 20);
構造体では、既定値による初期化も可能なので、コンストラクターだけで完全に初期化を強制できるとは限らない点に注意しましょう。
11-4. 構造体はnullにできる?
通常の構造体はnullにできません。
C#Point point = null; // エラー
ただし、Nullable型を使えばnullを扱えます。
C#Point? point = null;
Point?は、値がある場合とない場合を表せます。未設定を表したいときに便利です。
11-5. 構造体にメソッドを書いてもよい?
はい、構造体にメソッドを書いても問題ありません。
C#struct Rectangle
{
public int Width { get; }
public int Height { get; }
public Rectangle(int width, int height)
{
Width = width;
Height = height;
}
public int GetArea()
{
return Width * Height;
}
}
構造体に関連する処理であれば、メソッドとしてまとめると自然です。
ただし、構造体の状態を変更するメソッドは注意が必要です。可変な構造体はコピーと組み合わさることでわかりにくくなるため、できるだけ不変に設計しましょう。
11-6. record structとの違いは?
通常のstructは、自分でプロパティ、コンストラクター、Equals、GetHashCode、ToStringなどを設計します。
一方、record structは、値の比較や文字列表現などを簡潔に扱える構文です。
C#public readonly record struct Point(int X, int Y);
このように書くだけで、値を持つ構造体を簡単に定義できます。
C#var a = new Point(10, 20);
var b = new Point(10, 20);
Console.WriteLine(a == b); // True
Console.WriteLine(a); // Point { X = 10, Y = 20 }
値の等価性を重視するデータ型では、record structを使うとコードを短く書けます。
まとめ
C#の構造体は、structキーワードで定義する値型のデータ型です。複数の値を1つにまとめ、必要に応じてプロパティ、メソッド、コンストラクターを持たせることができます。
構造体とクラスの最大の違いは、構造体が値型、クラスが参照型であることです。構造体は代入や引数渡しでコピーされ、通常はnullにできず、継承もできません。一方で、小さく単純な値を表す場合には、構造体を使うことで意味の明確なコードを書けます。
構造体を使うべき場面は、座標、サイズ、範囲、色、金額など、小さな値のまとまりを表す場合です。特に、不変の値として扱えるデータは構造体に向いています。
反対に、データサイズが大きい場合、状態を頻繁に変更する場合、継承を使いたい場合、参照共有したい場合は、構造体ではなくクラスを選ぶほうが安全です。
初心者はまず、「構造体は小さな値を表すもの」「クラスは状態や振る舞いを持つオブジェクトを表すもの」と考えるとよいでしょう。判断に迷ったらクラスを選び、明確に値として扱いたいデータだけ構造体にするのがおすすめです。

