概要
「データの構造化」で少し触れて以来、ずっとクラスだけを使って説明してきましたが、ここで、C#における「もう1つの複合型」である構造体について説明します。
根本的な差は、次項で説明する「値型」か「参照型」かの違いに起因します。構造体は値型で、そのためにクラスと比べていくつか用途に制限がかかります。
構造体の制限
とりあえず、クラスと構造体を並べて書いてみましょう。
構造体
public struct SampleStruct : InterfaceA, InterfaceB
{
public int A { get; }
public int B { get; }
public SampleStruct(int a, int b) { A = a; B = b; }
public static SampleStruct operator-(SampleStruct x)
=> new SampleStruct(-x.A, -x.B);
}
public interface InterfaceA { int A { get; } }
public interface InterfaceB { int B { get; } }
クラス
public sealed class SampleClass : BaseClass, InterfaceA, InterfaceB
{
public int A { get; set; }
public int B { get; set; }
public SampleClass() { }
public SampleClass(int a, int b) { A = a; B = b; }
public static SampleClass operator -(SampleClass x)
=> new SampleClass(-x.A, -x.B);
public override void X() { }
~SampleClass() { }
}
public class BaseClass
{
public virtual void X() { }
}
public static class StaticClass
{
public static string Hex(int x) => x.ToString("X");
}
単純に、クラスの方ができることは多いです。
クラスにしかできないことは以下の通り。
-
他のクラスから派生(他のクラスを継承)する
- 継承に関連する修飾子(
abstract
,sealed
,virtual
,override
など)を使えるもクラスだけ
- 継承に関連する修飾子(
- 静的クラス
- 引数なしのコンストラクターの定義(C# 9.0 まで)
- デストラクターの定義
一方、クラス・構造体どちらでもできることは以下のとおりです。
- 引数なしコンストラクターとデストラクター以外のメンバー定義
- インターフェイスの実装(複数も可)
- (構造体自身には
static
修飾子を付けれないものの)静的メンバーの定義自体は可能
構造体の用途
「できること」でいうと、構造体はクラスの完全下位互換で、メリットがないように見えます。構造体の利点を理解するには、次項の値型と参照型についての知識が必要になります。
おおまかに言うと、
- クラスと構造体ではメモリの使い方が違う
-
小さなデータ構造に対しては構造体が有利
- 使い方にもよりますが、大まかなガイドラインとしては16バイト程度が境界と言われています
というものです。
この性質と、前節で説明した制限とを併せて考えると、構造体の利用を検討するのは、
- データ構造が16バイト未満
- 継承が必要ない
という状況下になります。
構造体の既定値
これも値型の性質になりますが、
クラス(new
するまでメモリ領域を確保しない)と違って、
構造体は宣言した時点でデータを記録するためのメモリ領域が確保されます。
クラス型のフィールドの場合は、new
するなり他のインスタンスを代入するなりして初期化するまでの間、
null
(何のインスタンスも指していない状態)が入ります。
一方、構造体の場合、いわゆる「0初期化」状態になっています。 全てのメンバーに対して、0、もしくはそれに類する以下のような値が入ります。
-
数値型(
int
,double
など)の場合は0- 列挙型も、0 に相当する値
bool
型の場合はfalse
- 参照型(
string
、配列、クラス、デリゲートなど)やNull許容型の場合はnull
これら、0初期化状態にある値を、構造体の既定値(default value)と呼びます。
using System;
struct Sample
{
public int I;
public double D;
public bool B;
public string S;
}
public class Program
{
static Sample s;
static void Main(string[] args)
{
Console.WriteLine(s.I);
Console.WriteLine(s.D);
Console.WriteLine(s.B);
Console.WriteLine(s.S);
}
}
0
0
False
引数なしコンストラクター
C# 9.0 まで、構造体のメンバーとして引数なしのコンストラクターを書くことはできませんでした。
これは、new T()
を既定値(0初期化)として使っていたせいです。
例えば以下のコードでは、Point
クラスには引数なしのコンストラクターを定義していませんが、
new Point()
という書き方で 0 初期化を行っています。
using System;
struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
public override string ToString() => $"({X}, {Y})";
}
public class Program
{
static void Main(string[] args)
{
var p1 = new Point(); // 既定値、つまり、「XもYも0に初期化」という意味で使われる
var p2 = new Point(10, 20);
var p3 = default(Point); // C# 2.0~9.0 まで、p1と同じ意味
Console.WriteLine(p1);
Console.WriteLine(p2);
Console.WriteLine(p3);
}
}
(0, 0)
(10, 20)
(0, 0)
Ver. 2.0
ちなみに、C# 2.0 以降では、構造体の既定値は、new T()
という書き方の他に、default(T)
という書き方もできます。
(主にジェネリックのために導入された構文です。)
既定値について、詳しくは別項「既定値」で説明します。
Ver. 10
C# 2.0 で default(T)
を使った既定値(0初期化)ができるようになって、
「new T()
と書く場合は引数なしコンストラクターを呼ぶ」という仕様に変えたい
(構造体にも引数なしコンストラクターを書けるようにして、new T()
と default(T)
を区別する)
という話は前々からありました。
C# 10.0 で、ついにその案が採用されることになり、 引数なしコンストラクターを書けるようになりました。 例えば以下のようなコードが書けるようになります。
struct A
{
public int X;
public A() => X = 1;
}
これで、new A()
で X
が1になります。
new() と default
背景説明の通り、new()
と default
の意味が変わったので注意が必要です。
この例の構造体 A
の場合、以下のような挙動になります。
Console.WriteLine(new A().X); // コンストラクターが呼ばれて、X == 1 になってる。
Console.WriteLine(default(A).X); // コンストラクターも呼ばれず 0 初期化で、X == 0 になってる。
C# 7.1/9.0 で、new()
や default
にターゲット型からの推論が働くようになったので、以下のようにも書けます。
A a = new();
Console.WriteLine(a.X); // 1
a = default;
Console.WriteLine(a.X); // 0
default
を書く以外に、配列の要素も既定値(0初期化)になるので注意が必要です。
// 配列の要素は暗黙的に default…
Console.WriteLine((new A[1])[0].X); // default(A) と同じ扱いで、X == 0 になってる。
ちなみに、ジェネリクス越しでも new()
と default
の呼び分けが掛かります。
Console.WriteLine(New<A>().X); // 1
Console.WriteLine(Default<A>().X); // 0
static T New<T>() where T : new() => new();
static T? Default<T>() => default;
また、これまで default
と同じ意味だった new()
が、引数なしコンストラクターの有無で違う意味になるのでこの点にも注意が必要です。
例えば、一般の構造体でオプション引数を使いたい場合、
既定値しか使えません。
引数なしコンストラクターがない場合には new()
も既定値扱いですが、
ある場合には new()
を渡せなくなります。
void m(
NoCtor n1 = new(),
NoCtor n2 = default,
Ctor c1 = new(), // この行だけコンパイル エラー
Ctor c2 = default
)
{ }
struct NoCtor { }
struct Ctor { public Ctor() { } }
フィールド初期化子
C# 10.0 で構造体に引数なしコンストラクターが使えるようになったことに伴って、 フィールド初期化子も使えるようになりました。 以下のようなコードは C# 10.0 から書けるようになります。
struct FieldInitializer
{
public int X = 1;
public int Y = 2;
public FieldInitializer() { }
}
new()
だけで、X
、Y
の値がそれぞれ1、2に初期化されます。
var f = new FieldInitializer();
Console.WriteLine(f.X); // 1
Console.WriteLine(f.Y); // 2
(※ 初期案では、明示的なコンストラクター定義もなしでフィールド初期化子を書けるようにする予定でした。 この際、フィールド初期化子を書くとコンパイラーが引数なしコンストラクターを生成していました。 C# 10 リリース当初はその案に基づいた実装になっていましたが、 ちょっと問題があって撤回され、明示的にコンストラクターを書かなければならなくなりました。)
引数なしコンストラクターのアクセシビリティ
new()
が default
と同じ意味になるのか、
それとも引数なしコンストラクターの呼び出しになるのか紛らわしくなるので、
構造体の引数なしコンストラクターは public 以外を認めていません。
struct A
{
public int X;
private A() => X = 0; // エラー
}
struct B
{
public int X;
internal B() => X = 0; // エラー
}
確実な初期化
※ C# 10 までの仕様になります。
new T()
やdefault(T)
で作る「既定値」とは違って、
引数付きのコンストラクターを使う場合は、コンストラクター内で全てのメンバーをきっちり自分の手で初期化する必要がありました。
例えば、以下のコードは、コンストラクター内で _z
の初期化を忘れているのでコンパイル エラーになっていました。
struct Sample
{
int _x;
int _y;
int _z;
public Sample(int x, int y)
{
_x = x;
_y = y;
// C# 10 以前はコンパイル エラー
}
}
(クラスの場合はこういう制限はなく、明示的に初期化しなかったフィールドは既定値(0)で初期化されます。)
また、全てのフィールドを初期化するまで、プロパティやメソッドなどの関数メンバーを呼べないという制約もありました。
struct Sample
{
int _x;
int _y;
public Sample(int x, int y)
{
M(); // エラー: _x, _y の初期化より前に呼んじゃダメ。
_x = x;
_y = y;
M(); // この順ならOK。
}
void M() { }
}
(同じくクラスの場合は制限はなし。既定値(0)が使われるだけ。)
構造体のフィールドの既定値初期化
Ver. 11.0
C# 11 では、構造体でもフィールドの明示的な初期化が不要になりました。 (クラスと構造体の差が1つなくなりました。)
前節のコードとほぼ同じですが、 C# 11 にすれば以下のようなコードがコンパイルできるようになります。
struct Sample
{
int _x;
int _y;
int _z;
public Sample(int x, int y)
{
M(); // C# 11 では初期化よりも先に読んでも平気。_x, _y にもこの時点でいったん 0 が入ってる。
_x = x;
_y = y;
// C# 11 では _z に 0 が自動で入る。
}
void M() => Console.WriteLine($"{_x}, {_y}, {_z}");
}
自動プロパティの扱い変更
Ver. 6
前節の「確実な初期化」と絡んで、C# 5.0までのC#では、自動プロパティの初期化が非常に面倒でした。
C# 5.0 以前の場合、以下のコードはコンパイル エラーを起こします。
public struct Point
{
public int X { get; private set; }
public int Y { get; private set; }
public Point(int x, int y)
{
// C# 5.0 まではエラーになる
X = x;
Y = y;
}
}
エラーを起こす原因は、以下の組み合わせのせいです。
- 自動プロパティを定義すると、コンパイラーが対応するフィールド(バック フィールド)を作る
- 構造体の制約のせいで、バック フィールドが初期化されるまで、プロパティの読み書きできない
- でも、自動プロパティの場合、プロパティを介さずにバック フィールドを初期化する方法がない
このせいで、構造体と自動プロパティは相性が悪く、以下のように、自動プロパティを使わない書き方に書き換える必要がありました。
public struct Point
{
private int _x;
public int X { get { return _x; } }
private int _y;
public int Y { get { return _y; } }
public Point(int x, int y)
{
_x = x;
_y = y;
}
}
これに対して、C# 6では、最初のコードがコンパイルできるようになっています。 C#の仕様書に以下の1文が追加されたことによります。
- 自動プロパティを型の中から使う場合、そのバック フィールドに対する読み書きに置き換える
この仕様が追加されたことで、先ほどのコードはバック フィールドの初期化と見なされ、構造体の制約に引っかからなくなりました。
ちなみに、C# 6の場合は get のみの自動プロパティ(get-only auto-property)という構文が追加されて、先ほどのコードはさらに、以下のように書けるようになりました。
public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y)
{
X = x;
Y = y;
}
}
メンバー毎コピー、メンバー毎比較
構造体の変数への代入は、全メンバーのコピーになります。
また、構造体には自動的にEquals
メソッドが実装されて、メンバー毎の比較(全メンバー一致の場合に一致)になります。
using System;
public class Program
{
public struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
public override string ToString() => $"({X}, {Y})";
}
static void Main(string[] args)
{
var x = new Point(1, 2);
var y = x;
Console.WriteLine(y); // x のメンバー毎コピー = (1, 2)
// メンバー毎比較(全メンバー一致なら一致)
Console.WriteLine(x.Equals(new Point(1, 2))); // true
Console.WriteLine(x.Equals(new Point(1, 3))); // false
}
}
構造体に対する特別な修飾子
ここでは紹介だけになりますが、構造体にだけ付けることができる特別な修飾子があります。
詳細についてはそれぞれリンク先を参照してください。
ちなみに、現状では、ref
には語順に制約があって、
struct
もしくはpartial
の直前に来る必要があります(緩和も検討されています)。
要するに、readonly ref struct
はOKですが、ref readonly struct
はエラーになります。
いくつか実例を挙げます。
// OK
readonly public ref struct Ok1 { }
readonly public ref partial struct Ok2 { }
public readonly ref partial struct Ok3 { }
// コンパイル エラー
ref readonly struct Ng1 { }
readonly ref public struct Ng2 { }
readonly public partial ref struct Ng3 { }
public ref readonly partial struct Ng4 { }
public ref partial readonly struct Ng5 { }
おそらく、以下のような型の入れ子とメソッド定義の区別を楽にするための制限(あくまでコンパイラー都合)と思われます。
class Sample
{
// 以下のエラー行、エラー内容は「readonly の後ろには型名が必要」になる
ref readonly struct InnerStruct { }
ref readonly int Method(in int x) => ref x;
}