C# の型システム

C# は、厳密に型指定された言語です。 すべての変数および定数は、値に評価されるすべての式がそうであるように、型を持ちます。 各メソッドの宣言は、各入力パラメーターと戻り値に対して、名前、型と種類 (値、参照、または出力) を指定します。 .NET クラス ライブラリは、さまざまなコンストラクトを表す組み込みの数値型および複合型を定義します。 これには、ファイル システム、ネットワーク接続、オブジェクトのコレクションと配列、および日付が含まれます。 一般的な C# プログラムは、クラス ライブラリの型と、そのプログラムの問題領域に固有の概念をモデル化するユーザー定義の型を使用します。

型には、次のような項目の情報が保存されます。

  • その型の変数が必要とする記憶領域。
  • 表すことができる最大値と最小値。
  • 含まれるメンバー (メソッド、フィールド、イベントなど)。
  • 継承元となった基本型。
  • 実装されるインターフェイス。
  • 許可される演算の種類。

コンパイラは型情報を使用して、コード内で実行されるすべての演算が "タイプ セーフ" となるようにします。 たとえば、int 型の変数を宣言すると、加算演算と減算演算でその変数を使用することがコンパイラにより許可されます。 同じ演算を bool 型の変数に対して実行しようとすると、コンパイラで次の例のようなエラーが発生します。

int a = 5;
int b = a + 2; //OK

bool test = true;

// Error. Operator '+' cannot be applied to operands of type 'int' and 'bool'.
int c = a + test;

注意

C や C++ と異なり、C# では、boolint に変換することはできません。

コンパイラは、型情報を実行可能ファイル内にメタデータとして埋め込みます。 共通言語ランタイム (CLR: Common Language Runtime) は、実行時にこのメタデータを使用して、メモリの割り当て時および再要求時に、タイプ セーフであるかどうかを再度確認します。

変数宣言での型の指定

プログラム内で変数や定数を宣言するときは、その型を指定するか、var キーワードを使用して、コンパイラが型を推論できるようにする必要があります。 次の例では、組み込みの数値型と複雑なユーザー定義の型の両方を使用する変数宣言を示します。

// Declaration only:
float temperature;
string name;
MyClass myClass;

// Declaration with initializers (four examples):
char firstLetter = 'C';
var limit = 3;
int[] source = { 0, 1, 2, 3, 4, 5 };
var query = from item in source
            where item <= limit
            select item;

メソッドのパラメーターおよび戻り値の型は、メソッドの宣言で指定します。 入力引数として int を使用する必要があり、戻り値として文字列を返すメソッドのシグネチャを次に示します。

public string GetName(int ID)
{
    if (ID < names.Length)
        return names[ID];
    else
        return String.Empty;
}
private string[] names = { "Spencer", "Sally", "Doug" };

変数を宣言した後は、新しい型を使用して再宣言することはできず、宣言された型と互換性のない値を代入することもできません。 たとえば、int を宣言してから、それに true のブール値を代入することはできません。 ただし、たとえば新しい変数に代入するときや、メソッドの引数として渡すときに、値を他の型に変換することは可能です。 データの損失を伴わない "型変換" は、コンパイラによって自動的に実行されます。 データの損失を伴う可能性のある変換には、ソース コードに cast を記述する必要があります。

詳細については、「キャストと型変換」を参照してください。

組み込み型

C# には、組み込み型の標準セットが用意されています。 これらは、整数、浮動小数点値、ブール式、テキスト文字、10 進数値などのデータ型を表現しています。 また、組み込みの string 型や object 型もあります。 これらの型は、どの C# プログラムでも使用できます。 組み込み型の完全な一覧については、組み込みの型に関するページを参照してください。

カスタム型

structclassinterfaceenum、および record コンストラクトを使用すれば、独自のカスタム型を作成できます。 .NET クラス ライブラリ自体が、ユーザーが独自のアプリケーションで使用できるカスタム型のコレクションです。 既定では、クラス ライブラリで最も頻繁に使用される型は任意の C# プログラムで使用可能になっています。 それ以外は、それらが定義されているアセンブリへのプロジェクト参照を明示的に追加した場合にのみ使用可能になります。 コンパイラがアセンブリを参照できるようになると、そのアセンブリ内で宣言されている型の変数 (および定数) をソース コード内で宣言できるようになります。 詳細については、「.NET クラス ライブラリの概要」を参照してください。

共通型システム

