C#を学ぶにあたって、構造体を深く学んでみようと思い備忘録的に記事にいたします。
構造体(struct)とは
C#には、機能も書き方もクラスとよく似た構造体というものがあり、クラスと同じく new
演算子を使ってそのオブジェクトを生成します。
C# では struct で定義します。
struct MyStruct
{
public int X { get; set; }
public int Y { get; set; }
}
MyStruct myStruct = new MyStruct();
Console.WriteLine(myStruct.X); // 0
使い方もクラスとよく似ており、構造体にもプロパティやメソッドが存在します。
それらをクラスと同じように使うことができます。
構造体とクラスの違い
それでは、構造体とクラスは何が違うのか?ですが、その大きな違いはメモリ上のオブジェクトの持ち方です。
クラスの場合
クラスの場合は、メモリの「ヒープ領域」と呼ばれる場所に割り当てられます。
そして、参照型です。
(参照型については後述いたします)
構造体の場合
構造体の場合は、「スタック領域」と呼ばれる場所に割り当てられます。
そして、値型です。
(値型については後述いたします)
これがクラスと構造体の大きな違いです。
その他、クラスと構造体の違い
先程のメモリ領域以外にも異なる点がございます。
一覧でまとめると以下のようになります。
# | 項目 | 構造体 | クラス |
---|---|---|---|
1 | オブジェクトの型 | 値型 | 参照型 |
2 | 具象クラスの継承 | 不可 | 可能 |
3 | interfaceの継承 | 可能 | 可能 |
4 | abstructの継承 | 不可 | 可能 |
5 | デフォルトコンストラクタ(引数なし)の定義 | 不可 | 可能 |
6 | デストラクタの定義 | 不可 | 可能 |
このように、構造体(struct)は制限が多くあります。
クラスに比べて制限の多い構造体ですが、軽量のオブジェクトを表すのに適していると言われていますので適宜使い分けが必要です。
※2022/01/30追記
C#10.0から構造体に対する引数なしコンストラクターの定義が開放されたようです。
https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/builtin-types/struct#parameterless-constructors-and-field-initializers
ヒープ領域とスタック領域
ヒープ領域とスタック領域について解説いたします。
ヒープ領域とは
ヒープ領域とは、プログラムの実行時に動的に確保されるメモリの領域で、このヒープ領域は、領域を確保すると破棄されるまでずっとメモリの領域を掴み続けます。
C#では、クラスのインスタンス化を行うためにnew演算子を活用しますが、これはヒープ領域から動的メモリを割り当てています。
ヒープ領域には、インスタンスや実際の値、そして静的領域(static)などが格納されてます。
スタック領域とは
スタック領域とは、主にプログラムの実行を制御する領域で、メソッドなどが格納されます。
あとローカル変数は、すべてスタック領域に格納されます。
スタック領域に格納された処理は、上から順番に実行され、実行済みの処理は破棄されていきます。
値型と参照型
さきほど、クラスは参照型、構造体は値型と申しましたが、こちらについて解説いたします。
値型とは
C#で値型は構造体以外にも、「int / decimal / enum / char/ bytes / etc...」などが該当します。
この値型は、実際の値(データ)を変数に格納します。
参照型とは
参照型で代表されるのが、クラスです。
クラスの他にはstring型や配列、objectなどが該当します。
メモリのアドレス番号(番地)が16進数で振り分けられてろ、参照型ではこのアドレス番号(番地)を参照しています。
実際の値はヒープ領域に格納されています。
値型の動きと参照型の動き
値型を実行した場合の違いを説明いたします。
値型
まずは値型からです。
以下のコードがあったとします。
// 値型なので「struct」
struct MyPoint
{
public int X, Y;
public MyPoint(int x, int y)
{
this.X = x;
this.Y = y;
}
}
public class Program
{
public static void Main()
{
MyPoint a = new MyPoint(10, 20);
MyPoint b = a;
Console.WriteLine("a: ({0}, {1})", a.X, a.Y);
Console.WriteLine("b: ({0}, {1})", b.X, b.Y);
a.X = 80;
Console.WriteLine("a: ({0}, {1})", a.X, a.Y);
Console.WriteLine("b: ({0}, {1})", b.X, b.Y);
}
}
このコードの実行結果は以下の通りです。
a: (10, 20)
b: (10, 20)
a: (80, 20)
b: (10, 20)
値型の場合、変数の数だけ値が存在しているので、1つの値を書き換えても他の変数には影響がありません。
参照型
続いては、参照型を見てみます。
// 参照型なので「class」
class MyPoint
{
public int X, Y;
public MyPoint(int x, int y)
{
this.X = x;
this.Y = y;
}
}
public class Program
{
public static void Main()
{
MyPoint a = new MyPoint(10, 20);
MyPoint b = a;
Console.WriteLine("a: ({0}, {1})", a.X, a.Y);
Console.WriteLine("b: ({0}, {1})", b.X, b.Y);
a.X = 80; // b.X = 80 でも結果は同じ
Console.WriteLine("a: ({0}, {1})", a.X, a.Y);
Console.WriteLine("b: ({0}, {1})", b.X, b.Y);
}
}
先程の値型と違うのは、structかclassかだけの違いです。
他のコードは全て一緒です。
では、このコードを実行してみます。
すると結果は以下のようになります。
a: (10, 20)
b: (10, 20)
a: (80, 20)
b: (80, 20)
値型の時とは違う結果になりました。
なぜでしょうか?
参照型の場合、変数が違っていたとしても、参照元が同じケースだと、変数はメモリのアドレスしか見ていないからです。
図にするとこのようなイメージです。
なぜ値型と参照型が必要なのか?
これには大きな理由があります。
それは、実行効率とメモリ効率に大きく関わっています。
大きなサイズのオブジェクト型だった場合 、それら全てが値型だと、変数の代入をするたびにオブジェクトの中身のコピー処理が行われてしまいます。
いっぽう、参照型の場合は、参照(アドレス)をコピーするだけで済むので効率がとても良くなります。
しかし、小さなオブジェクト型の場合 は、参照型にすると、参照(アドレス)を格納する領域と、オブジェクトそのものを格納する領域の2つが必要になり、メモリ効率上よろしくありません。
この場合は、値型が有利になります。
つまり、大きなサイズのオブジェクトは参照型が有利 で 小さなオブジェクト型は値型が有利 となります。
Playground
C# のPlayGroundがあり気軽に動かしながら学ぶことができます。
まとめ
- 構造体(struct)とは、値型でありスタック領域を使用している
- スタック領域は、上から順番に実行されると破棄される
- ヒープ領域は、アドレス(番地)を参照して値を見ている
- 値型は、変数ごとに値を保持しており、他の変数への干渉はない
- 参照型は、同じアドレス(番地)を見ている場合は、それらの変数への干渉がある
- 小さなオブジェクトの場合は、値型が効率がよい
- 大きなオブジェクトの場合は、参照型が効率がよい
以上となります。
ご拝読有難うございました。