C# の型とメンバー
C# は、オブジェクト指向言語として、カプセル化、継承、およびポリモーフィズムの概念をサポートしています。 クラスは 1 つの親クラスから直接継承でき、任意の数のインターフェイスを実装できます。 親クラスの仮想メソッドをオーバーライドする場合、誤って再定義しないように、override
キーワードを指定する必要があります。 C# の構造体はコンパクトなクラスのようなものです。インターフェイスを実装できるスタック割り当て型ですが、継承はサポートされていません。 C# には record class
と、主にデータ値を格納する目的の型の record struct
型があります。
クラスとオブジェクト
"クラス" は C# の最も基本的な型です。 クラスは、状態 (フィールド) とアクション (メソッドおよびその他の関数メンバー) を 1 つの単位としてまとめたデータ構造です。 クラスには、オブジェクトとも呼ばれる、クラスのインスタンスの定義があります。 クラスでは、"継承"と "ポリモーフィズム" をサポートします。これによって "派生クラス" が "基底クラス" を拡張して特殊化できます。
新しいクラスはクラス宣言を使用して作成されます。 クラス宣言は、ヘッダーで始まります。 ヘッダーでは次を指定します。
- クラスの属性と修飾子
- クラスの名前
- 基底クラス (基底クラスから継承する場合)
- このクラスによって実装されるインターフェイス。
ヘッダーの後にはクラス本体が続きます。これは、区切り記号 {
と }
の間に記述するメンバー宣言のリストで構成されます。
次のコードは、Point
という名前の単純なクラスの宣言を示しています。
public class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}
クラスのインスタンスは new
演算子を使用して作成されます。この演算子は新しいインスタンスのメモリを割り当て、コンストラクターを呼び出してインスタンスを初期化し、インスタンスへの参照を返します。 次のステートメントは、2 つの Point
オブジェクトを作成し、2 つの変数に、それらのオブジェクトへの参照を格納します。
var p1 = new Point(0, 0);
var p2 = new Point(10, 20);
オブジェクトで占有されたメモリは、そのオブジェクトに到達できなくなると自動的に解放されます。 C# では、オブジェクトの割り当てを明示的に解除する必要がなく、また解除することもできません。
型パラメーター
ジェネリック クラスでは "型パラメーター" を定義します。 型パラメーターは、山かっこで囲まれた型パラメーターの一連の名前です。 型パラメーターは、クラス名の後にあります。 これで、クラスのメンバーを定義するクラス宣言の本体で型パラメーターを使用できます。 次の例では、Pair
の型パラメーターは TFirst
と TSecond
です。
public class Pair<TFirst, TSecond>
{
public TFirst First { get; }
public TSecond Second { get; }
public Pair(TFirst first, TSecond second) =>
(First, Second) = (first, second);
}
型パラメーターを受け取るために宣言されるクラス型は、"ジェネリック クラス型" と呼ばれます。 構造体、インターフェイス、およびデリゲートの型もジェネリックです。 ジェネリック クラスを使用する場合は、それぞれの型パラメーターの型引数を指定する必要があります。
var pair = new Pair<int, string>(1, "two");
int i = pair.First; //TFirst int
string s = pair.Second; //TSecond string
上記の Pair<int,string>
のような、型引数が指定されたジェネリック型は "構築された型" と呼ばれます。
基底クラス
クラスの宣言で、基底クラスを指定する場合もあります。 クラス名と型パラメーターの後には、コロンと基底クラスの名前を指定します。 基底クラスの指定の省略は、object
型からの派生と同じです。 次の例の Point3D
の基底クラスは Point
です。 最初の例の Point
の基底クラスは object
です。
public class Point3D : Point
{
public int Z { get; set; }
public Point3D(int x, int y, int z) : base(x, y)
{
Z = z;
}
}
クラスは、その基底クラスのメンバーを継承します。 継承は、クラスにその基底クラスのほぼすべてのメンバーが暗黙的に含まれることを意味します。 クラスは、インスタンス コンストラクター、静的コンストラクター、およびファイナライザーを継承しません。 派生クラスでは、継承するメンバーに新しいメンバーを追加できますが、継承されたメンバーの定義を削除することはできません。 前述の例では、Point3D
は、Point
から X
メンバーと Y
メンバーを継承しており、各 Point3D
インスタンスには、X
、Y
、Z
の 3 つのプロパティが含まれています。
暗黙的な変換は、クラス型からその基底クラス型のいずれかに存在します。 クラス型の変数は、そのクラスのインスタンスまたは任意の派生クラスのインスタンスを参照できます。 たとえば、前述のクラス宣言では、Point
型の変数が Point
または Point3D
を参照できます。
Point a = new(10, 20);
Point b = new Point3D(10, 20, 30);
構造体
クラスは、継承とポリモーフィズムをサポートする型を定義します。 これにより、派生クラスの階層に基づいて高度なビヘイビアーを作成できます。 これに対し、"構造体" 型は、データ値を格納することを主な目的とするより単純な型です。 構造体では基本データ型を宣言できません。これらは、暗黙的に System.ValueType から派生します。 struct
型から、他の struct
型を派生させることはできません。 これらは、暗黙的にシールされています。
public struct Point
{
public double X { get; }
public double Y { get; }
public Point(double x, double y) => (X, Y) = (x, y);
}
インターフェイス
"インターフェイス" では、クラスと構造体によって実装できるコントラクトを定義します。 個別の型間で共有される機能を宣言するには、interface を定義します。 たとえば、System.Collections.Generic.IEnumerable<T> インターフェイスを使用すると、配列などのコレクション内のすべての項目を走査する一貫した方法を定義できます。 1 つのインターフェイスには、メソッド、プロパティ、イベント、およびインデクサーが含まれる場合があります。 インターフェイスは、定義するメンバーの実装を通常行いません。インターフェイスを実装するクラスまたは構造体が提供する必要があるメンバーを指定するだけです。
インターフェイスでは "多重継承" を使用できます。 次の例では、インターフェイス IComboBox
は ITextBox
と IListBox
の両方から継承します。
interface IControl
{
void Paint();
}
interface ITextBox : IControl
{
void SetText(string text);
}
interface IListBox : IControl
{
void SetItems(string[] items);
}
interface IComboBox : ITextBox, IListBox { }
クラスと構造体は、複数のインターフェイスを実装できます。 次の例では、クラス EditBox
は IControl
と IDataBound
の両方を実装しています。
interface IDataBound
{
void Bind(Binder b);
}
public class EditBox : IControl, IDataBound
{
public void Paint() { }
public void Bind(Binder b) { }
}
クラスまたは構造体が特定のインターフェイスを実装する場合、そのクラスまたは構造体のインスタンスはそのインターフェイス型に暗黙的に変換できます。 次に例を示します。
EditBox editBox = new();
IControl control = editBox;
IDataBound dataBound = editBox;
列挙型
"列挙" 型では、一連の定数値を定義します。 次の enum
では、さまざまな根菜の定数の定義を宣言しています。
public enum SomeRootVegetable
{
HorseRadish,
Radish,
Turnip
}
enum
は組み合わせてフラグとして使用できるよう、定義することもできます。 次の宣言では、4 つの季節のフラグのセットを宣言しています。 任意に季節を組み合わせることも、すべての季節を含む All
値を含め、適用することも可能です。
[Flags]
public enum Seasons
{
None = 0,
Summer = 1,
Autumn = 2,
Winter = 4,
Spring = 8,
All = Summer | Autumn | Winter | Spring
}
次の例では、上記の両列挙型を宣言しています。
var turnip = SomeRootVegetable.Turnip;
var spring = Seasons.Spring;
var startingOnEquinox = Seasons.Spring | Seasons.Autumn;
var theYear = Seasons.All;
null 許容型
すべての型の変数は、"null 非許容" または "null 許容" として宣言できます。 null 許容の変数には、値がないことを示す null
値をさらに含めることができます。 null 許容値型 (構造体または列挙型) は、System.Nullable<T> で表します。 null 非許容の参照型と null 許容の参照型はいずれも、基になる参照型によって表します。 この区別は、コンパイラと一部のライブラリに読み取られたメタデータによって表します。 null 許容の参照が、値を最初に null
に対してチェックせずに逆参照されると、コンパイラによって警告が出されます。 null 非許容の参照が null
である可能性のある値に割り当てられている場合にも、コンパイラによって警告が出されます。 次の例では、null
に初期化される "null 許容 int" を宣言しています。 次いで、その値を 5
に設定しています。 同じ概念を、"Null 許容文字列" でも示しています。 詳細については、「null 許容値型」と「null 許容参照型」を参照してください。
int? optionalInt = default;
optionalInt = 5;
string? optionalText = default;
optionalText = "Hello World.";
タプル
C# では、軽量のデータ構造に複数のデータ要素を簡潔な構文でグループ化できる、"タプル" がサポートされます。 タプルは、次の例のように、(
と )
の間にメンバーの型と名前を宣言することで、インスタンス化できます。
(double Sum, int Count) t2 = (4.5, 3);
Console.WriteLine($"Sum of {t2.Count} elements is {t2.Sum}.");
//Output:
//Sum of 3 elements is 4.5.
タプルは、次の記事で説明する構成要素を使用しない、複数のメンバーを持つデータ構造の代わりとなります。