.NET で型システムを使用する場合は、次の 2 つの基本事項を理解しておく必要があります。

  • 継承の原則がサポートされています。 他の型から型を派生させることができます。派生元の型は "基本型" と呼ばれます。 派生した型は、基本型のメソッド、プロパティ、およびその他のメンバーを (若干の制限付きで) 継承します。 基本型もなんらかの他の型から派生できます。この場合、派生した型はその継承階層内の両方の基本型のメンバーを継承します。 System.Int32 (C# のキーワード: int) などの組み込み数値型を含むすべての型は、最終的に System.Object (C# のキーワード: object) という単一の基本型から派生します。 この一元化された型階層は、共通型システム (CTS) と呼ばれます。 C# での継承の詳細については、「継承」を参照してください。
  • CTS の各型は、"値型" または "参照型" として定義されます。 この型には、.NET クラス ライブラリのすべてのカスタムの型だけでなく、ユーザーが独自に定義した型も含まれます。 struct キーワードを使用して定義した型は値型であり、すべての組み込み数値型は structs です。 class または record のキーワードを使用して定義した型は、参照型です。 参照型と値型では、コンパイル時の規則や実行時の動作が異なります。

次の図は、CTS における値型と参照型の関係を示しています。

Screenshot that shows CTS value types and reference types.

注意

この図を見るとわかるように、最もよく使用される型はすべて System 名前空間に属しています。 しかし、型が属している名前空間は、その型が値型と参照型のどちらであるかには関係ありません。

クラスと構造体は、.NET の共通型システムの 2 つの基本構成です。 C# 9 では、クラスの一種であるレコードが追加されます。 クラスと構造体は、どちらも基本的にはデータと動作のセットを 1 つの論理単位としてカプセル化するデータ構造です。 データと動作は、クラス、構造体、またはレコードの "メンバー" です。 メンバーにはそのメソッド、プロパティ、イベントなどが含まれます。この記事で後ほど一覧表示します。

クラス、構造体、またはレコードの宣言は、実行時にインスタンスやオブジェクトを作成するために使用する設計図のようなものです。 Person という名前のクラス、構造体、またはレコードを定義する場合、Person が、型の名前です。 型 p の変数 Person を宣言して初期化すると、pPerson のオブジェクトまたはインスタンスになります。 同じ Person 型のインスタンスを複数作成し、各インスタンスのプロパティとフィールドに異なる値を設定することができます。

クラスは参照型です。 型のオブジェクトが作成されると、オブジェクトが割り当てられている変数にはそのメモリへの参照だけが設定されます。 オブジェクト参照が新しい変数に割り当てられると、新しい変数は元のオブジェクトを参照します。 いずれの変数も同じデータを参照しているため、1 つの変数に加えられた変更は他の変数にも反映されます。

構造体は値の型です。 構造体が作成されると、構造体が割り当てられている変数にはその構造体の実際のデータが設定されます。 構造体が新しい変数に代入されると、それはコピーされます。 したがって、新しい変数と元の変数には、同じデータのコピーが別個に含まれることになります。 一方のコピーに対して行われた変更は、もう一方のコピーには影響しません。

レコードの型は、参照型 (record class) と値の型 (record struct) のどちらかになります。

一般的に、クラスはより複雑な動作をモデル化するために使用します。 通常、クラスには、クラス オブジェクトの作成後に変更されることを意図したデータが格納されます。 構造体は、小規模なデータ構造に最適です。 通常、構造体には、構造体の作成後に変更されることを意図したデータが格納されます。 レコード型は、コンパイラによって合成された追加メンバーを持つデータ構造です。 通常、レコードには、オブジェクトの作成後に変更されることを意図したデータが格納されます。

値型

値型は、System.ValueType の派生型である System.Object から派生します。 System.ValueType から派生した型は、CLR では特殊な動作をします。 値の型の変数には、その値が直接含まれています。 構造体のメモリは、変数が宣言されているコンテキストでインラインで割り当てられます。 値型の変数には、独立したヒープ割り当てやガベージ コレクションのオーバーヘッドはありません。 値の型であり、レコードの合成メンバーが含まれる record struct 型を宣言することができます。

値型には、structenumの 2 つのカテゴリがあります。

組み込みの数値型は構造体であり、次のようにしてアクセスできるフィールドとメソッドを持ちます。

// constant field on type byte.
byte b = byte.MaxValue;

ただし、宣言とそこへの値の代入は、あたかも単純な非集約型であるかのように行うことができます。

byte num = 0xA;
int i = 5;
char c = 'Z';

値の型は、"シールド" です。 System.Int32 などの値の型から、型を派生させることはできません。 構造体は System.ValueType からしか継承できないため、ユーザー定義のクラスまたは構造体を継承する構造体を定義することはできません。 ただし、構造体は 1 つ以上のインターフェイスを実装できます。 実装するインターフェイス型に構造体型をキャストすることができます。 このキャストによる "ボックス化" 操作によって、構造体がマネージド ヒープ上の参照型オブジェクト内にラップされます。 ボックス化操作が発生するのは、入力パラメーターとして System.Object または任意のインターフェイス型を受け取るメソッドに値型を渡した場合です。 詳細については、「ボックス化とボックス化解除」を参照してください。

独自のカスタム値型を作成するには、struct キーワードを使用します。 通常、構造体は、次の例に示すように、少数の関連する変数のコンテナーとして使用します。

public struct Coords
{
    public int x, y;

    public Coords(int p1, int p2)
    {
        x = p1;
        y = p2;
    }
}

構造体の詳細については、構造体型に関する記事をご覧ください。 値型の詳細については、値型に関するページを参照してください。

別の種類の値型として、enumがあります。 列挙体は、一連の名前付き整数定数を定義します。 たとえば、.NET クラス ライブラリの System.IO.FileMode 列挙体には、ファイルを開く方法を指定する一連の名前付き整数定数が格納されています。 これは、次の例のように定義されます。

public enum FileMode
{
    CreateNew = 1,
    Create = 2,
    Open = 3,
    OpenOrCreate = 4,
    Truncate = 5,
    Append = 6,
}

定数 System.IO.FileMode.Create は、2 という値を持ちます。 しかし、人間がソース コードを読む場合は名前があるとわかりやすいため、このような場合は、リテラルの数値の定数を使用するよりも、列挙体を使用する方がよいと言えます。 詳細については、「System.IO.FileMode」を参照してください。

すべての列挙体は、System.Enum の派生型である System.ValueType から派生します。 構造体に適用されるすべての規則が、列挙体にも適用されます。 詳細については、「列挙型」を参照してください。

参照型

classrecorddelegate、配列、または interface として定義された型は、reference type です。

reference type の変数を宣言した場合は、その型のインスタンスを代入するか、 演算子を使用して作成するまで、値 newnull が格納されます。 次の例では、クラスの作成と代入が示されています。

MyClass myClass = new MyClass();
MyClass myClass2 = myClass;

new 演算子を使用して interface のインスタンスを直接作成することはできません。 代わりに、そのインターフェイスを実装するクラスのインスタンスを作成して代入します。 次の例を考えてみましょう。

MyClass myClass = new MyClass();

// Declare and assign using an existing value.
IMyInterface myInterface = myClass;

// Or create and assign a value in a single statement.
IMyInterface myInterface2 = new MyClass();

オブジェクトが作成されると、メモリがマネージド ヒープ上に割り当てられます。 変数は、オブジェクトの場所の参照のみを保持します。 マネージド ヒープの型では、割り当て時と再要求時の両方でオーバーヘッドが必要です。 "ガベージ コレクション" は、再要求を実行する CLR の自動メモリ管理機能です。 しかし、ガベージ コレクションも高度に最適化されるため、ほとんどのシナリオでは、パフォーマンス上の問題が発生することはありません。 ガベージ コレクションの詳細については、「自動メモリ管理」を参照してください。

配列は、その要素が値型の場合でも、すべて参照型です。 配列は、System.Array クラスから暗黙的に派生します。 C# に用意されている次の例のような簡単な構文で宣言および使用します。

// Declare and initialize an array of integers.
int[] nums = { 1, 2, 3, 4, 5 };

// Access an instance property of System.Array.
int len = nums.Length;

参照型では、継承が全面的にサポートされています。 クラスを作成すると、シールドとして定義されていないその他のインターフェイスやクラスから継承することができます。 他のクラスは、クラスから継承し、仮想メソッドをオーバーライドできます。 独自のクラスを作成する方法の詳細については、「クラス、構造体、レコード」を参照してください。 継承と仮想メソッドの詳細については、「継承」を参照してください。

リテラル値の型

C# では、リテラル値の型がコンパイラによって決定されます。 数値リテラルの型指定の方法を指定するには、その数値の末尾に文字を付加します。 たとえば、値 4.56float として扱う必要があることを指定するには、次のように数値の後に "f" または "F" を付加します: 4.56f。 文字を付加しない場合、リテラルの型はコンパイラによって推論されます。 文字サフィックスで指定できる型の詳細については、「整数数値型」と「浮動小数点数値型」を参照してください。

リテラルは型指定され、すべての型は最終的に System.Object から派生するため、次のようなコードを記述してコンパイルできます。

string s = "The answer is " + 5.ToString();
// Outputs: "The answer is 5"
Console.WriteLine(s);

Type type = 12345.GetType();
// Outputs: "System.Int32"
Console.WriteLine(type);

ジェネリック型

実際の型 ("具象型") のプレースホルダーとして使用される 1 つまたは複数の "型パラメーター" で、型を宣言することができます。 クライアント コードから、型のインスタンスの作成時に具象型が提供されます。 このような型は、ジェネリック型と呼ばれます。 たとえば、.NET の型 System.Collections.Generic.List<T> には、慣例により T という名前が与えられる 1 つの型パラメーターがあります。 この型のインスタンスを作成するときには、リストに含まれるオブジェクトの型を指定します。たとえば、string の場合:

List<string> stringList = new List<string>();
stringList.Add("String example");
// compile time error adding a type other than a string:
stringList.Add(4);

型パラメーターを使用することで、同じクラスを再利用して任意の型の要素を格納できます。このとき、各要素をオブジェクトに変換する必要はありません。 ジェネリック コレクション クラスが "厳密に型指定されたコレクション" と呼ばれるのは、コレクションの要素の固有の型をコンパイラが認識しているためで、たとえば、前の例の stringList オブジェクトに整数を追加しようとすると、コンパイル時にエラーが発生します。 詳細については、「ジェネリック」を参照してください。

暗黙の型、匿名型、および Null 許容値型

ローカル変数 (クラスのメンバーではない) の型を暗黙的に指定するには、var キーワードを使用します。 変数の型はコンパイル時に決定されますが、その型はコンパイラによって指定されます。 詳細については、「暗黙的に型指定されるローカル変数」を参照してください。

メソッドの境界を越えて格納したり受け渡したりする予定のない単純な一連の関連値に名前付きの型を作成するのは便利ではないこともあります。 このような場合は、"匿名型" を作成できます。 詳細については、「匿名型」を参照してください。

通常の値型が null 値を持つことはできません。 しかし、型の後ろに ? を付けることによって、"Null 許容値型" を作成できます。 たとえば、int? は、null 値も持つことができる int 型です。 Null 許容値型は一般的な構造体型 System.Nullable<T> のインスタンスです。 Null 許容値型は、数値が null になる可能性のあるデータベースとの間でデータを受け渡しする場合に、特に便利です。 詳細については、「Null 許容値型」を参照してください。

コンパイル時の型と実行時の型

変数は、コンパイル時と実行時で型が異なる場合があります。 "コンパイル時の型" は、ソースコード内の変数の宣言または推論された型です。 "実行時の型" は、その変数によって参照されるインスタンスの型です。 次の例に示すように、多くの場合、これら 2 つの型は同じです。

string message = "This is a string of characters";

そうでない場合、次の 2 つの例に示すように、コンパイル時の型が異なります。

object anotherMessage = "This is another string of characters";
IEnumerable<char> someCharacters = "abcdefghijklmnopqrstuvwxyz";

前の例はどちらも、実行時の型が string です。 コンパイル時の型は最初が object、2 番目が IEnumerable<char> です。

変数の 2 つの型が異なる場合は、コンパイル時の型と実行時の型が適用されるタイミングを理解することが重要です。 コンパイル時の型によって、コンパイラによって実行されるすべてのアクションが決まります。 これらのコンパイラ アクションには、メソッド呼び出しの解決、オーバーロードの解決、使用できる暗黙的および明示的なキャストが含まれます。 実行時の型によって、実行時に解決されるすべてのアクションが決まります。 この実行時のアクションには、仮想メソッド呼び出しのディスパッチ、isswitch の式と、その他の型のテスト API の評価が含まれます。 コードが型とどのように対話するかを理解するには、どのアクションがどの型に適用されるかを認識してください。

詳細については、次の記事を参照してください。

C# 言語仕様

詳細については、「C# 言語の仕様」を参照してください。 言語仕様は、C# の構文と使用法に関する信頼性のある情報源です。