C# structとは?classとの違い・使い方・初心者がつまずく値型の仕組みをわかりやすく解説
はじめに
C#でプログラミングを学んでいると、classと並んでよく登場するのがstructです。
structは日本語では「構造体」と呼ばれ、複数のデータをひとまとまりにして扱うための仕組みです。たとえば、座標を表すXとY、金額を表すAmountとCurrency、範囲を表すStartとEndのように、関連する値を1つの型としてまとめたいときに使えます。
ただし、C#のstructは単に「小さなclassのようなもの」ではありません。classとの大きな違いは、structが値型である点です。この値型の仕組みを理解していないと、代入したときやメソッドに渡したときに「なぜ値が変わらないのか」「なぜ別物として扱われるのか」と混乱しやすくなります。
この記事では、C#のstructとは何か、classとの違い、基本的な使い方、値型の仕組み、初心者がつまずきやすいポイントまで、コード例を交えながらわかりやすく解説します。
1. C#のstructとは?まず押さえる基本
C#のstructは、自分で新しい値型を定義するための機能です。
C#では、データを扱う型が大きく分けて「値型」と「参照型」に分かれます。structは値型に分類され、classは参照型に分類されます。
この違いは、C#のstructを理解するうえで非常に重要です。
1-1. structは「値型」を定義するための仕組み
structを使うと、複数の値を1つの型としてまとめられます。
たとえば、2次元座標を表す型を作りたい場合、次のように書けます。
C#public struct Point
{
public int X;
public int Y;
}
このPointは、XとYという2つの整数を持つ独自の型です。
C#Point p;
p.X = 10;
p.Y = 20;
Console.WriteLine(p.X); // 10
Console.WriteLine(p.Y); // 20
ここで重要なのは、Pointがclassではなくstructで定義されているため、値型として扱われることです。
値型は、変数そのものが値を持つイメージです。代入すると値がコピーされ、元の変数とは別のデータとして扱われます。
1-2. int・bool・DateTimeもstructとして扱われる
C#では、普段何気なく使っている多くの型も実はstructです。
代表的な例として、次のような型があります。
C#int number = 10;
bool isActive = true;
DateTime today = DateTime.Now;
decimal price = 1200m;
これらのint、bool、DateTime、decimalはすべて値型です。そして、C#ではこれらは構造体として定義されています。
つまり、structは特別な場面だけで使うものではありません。C#の基本的な型の多くがstructでできており、C#の型システムを理解するうえで欠かせない存在です。
1-3. structで作れるデータ構造のイメージ
structは、いくつかの値をまとめて1つの意味を持たせたいときに使います。
たとえば、次のようなデータはstructに向いています。
C#public struct Size
{
public int Width;
public int Height;
}
C#public struct Range
{
public int Start;
public int End;
}
C#public struct Money
{
public decimal Amount;
public string Currency;
}
これらは、1つひとつの値だけでは意味が弱く、組み合わせることで意味を持つデータです。
WidthとHeightをまとめればサイズ、StartとEndをまとめれば範囲、AmountとCurrencyをまとめれば金額を表せます。
このように、structは「小さく、まとまった値」を表現するのに適しています。
1-4. classではなくstructを学ぶべき理由
C#では、オブジェクト指向を学ぶときにclassがよく使われます。そのため、初心者はclassを先に覚えることが多いです。
しかし、structを理解しておくことも非常に重要です。
理由は、C#の基本型の多くがstructだからです。
C#int a = 10;
int b = a;
b = 20;
Console.WriteLine(a); // 10
Console.WriteLine(b); // 20
このように、intを代入すると値がコピーされます。これはintが値型だからです。
自分で作ったstructも同じように動作します。
structを理解すると、C#における値のコピー、メソッド引数、ref、out、in、Nullable<T>、boxingなど、多くの重要な仕組みが理解しやすくなります。
2. C# structの基本的な書き方と使い方
ここからは、C#でstructをどのように書くのかを見ていきましょう。
structの基本構文はclassとよく似ています。フィールド、プロパティ、メソッド、コンストラクターを持つことができます。
2-1. structの定義方法
structは、次のように定義します。
C#public struct Point
{
public int X;
public int Y;
}
基本形は次のとおりです。
C#アクセス修飾子 struct 構造体名
{
メンバー
}
publicを付けると、他のクラスやファイルからも使える構造体になります。
C#public struct UserId
{
public int Value;
}
struct名は、classと同じようにパスカルケースで書くのが一般的です。
C#Point
Money
DateRange
UserId
2-2. フィールドとプロパティの書き方
structにはフィールドを持たせることができます。
C#public struct Point
{
public int X;
public int Y;
}
ただし、実務ではフィールドを直接公開するよりも、プロパティを使うことが多いです。
C#public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
このように書くと、XとYをプロパティとして扱えます。
C#Point p = new Point();
p.X = 10;
p.Y = 20;
Console.WriteLine($"{p.X}, {p.Y}");
変更できない値として扱いたい場合は、setをなくして読み取り専用にできます。
C#public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
この場合、作成後にXやYを変更できません。
C#Point p = new Point(10, 20);
// p.X = 30; // エラー
2-3. メソッドを持つstructの例
structは、フィールドやプロパティだけでなくメソッドも持てます。
C#public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
public double DistanceFromOrigin()
{
return Math.Sqrt(X * X + Y * Y);
}
}
使い方は次のとおりです。
C#Point p = new Point(3, 4);
Console.WriteLine(p.DistanceFromOrigin()); // 5
この例では、原点からの距離を計算するメソッドをPoint構造体に持たせています。
データと、そのデータに関する処理を一緒に定義できる点はclassと似ています。
2-4. インスタンス化と初期化の方法
structはnewを使ってインスタンス化できます。
C#Point p = new Point();
p.X = 10;
p.Y = 20;
コンストラクターを定義している場合は、引数を渡して初期化できます。
C#Point p = new Point(10, 20);
また、プロパティにsetがある場合はオブジェクト初期化子も使えます。
C#Point p = new Point
{
X = 10,
Y = 20
};
structは値型なので、変数宣言時に既定値を持つことができます。
C#Point p = default;
Console.WriteLine(p.X); // 0
Console.WriteLine(p.Y); // 0
intの既定値が0であるように、structの各フィールドもそれぞれの型の既定値で初期化されます。
2-5. コンストラクターを使うときの注意点
structにコンストラクターを書くときは、すべてのフィールドまたはプロパティを初期化する必要があります。
C#public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
次のように、一部だけ初期化するとエラーになります。
C#public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x)
{
X = x;
// Yが初期化されていないためエラー
}
}
structは常に既定値を持てる必要があるため、初期化のルールがclassより厳しく感じられることがあります。
また、defaultで作成された場合は、自分で定義したコンストラクターを通らずに、各メンバーが既定値になります。
C#Point p = default;
このとき、int型のXとYは0になります。
3. structとclassの違いを初心者向けに比較
structとclassは見た目が似ていますが、内部の扱いは大きく異なります。
最大の違いは、structは値型、classは参照型であることです。
3-1. 値型と参照型の違い
値型は、変数が値そのものを持つイメージです。
C#int a = 10;
int b = a;
b = 20;
Console.WriteLine(a); // 10
Console.WriteLine(b); // 20
b = aと書くと、aの値がbにコピーされます。その後bを変更しても、aには影響しません。
一方、参照型は、変数がオブジェクトそのものではなく、オブジェクトへの参照を持つイメージです。
C#public class Person
{
public string Name { get; set; }
}
C#Person p1 = new Person { Name = "Alice" };
Person p2 = p1;
p2.Name = "Bob";
Console.WriteLine(p1.Name); // Bob
Console.WriteLine(p2.Name); // Bob
p1とp2は同じオブジェクトを参照しているため、p2から変更するとp1にも反映されます。
3-2. 代入したときの挙動の違い
structを代入すると、値がコピーされます。
C#public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
C#Point p1 = new Point { X = 10, Y = 20 };
Point p2 = p1;
p2.X = 99;
Console.WriteLine(p1.X); // 10
Console.WriteLine(p2.X); // 99
p2 = p1の時点で、p1の値がp2にコピーされます。そのため、p2.Xを変更してもp1.Xは変わりません。
一方、classの場合は参照がコピーされます。
C#public class PointClass
{
public int X { get; set; }
public int Y { get; set; }
}
C#PointClass p1 = new PointClass { X = 10, Y = 20 };
PointClass p2 = p1;
p2.X = 99;
Console.WriteLine(p1.X); // 99
Console.WriteLine(p2.X); // 99
p1とp2が同じインスタンスを指しているため、片方の変更がもう片方にも見えます。
3-3. メソッドに渡したときの挙動の違い
structをメソッドに渡すと、基本的にはコピーが渡されます。
C#public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
C#static void Move(Point p)
{
p.X += 10;
}
C#Point point = new Point { X = 1, Y = 2 };
Move(point);
Console.WriteLine(point.X); // 1
Moveメソッドの中でp.Xを変更しても、元のpointは変わりません。メソッドに渡されたのはコピーだからです。
一方、classを渡すと参照が渡されるため、オブジェクトの中身を変更できます。
C#public class PointClass
{
public int X { get; set; }
public int Y { get; set; }
}
C#static void Move(PointClass p)
{
p.X += 10;
}
C#PointClass point = new PointClass { X = 1, Y = 2 };
Move(point);
Console.WriteLine(point.X); // 11
この違いは、初心者が特につまずきやすいポイントです。
3-4. nullを代入できるかどうかの違い
classは参照型なので、通常はnullを代入できます。
C#Person person = null;
一方、structは値型なので、通常はnullを代入できません。
C#Point point = null; // エラー
ただし、Nullable<T>または?を使うと、structでもnullを扱えます。
C#Point? point = null;
int? number = null;
DateTime? date = null;
Point?はNullable<Point>の省略記法です。
3-5. 継承できるかどうかの違い
classは他のクラスを継承できます。
C#public class Animal
{
public void Eat()
{
Console.WriteLine("Eat");
}
}
public class Dog : Animal
{
}
一方、structは他のstructやclassを継承できません。
C#public struct MyStruct : OtherStruct // エラー
{
}
ただし、structはインターフェースを実装できます。
C#public interface IPrintable
{
void Print();
}
public struct Point : IPrintable
{
public int X { get; set; }
public int Y { get; set; }
public void Print()
{
Console.WriteLine($"{X}, {Y}");
}
}
つまり、structはクラス継承には向いていませんが、インターフェースによる共通化は可能です。
3-6. structとclassの比較表
| 比較項目 | struct | class |
|---|---|---|
| 種類 | 値型 | 参照型 |
| 代入時の挙動 | 値がコピーされる | 参照がコピーされる |
| メソッド引数 | 基本的にコピーされる | 参照先のオブジェクトを操作できる |
| null | 通常は代入できない | 代入できる |
| 継承 | クラスや構造体の継承はできない | 継承できる |
| インターフェース実装 | できる | できる |
| 既定値 | 常に存在する | nullになり得る |
| 向いている用途 | 小さな値の表現 | 状態や振る舞いを持つオブジェクト |
structとclassは、どちらが優れているというものではありません。用途に応じて使い分けることが大切です。
4. structが「値型」と呼ばれる理由
structが値型と呼ばれる理由は、代入や引数渡しのときに、基本的に値そのものがコピーされるからです。
この仕組みを理解すると、structの挙動がかなりわかりやすくなります。
4-1. 値そのものがコピーされる仕組み
次のコードを見てください。
C#public struct Counter
{
public int Value { get; set; }
}
C#Counter c1 = new Counter { Value = 1 };
Counter c2 = c1;
c2.Value = 100;
Console.WriteLine(c1.Value); // 1
Console.WriteLine(c2.Value); // 100
c2 = c1と書いたとき、c1の中身がc2にコピーされます。
つまり、c1とc2は別々の値です。
これはintと同じです。
C#int a = 1;
int b = a;
b = 100;
Console.WriteLine(a); // 1
Console.WriteLine(b); // 100
structは、自分で定義できるintのような値型だと考えると理解しやすくなります。
4-2. コピーによって起きやすい初心者の勘違い
初心者がよく混乱するのは、「変更したはずなのに元の値が変わらない」というケースです。
C#static void Increment(Counter counter)
{
counter.Value++;
}
C#Counter c = new Counter { Value = 1 };
Increment(c);
Console.WriteLine(c.Value); // 1
Incrementメソッドの中ではcounter.Value++を実行しています。しかし、呼び出し元のc.Valueは変わりません。
なぜなら、メソッドに渡されたcounterはcのコピーだからです。
このような挙動は、classに慣れている人ほど混乱しやすいです。
4-3. ref・out・inを使った参照渡し
structをコピーせずにメソッドへ渡したい場合は、ref、out、inを使います。
refを使うと、元の変数を参照渡しできます。
C#static void Increment(ref Counter counter)
{
counter.Value++;
}
C#Counter c = new Counter { Value = 1 };
Increment(ref c);
Console.WriteLine(c.Value); // 2
refを使うと、メソッド内での変更が呼び出し元にも反映されます。
outは、メソッド内で値を設定して返すときに使います。
C#static void CreateCounter(out Counter counter)
{
counter = new Counter { Value = 10 };
}
C#CreateCounter(out Counter c);
Console.WriteLine(c.Value); // 10
inは、コピーを避けつつ、メソッド内で変更させたくないときに使います。
C#static void Print(in Counter counter)
{
Console.WriteLine(counter.Value);
}
inを使うと読み取り専用の参照として渡せるため、大きめのstructをコピーしたくない場合に役立つことがあります。
4-4. Nullable<T>でstructにnullを扱わせる方法
通常、structにはnullを代入できません。
C#int number = null; // エラー
DateTime date = null; // エラー
しかし、Nullable<T>を使うと、値型でもnullを表現できます。
C#Nullable<int> number = null;
より一般的には、?を使って次のように書きます。
C#int? number = null;
DateTime? date = null;
自作のstructでも同じです。
C#Point? point = null;
値があるかどうかはHasValueで確認できます。
C#int? number = 10;
if (number.HasValue)
{
Console.WriteLine(number.Value);
}
また、??演算子を使うと、nullの場合の既定値を指定できます。
C#int? number = null;
int result = number ?? 0;
Console.WriteLine(result); // 0
4-5. boxingとunboxingの基本
structは値型ですが、object型に代入すると参照型のように扱われます。この変換をboxingと呼びます。
C#int number = 10;
object obj = number; // boxing
このとき、値型のnumberが箱に入れられ、objectとして扱われます。
逆に、objectから元の値型へ戻すことをunboxingと呼びます。
C#object obj = 10;
int number = (int)obj; // unboxing
自作のstructでも同じです。
C#Point p = new Point { X = 1, Y = 2 };
object obj = p; // boxing
Point p2 = (Point)obj; // unboxing
boxingは便利ですが、余計なメモリ確保や変換コストが発生する場合があります。パフォーマンスを意識する場面では、不要なboxingを避けることが重要です。
5. structを使うメリット・デメリット
structにはメリットもありますが、使い方を間違えるとデメリットも大きくなります。
特に「値型だから常に速い」と考えるのは危険です。
5-1. structを使うメリット
structを使う主なメリットは、値としてシンプルに扱えることです。
たとえば、座標やサイズのような小さなデータは、値としてコピーされた方が自然です。
C#Point p1 = new Point { X = 10, Y = 20 };
Point p2 = p1;
p2.X = 99;
Console.WriteLine(p1.X); // 10
このように、コピー後に変更しても元の値に影響しません。
また、structは小さなデータを大量に扱う場面で、効率よく扱えることがあります。
たとえば、ゲーム開発で座標、ベクトル、色、範囲などを大量に扱う場合、適切に設計されたstructは有効です。
5-2. structを使うデメリット
structのデメリットは、コピーによる混乱やコストが発生しやすいことです。
structは代入や引数渡しのたびにコピーされるため、大きなデータを持つstructではコピーのコストが高くなる可能性があります。
C#public 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;
}
このような大きなstructを頻繁に渡すと、コピーの負担が増えます。
また、値がコピーされることを理解していないと、変更したつもりの値が反映されないというバグにつながります。
5-3. パフォーマンスが良くなるケース
structがパフォーマンス面で有利になることがあるのは、次のようなケースです。
小さなデータを表す場合です。
C#public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
このような小さくて不変な値は、structに向いています。
また、大量の小さな値を扱う場合にも、classより効率的になる可能性があります。classはインスタンスごとにヒープ上のオブジェクトを作り、参照を通じて扱います。一方、structは値として扱えるため、状況によってはメモリ効率が良くなります。
ただし、これは設計や利用方法によって変わります。
5-4. 逆にパフォーマンスが悪くなるケース
structを使うと必ず速くなるわけではありません。
たとえば、大きなstructを頻繁にコピーすると、かえって遅くなることがあります。
C#static void Process(LargeData data)
{
// dataはコピーされる
}
このようなメソッドを何度も呼び出すと、そのたびにLargeDataがコピーされます。
また、objectやインターフェースとして扱うことでboxingが発生すると、余計なメモリ確保が起きる場合があります。
C#int number = 10;
object obj = number; // boxing
structをパフォーマンス目的で使う場合は、コピーやboxingが発生していないかを意識する必要があります。
5-5. 大きすぎるstructを避けるべき理由
structは小さな値を表すのに向いています。
逆に、多くのフィールドを持つ大きなデータ構造には向いていません。
C#public struct UserData
{
public int Id;
public string Name;
public string Email;
public string Address;
public string PhoneNumber;
public DateTime CreatedAt;
public DateTime UpdatedAt;
}
このようなデータは、structよりclassの方が自然です。
理由は、ユーザー情報のようなデータは単なる値というより、状態を持つエンティティとして扱うことが多いからです。
また、大きなstructはコピーコストが高くなりやすく、思わぬパフォーマンス低下につながる可能性があります。
6. structを使うべき場面・classを使うべき場面
structとclassを使い分けるときは、「そのデータを値として扱いたいか」「オブジェクトとして扱いたいか」を考えると判断しやすくなります。
6-1. structが向いているケース
structが向いているのは、次のようなケースです。
値そのものとして扱いたいデータです。
C#public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
座標、サイズ、色、範囲、日付、金額などは、値として扱いやすいデータです。
たとえば、座標(10, 20)は、それ自体が1つの値です。誰かに共有されて状態が変化するオブジェクトというより、数値の組み合わせとして扱う方が自然です。
6-2. classが向いているケース
classが向いているのは、状態や振る舞いを持つオブジェクトを表したい場合です。
C#public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
}
ユーザー、注文、商品、予約、アカウントなどは、classで表すことが多いです。
これらは単なる値ではなく、システム内で一意性を持ったり、状態が変化したり、他のオブジェクトと関連したりします。
6-3. 座標・範囲・日付などstructに向く例
structに向いている代表例として、座標があります。
C#public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
範囲もstructに向いています。
C#public readonly struct IntRange
{
public int Start { get; }
public int End { get; }
public IntRange(int start, int end)
{
Start = start;
End = end;
}
public bool Contains(int value)
{
return Start <= value && value <= End;
}
}
日付や時刻も値として扱われることが多いため、DateTimeはstructとして定義されています。
金額も、金額と通貨を組み合わせた値として扱うならstructに向いている場合があります。
C#public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
6-4. ユーザー情報や注文情報はclassが向く理由
ユーザー情報や注文情報は、一般的にはclassが向いています。
C#public class Order
{
public int Id { get; set; }
public string CustomerName { get; set; }
public decimal TotalAmount { get; set; }
public DateTime OrderedAt { get; set; }
}
注文は、注文番号によって識別され、状態が変化することがあります。
たとえば、注文状態が「未処理」から「発送済み」に変わる場合があります。
C#public class Order
{
public int Id { get; set; }
public string Status { get; set; }
}
このようなデータは、値としてコピーされるより、同じオブジェクトを参照して状態を共有する方が自然です。
ユーザー情報も同じです。ユーザーIDを持ち、名前やメールアドレスが更新される可能性があります。このような一意性や状態変化を持つものは、classで表す方がわかりやすいです。
6-5. 判断に迷ったときの選び方
structとclassで迷った場合は、まずclassを選ぶのが安全です。
特に初心者のうちは、structのコピー挙動によるバグを避けるためにも、値として扱う明確な理由がある場合だけstructを使うとよいでしょう。
判断の目安は次のとおりです。
| 判断ポイント | struct向き | class向き |
|---|---|---|
| データの性質 | 値そのもの | 状態を持つオブジェクト |
| サイズ | 小さい | 大きい |
| 変更 | 不変にしやすい | 変更されることが多い |
| 一意性 | 重要ではない | 重要 |
| コピー | 自然 | 不自然 |
| 継承 | 不要 | 必要になる場合がある |
座標や範囲のように、コピーされても問題ない小さな値ならstructが向いています。
ユーザーや注文のように、同じものを参照して扱いたい場合はclassが向いています。
7. structで初心者がつまずきやすいポイント
structは便利ですが、初心者がつまずきやすいポイントがいくつかあります。
特に、コピーの仕組みと既定値の存在はしっかり理解しておきましょう。
7-1. 代入すると別物になる
structは代入するとコピーされます。
C#Point p1 = new Point { X = 1, Y = 2 };
Point p2 = p1;
p2.X = 100;
Console.WriteLine(p1.X); // 1
Console.WriteLine(p2.X); // 100
p1とp2は別物です。
classの感覚で「同じものを指している」と考えると、ここで混乱します。
structは値そのものがコピーされるため、代入後に片方を変更しても、もう片方には影響しません。
7-2. プロパティを変更したつもりが反映されない
structはプロパティ経由で扱うと、コピーが発生して思ったように変更できない場合があります。
たとえば、次のようなコードを考えます。
C#public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
public class Shape
{
public Point Position { get; set; }
}
次のコードはエラーになる場合があります。
C#Shape shape = new Shape();
shape.Position.X = 10;
なぜなら、shape.Positionで取得したPointは値型であり、プロパティから返されたコピーに対して変更しようとしているからです。
正しくは、一度変数に取り出して、変更してから戻します。
C#Point position = shape.Position;
position.X = 10;
shape.Position = position;
このように、structを可変にすると、プロパティやコレクションと組み合わせたときに扱いづらくなることがあります。
7-3. default値が自動で作られる
structには必ず既定値があります。
C#Point p = default;
このとき、各フィールドは型ごとの既定値になります。
C#public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
C#Point p = default;
Console.WriteLine(p.X); // 0
Console.WriteLine(p.Y); // 0
intは0、boolはfalse、参照型のフィールドはnullになります。
たとえば、次のようなstructでは注意が必要です。
C#public struct UserName
{
public string Value { get; }
public UserName(string value)
{
Value = value;
}
}
C#UserName name = default;
Console.WriteLine(name.Value); // null
コンストラクターで必ず値を設定する設計にしていても、defaultではコンストラクターを通らず既定値が作られることがあります。
そのため、structを設計するときはdefault状態でも問題が起きにくいように考える必要があります。
7-4. 引数に渡すとコピーされる
structをメソッドに渡すとコピーされます。
C#static void SetX(Point point)
{
point.X = 100;
}
C#Point p = new Point { X = 1, Y = 2 };
SetX(p);
Console.WriteLine(p.X); // 1
メソッドの中で変更しても、元のpは変わりません。
元の値を変更したい場合はrefを使います。
C#static void SetX(ref Point point)
{
point.X = 100;
}
C#Point p = new Point { X = 1, Y = 2 };
SetX(ref p);
Console.WriteLine(p.X); // 100
ただし、refを多用するとコードの見通しが悪くなる場合もあります。そもそも変更が必要なデータなら、classの方が適していることもあります。
7-5. 可変structがトラブルを生みやすい理由
可変structとは、作成後に値を変更できるstructのことです。
C#public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
このようなstructは一見便利ですが、コピーとの相性が悪いです。
C#Point p1 = new Point { X = 1, Y = 2 };
Point p2 = p1;
p2.X = 100;
この場合、p1は変わりません。値型としては正しい挙動ですが、慣れていないとバグの原因になります。
特に、プロパティやコレクションに入れた可変structを変更しようとすると、コピーを変更しているだけになることがあります。
そのため、structはできるだけ不変にするのが基本です。
C#public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
不変にしておけば、コピーされても値が変わることがないため、扱いやすくなります。
7-6. readonly structを使うべき場面
readonly structは、構造体全体を読み取り専用にするための機能です。
C#public 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に向いています。
C#public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
不変なstructにすると、コピーによる副作用を気にしなくてよくなります。
structを使うなら、まずはreadonly structで設計できないかを考えるとよいでしょう。
8. C# structの実践コード例
ここからは、実際に使えるstructのコード例を見ていきます。
座標、金額、メソッド付き構造体、readonly struct、classとの違いがわかる例を順番に紹介します。
8-1. 座標を表すPoint構造体の例
まずは、座標を表すPoint構造体です。
C#public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
使い方は次のとおりです。
C#Point point = new Point
{
X = 10,
Y = 20
};
Console.WriteLine($"X: {point.X}, Y: {point.Y}");
ただし、このPointは可変です。より安全にするなら、読み取り専用にします。
C#public readonly 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($"X: {point.X}, Y: {point.Y}");
座標は値として扱うのが自然なので、structに向いている例です。
8-2. 金額を表すMoney構造体の例
金額を表すMoney構造体を作ってみます。
C#public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
public override string ToString()
{
return $"{Amount} {Currency}";
}
}
使い方は次のとおりです。
C#Money price = new Money(1200m, "JPY");
Console.WriteLine(price); // 1200 JPY
金額は、数値だけでは不十分な場合があります。1200という数値だけでは、日本円なのかドルなのかわかりません。
そこで、AmountとCurrencyをまとめてMoneyとして扱うと意味が明確になります。
C#Money yen = new Money(1200m, "JPY");
Money dollar = new Money(1200m, "USD");
このように、複数の値を組み合わせて1つの意味を持つ小さな型を作るとき、structは便利です。
8-3. メソッド付きstructの例
structにはメソッドも定義できます。
次の例では、範囲を表すIntRange構造体に、値が範囲内かどうかを判定するContainsメソッドを持たせています。
C#public readonly struct IntRange
{
public int Start { get; }
public int End { get; }
public IntRange(int start, int end)
{
Start = start;
End = end;
}
public bool Contains(int value)
{
return Start <= value && value <= End;
}
}
使い方は次のとおりです。
C#IntRange range = new IntRange(10, 20);
Console.WriteLine(range.Contains(15)); // true
Console.WriteLine(range.Contains(25)); // false
データと、そのデータに関係する処理を同じ型にまとめることで、コードの意味がわかりやすくなります。
8-4. readonly structの例
readonly structは、作成後に値を変更できない構造体を作りたいときに便利です。
C#public readonly 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;
}
}
C#Size size = new Size(10, 20);
Console.WriteLine(size.Area()); // 200
WidthとHeightにはsetがないため、作成後に変更できません。
C#// size.Width = 30; // エラー
このような不変の設計にすると、値型のコピーによる混乱を減らせます。
structを使う場合は、まずreadonly structで表現できないか考えるのがおすすめです。
8-5. structとclassで挙動が変わるコード例
最後に、structとclassで挙動が変わる例を見てみましょう。
まずはstructです。
C#public struct CounterStruct
{
public int Value { get; set; }
}
C#CounterStruct a = new CounterStruct { Value = 1 };
CounterStruct b = a;
b.Value = 10;
Console.WriteLine(a.Value); // 1
Console.WriteLine(b.Value); // 10
structでは、代入時に値がコピーされるため、aとbは別物です。
次にclassです。
C#public class CounterClass
{
public int Value { get; set; }
}
C#CounterClass a = new CounterClass { Value = 1 };
CounterClass b = a;
b.Value = 10;
Console.WriteLine(a.Value); // 10
Console.WriteLine(b.Value); // 10
classでは、代入時に参照がコピーされるため、aとbは同じオブジェクトを指しています。
この違いを理解することが、C#のstructとclassを使い分ける第一歩です。
9. structの応用機能
C#のstructには、基本的な構造体以外にも便利な応用機能があります。
ここでは、record struct、readonly record struct、ref struct、インターフェース実装、演算子オーバーロードについて紹介します。
9-1. record structとは
record structは、値の比較や表示に便利な機能を持つ構造体です。
C#public record struct Point(int X, int Y);
このように短く書くだけで、XとYを持つ構造体を定義できます。
C#Point p1 = new Point(10, 20);
Point p2 = new Point(10, 20);
Console.WriteLine(p1 == p2); // true
Console.WriteLine(p1); // Point { X = 10, Y = 20 }
通常のstructでは、表示用のToStringや比較処理を自分で書くことがあります。record structを使うと、値を中心に扱う型を簡潔に定義できます。
ただし、record structは既定では可変です。
C#public record struct Point(int X, int Y);
この場合、プロパティの値を変更できます。
C#Point p = new Point(10, 20);
p.X = 99;
不変にしたい場合は、readonly record structを検討します。
9-2. readonly record structとは
readonly record structは、読み取り専用のrecord structです。
C#public readonly record struct Point(int X, int Y);
値を簡潔に表現しつつ、不変にしたい場合に便利です。
C#Point p1 = new Point(10, 20);
Point p2 = new Point(10, 20);
Console.WriteLine(p1 == p2); // true
Console.WriteLine(p1); // Point { X = 10, Y = 20 }
readonly record structは、座標、サイズ、範囲、ID、金額など、値として扱いたいデータに向いています。
C#public readonly record struct UserId(int Value);
public readonly record struct ProductCode(string Value);
public readonly record struct Money(decimal Amount, string Currency);
このように、値オブジェクトを簡潔に定義したいときにも使いやすい機能です。
9-3. ref structとは
ref structは、通常のstructよりも制約が強い特殊な構造体です。
C#public ref struct BufferReader
{
private Span<byte> _buffer;
public BufferReader(Span<byte> buffer)
{
_buffer = buffer;
}
}
ref structは、スタック上で安全に扱う必要があるデータを表すために使われます。
代表的な例として、Span<T>があります。
C#Span<int> numbers = stackalloc int[3] { 1, 2, 3 };
ref structには、通常のstructより多くの制限があります。たとえば、通常のクラスのフィールドとして保持できなかったり、boxingできなかったりします。
初心者が日常的に使う機会は多くありませんが、高性能な処理やメモリ効率を意識する場面で登場します。
9-4. interfaceを実装するstruct
structはクラスを継承できませんが、インターフェースは実装できます。
C#public interface IPrintable
{
void Print();
}
C#public struct Point : IPrintable
{
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();
インターフェースを使うと、複数の型に共通の操作を持たせることができます。
ただし、structをインターフェース型として扱うとboxingが発生する場合があります。
C#IPrintable printable = point; // boxingが発生する場合がある
printable.Print();
パフォーマンスを意識する場合は、この点に注意が必要です。
9-5. structで演算子オーバーロードを使う方法
structでは、演算子オーバーロードを定義できます。
たとえば、金額を表すMoney構造体で足し算をできるようにしてみます。
C#public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
public static Money operator +(Money left, Money right)
{
if (left.Currency != right.Currency)
{
throw new InvalidOperationException("通貨が異なるため加算できません。");
}
return new Money(left.Amount + right.Amount, left.Currency);
}
public override string ToString()
{
return $"{Amount} {Currency}";
}
}
使い方は次のとおりです。
C#Money a = new Money(1000m, "JPY");
Money b = new Money(500m, "JPY");
Money total = a + b;
Console.WriteLine(total); // 1500 JPY
演算子オーバーロードを使うと、値型を自然な書き方で扱えるようになります。
ただし、わかりにくい演算子を定義するとコードの可読性が下がります。直感的に意味が伝わる場合に限定して使うのがおすすめです。
10. C# structに関するよくある質問
ここでは、C#のstructについて初心者が疑問に感じやすいポイントをQ&A形式で整理します。
10-1. structはいつ使えばいい?
structは、小さくて、値そのものとして扱いたいデータに向いています。
たとえば、次のようなものです。
C#public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
座標、サイズ、範囲、日付、金額、IDなどはstructに向いていることがあります。
逆に、ユーザー、注文、商品、アカウントのように、一意性や状態変化を持つものはclassを選ぶことが多いです。
10-2. structとclassはどちらを使うべき?
迷った場合は、まずclassを使うのが無難です。
structは値型であり、代入や引数渡しでコピーされます。この挙動を理解していないと、思わぬバグにつながることがあります。
structを選ぶのは、次の条件に当てはまる場合です。
| 条件 | struct向きか |
|---|---|
| 小さなデータである | 向いている |
| 値として扱いたい | 向いている |
| 不変にできる | 向いている |
| 継承が不要 | 向いている |
| 頻繁に状態変更する | class向き |
| 一意性が重要 | class向き |
「値として自然かどうか」を基準に考えると判断しやすくなります。
10-3. structにコンストラクターは書ける?
structにもコンストラクターを書けます。
C#public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
ただし、コンストラクターではすべてのフィールドや自動プロパティを初期化する必要があります。
C#public Point(int x)
{
X = x;
// Yが初期化されていないためエラー
}
また、defaultで作成される場合は、各メンバーが既定値になります。
C#Point p = default;
この点はclassとの違いとして押さえておきましょう。
10-4. structはnullにできる?
通常のstructはnullにできません。
C#int number = null; // エラー
Point point = null; // エラー
ただし、?を使うとnullを扱えます。
C#int? number = null;
Point? point = null;
DateTime? date = null;
これはNullable<T>の省略記法です。
C#Nullable<int> number = null;
値があるかどうかはHasValueで確認できます。
C#int? number = 10;
if (number.HasValue)
{
Console.WriteLine(number.Value);
}
10-5. structは継承できる?
structは他のstructやclassを継承できません。
C#public struct Child : Parent // エラー
{
}
ただし、インターフェースは実装できます。
C#public interface IPrintable
{
void Print();
}
public struct Point : IPrintable
{
public int X { get; set; }
public int Y { get; set; }
public void Print()
{
Console.WriteLine($"{X}, {Y}");
}
}
継承を使って機能を拡張したい場合は、classを使うのが基本です。
10-6. structはパフォーマンス改善に必ず役立つ?
structを使えば必ずパフォーマンスが良くなるわけではありません。
小さくて不変な値を大量に扱う場合は、structが有効なことがあります。
しかし、大きなstructを頻繁にコピーすると、逆にパフォーマンスが悪くなる可能性があります。
C#static void Process(LargeData data)
{
// LargeDataがコピーされる
}
また、objectやインターフェースとして扱うことでboxingが発生する場合もあります。
C#int number = 10;
object obj = number; // boxing
パフォーマンス目的でstructを使う場合は、コピー、boxing、データサイズ、利用頻度を考慮する必要があります。
初心者のうちは、「structは速いから使う」と考えるのではなく、「値として扱うのが自然だから使う」と考えるのがおすすめです。
まとめ
C#のstructは、値型を定義するための仕組みです。
int、bool、DateTimeなどの基本的な型もstructとして扱われており、C#の型システムを理解するうえで欠かせない存在です。
structとclassの最大の違いは、structが値型、classが参照型であることです。
structは代入やメソッド引数で基本的に値がコピーされます。そのため、コピー後に片方を変更しても、もう片方には影響しません。
一方、classは参照型なので、代入すると同じオブジェクトを参照します。片方からオブジェクトを変更すると、もう片方にもその変更が見えます。
structは、座標、サイズ、範囲、日付、金額、IDなど、小さくて値として扱いたいデータに向いています。特に、不変にできるデータはreadonly structとして定義すると安全に扱いやすくなります。
ただし、structを使えば必ずパフォーマンスが良くなるわけではありません。大きなstructを頻繁にコピーすると、かえって効率が悪くなることがあります。また、boxingが発生する場面にも注意が必要です。
初心者がstructを使うときは、次のポイントを意識しましょう。
| ポイント | 内容 |
|---|---|
| structは値型 | 代入や引数渡しでコピーされる |
| classは参照型 | 同じオブジェクトを参照する |
| structはnullにできない | ?を使えばnullを扱える |
| structは継承できない | インターフェースは実装できる |
| 大きなstructは避ける | コピーコストが高くなる可能性がある |
| できるだけ不変にする | readonly structが便利 |
C#でstructを正しく使いこなすには、まず「値型としてコピーされる」という仕組みを理解することが大切です。
classとの違いを押さえたうえで、値として自然に扱える小さなデータにはstruct、状態や一意性を持つオブジェクトにはclassを使うようにすると、読みやすく安全なC#コードを書けるようになります。

