C Sharpのデータ型

この記事では、プログラミング言語C#のデータ型について説明する。

それぞれのデータ型が取り込まれたバージョンについては、親記事C#を参照のこと。C#のデータ型は共通言語基盤 (CLI) のデータ型と密接な関係があり、ターゲットとする.NET Frameworkあるいは.NET Coreのバージョンによっては使用できないものも存在する。

参照型と値型 
C#の任意の型は (後述するポインタ型を除いて) 参照型と値型に二分される。

参照型の変数が、実際のオブジェクトを格納する領域の参照のみを保持するのに対して、
値型の変数は、実際のオブジェクトの値全てを保持する。

この特徴により、値の代入や引数の受け渡しの挙動が大きく異なって見える。

参照型 
 #クラス型
 object
 dynamic
 string
 #配列型
 #デリゲート型
 #匿名型
 #インターフェイス型

値型 
 #構造体型
 bool
 char
 整数型
 浮動小数点数型
 decimal
 #列挙型

参照型にも値型にも分別されない型 
 #ポインタ型
ポインタ型はobject型と相互に変換することができず、参照型にも値型にも相当しない。

「任意の型について」記載している内容は、ポインタ型が当てはまらないことも多いことに留意されたい。例えば、#ジェネリクスの型引数にポインタ型を指定することはできない。

基本的なデータ型 
C#において、キーワード指定されたデータ型（組み込み型、）を下に示す。

decimal 
C#のdecimal型は128bitの十進浮動小数点数であるが、以下の点で他の浮動小数点数とは大きく異なる。

 非数NaN, 負の無限大NegativeInfinity, 正の無限大PositiveInfinityは表現できない。
 ゼロ除算では、DivideByZeroExceptionをthrowする。
 decimal.MaxValueを上回る、あるいはdecimal.MinValueを下回る演算では、OverFlowExceptionをthrowする。
 指数部は常に非正数 [-28, 0] を取る。
 仮数部は単に96bit符号無し整数として扱われる。

 符号部は 0 ならば正、1 ならば負を表現する。
 指数部は 0 〜 28 の値を取り、実際にはその反数の値を表現する。基数は10である。
 仮数部は 0 〜 79,228,162,514,264,337,593,543,950,335 の値を表現する。

主なデータ型

クラス型 
クラス型は、関連するメンバーをカプセル化した参照型である。メンバーにはフィールド、メソッド、プロパティ、コンストラクタなどが含まれる。

 クラスはclassキーワードを使用して宣言する。
 クラスは1個の基底クラスを継承する。継承する基底クラスを指定しない場合、System.Objectが直接基底クラスとなる。
 クラスは0個以上の複数のインターフェイスを実装できる。
 abstract指定されたクラスは「抽象クラス」と呼ばれ、インスタンス化できない。派生クラスはabstractでない限りインスタンス化できる。
 sealed指定されたクラスは、これ以上派生できない。
 static指定されたクラスは「静的クラス」と呼ばれ、インスタンス化も派生もできない。静的なメンバーしか持たず、System.Object以外を継承することはない。
 クラス型変数の初期値はnullとなる。

構造体型 
構造体型は、関連するメンバーをカプセル化した値型である。

 構造体はstructキーワードを使用して宣言する。
 構造体は明示的に基底クラスを継承することができない。全ての構造体はSystem.ValueTypeが直接基底クラスとなる。
 構造体は0個以上の複数のインターフェイスを実装できる。
 構造体は派生できない。
 構造体は暗黙にデフォルトコンストラクタを持ち、構造体型変数の全てのフィールドはdefaultで初期化される。
 readonly指定された構造体では、フィールドは全てreadonly指定されている必要がある（C# 7.2以降）。
 ref指定された構造体は常にスタックに確保され、ボックス化されない（C# 7.2以降）。

インターフェイス型 
インターフェイス型は、型の多重継承を実現するための抽象型である。

 インターフェイスはinterfaceキーワードを使用して宣言する。
 インターフェイスは0個以上の複数のインターフェイスを継承できる。
 インターフェイスはインスタンスフィールドを持たない。
 インターフェイスはメソッド、プロパティ、インデクサ、イベントの宣言を含むことができるが、実装を持つことはできない。ただしC# 8.0以降では、これらのデフォルト実装を持つことができる。
 インターフェイスは定数および静的メンバーを持たない。ただしC# 8.0以降では、これらを持つことができる。
 インターフェイス型変数の初期値はnullとなる。

列挙型 
列挙型は、名前付き定数の集まりで構成される値型である。
byte, sbyte, short, ushort, int, uint, long, ulong のいずれかの整数型を基となる型として宣言できる。

 列挙型はenumキーワードを使用して宣言する。
 列挙型は明示的に基底クラスを継承することができない。全ての列挙型はSystem.Enumが直接基底クラスとなる。
 列挙型は派生できない。
 列挙型変数の初期値は基となる整数型の0を表す名前付き定数となる。
 FlagsAttribute属性が付いた列挙型はビット演算可能となり、値の組合せを表現できる。

/// <summary>光の三原色を1bitずつで表す</summary>
[Flags]
enum RgbColor : byte {
    Black = 0,
    Red = 1 << 0,
    Green = 1 << 1,
    Blue = 1 << 2,
    Cyan = Green | Blue,
    Magenta = Blue | Red,
    Yellow = Red | Green,
    White = Red | Green | Blue,
}

static class RgbColorExtension {
    /// <summary>White (0b111) とのXORを返す拡張メソッド。C# 3.0以降が必要。</summary>
    public static RgbColor GetComplementaryColor(this RgbColor color) { return color ^ RgbColor.White; }
}

static void Main() {
    // Magenta を定義していない場合、"Red, Blue" が表示される。
    Console.WriteLine(RgbColor.Green.GetComplementaryColor());
    // Yellow を定義していない場合、"Red, Green" が表示される。
    Console.WriteLine(RgbColor.Blue.GetComplementaryColor());
}

デリゲート型 
デリゲート型は、オブジェクトインスタンスへの参照とメソッドへの参照をまとめてカプセル化する参照型である。

 デリゲート型はdelegateキーワードを使用して宣言する。
 デリゲート型は明示的に基底クラスを継承することができない。全てのデリゲート型はSystem.Delegateが直接基底クラスとなる。
 デリゲート型は派生できない。
 デリゲート型変数の初期値はnullとなる。
 デリゲート型変数は2項演算子+および加算代入演算子+=で複数の値を連結できる。2項演算子-および減算代入演算子-=でデリゲートを削除できる。

配列型 
配列型は、初期化時に指定した型と長さを持つ参照型である。
ジャグ配列 (配列の配列) のほかに、真の多次元配列がサポートされる。

匿名型 
匿名型は一時的に使用される型を簡単に定義するためのクラス型。読み取り専用プロパティのみを持つ。
特定の型名を持たないが、varキーワードで宣言したローカル変数に格納することができる。

dynamic型 
dynamic型は、動的型付け変数を表す型である。
コンパイル時の型チェックをバイパスし、実行時に演算が解決される。

ポインタ型 
ポインタ型はC++ライクなポインタ操作を可能とする型である。
通常の共通型システムの型とは異なり、共通言語ランタイムでは検査されない。
unsafe キーワードによって指定された範囲でしか利用できないようになっており、
コンパイル時には /unsafe オプションを指定する必要がある。

ジェネリクス 
C#はジェネリックプログラミングに対応する。

クラス、構造体、インターフェイス、デリゲート、メソッドに対して、型引数を適用することができる。

型引数はout/inパラメータと共に指定することで、共変性、反変性を持つ。

/// <summary>型引数を取るinterfaceの例</summary>
/// <typeparam name="TIn">反変な型引数</typeparam>
/// <typeparam name="TOut">共変な型引数</typeparam>
/// <typeparam name="T">不変な型引数</typeparam>
interface ISampleTypeParameter<in TIn, out TOut, T> {
    void Procedure(TIn input);
    TOut Supply();
    T Function(T input);
    void UseDelegate(Func<TOut, TIn> func);
    void UseDelegate(Func<T, T> func);
    Func<TIn, TOut> SupplyDelegate1();
    Func<T, T> SupplyDelegate2();
}

特別扱いされる型 
C#において、文法上特別扱いを受ける型の例を下に示す。

Exception型 
Exception型および派生型のインスタンスは、
throw可能であり、
try-catch構文で例外処理に対応する。

Attribute型 
Attribute型の派生型は、
コードの要素に対して属性を付けることができる。
例えばSerializableAttributeであれば、クラス宣言の手前で[Serializable]を記述することで、そのクラスが直列化可能であることを宣言できる。

IEnumerable<T>, IEnumerator<T> 
IEnumerable<T>,IEnumerator<T>インターフェイスを戻り値とするメソッドやgetアクセサは、
yieldキーワードによるコルーチンに対応し、ジェネレータを簡潔に記述できる。

IDisposable 
IDisposableインターフェイスを実装した型は、
usingステートメントによるリソースの安全な破棄に対応する。

Nullable<T> 
型名の後ろに?を付けることで、Nullable<T>型の糖衣構文となる。
また、値型であるにもかかわらずnullを代入することができる。

int? maybeNum = null;
int correctlyNum = maybeNum ?? 0;
上記構文は下記の糖衣構文である。
Nullable<int> maybeNum = null;
int correctlyNum = maybeNum.HasValue ? maybeNum.Value : 0;

また、Null許容型では、一部を除いて各演算子が再定義される。
// Null許容int型の演算子再定義
int? x = null;
int? y = 123;
// nullを含む単項演算子、二項演算子 (NULL,NULL,NULL,NULL)
Console.WriteLine((x + y)?.ToString() ?? "NULL");
Console.WriteLine((x << y)?.ToString() ?? "NULL");
Console.WriteLine((-x)?.ToString() ?? "NULL");
Console.WriteLine((++x)?.ToString() ?? "NULL");
// 片方がnullの場合の比較演算子(false,false,false,true)
Console.WriteLine($"{x <= y},{x == y},{x >= y},{x != y}");
// 双方がnullの場合の比較演算子(false,true,false,false)
int? z = null;
Console.WriteLine($"{x <= z},{x == z},{x >= z},{x != z}");

// Null許容bool型の演算子再定義
bool? b1 = null;
bool? b2 = true;
// nullを含む単項演算子、二項演算子 (NULL,NULL)
Console.WriteLine((b1 & b2)?.ToString() ?? "NULL");
Console.WriteLine((!b1)?.ToString() ?? "NULL");
// ショートサーキットは再定義されない
//Console.WriteLine((b1 && b2)?.ToString() ?? "NULL");
// Null許容bool型のままでは条件式では使用できない
//if (b1) { DoSomething(); }
if (b1 ?? false) { DoSomething(); }
if (b1 == true) { DoSomething(); }

Task, Task<T>, ValueTask<T> 
async/awaitキーワードによる、非同期プログラミングに対応する。

ValueTuple<> 
2個以上の要素を持つValueTupleについては、例えば(string s, int n)と書くことでValueTuple<string, int>型の糖衣構文となる。
また、例えば("answer", 42)と書くことで、リテラルを表記できる。

IFormattable, FormattableString 
挿入文字列リテラルから暗黙の型変換が可能である。

// 書式指定可能(IFormattable)型で受け取ることができる。
IFormattable fmt = $"id: {id:D}, money: {money:C}";
var str = fmt.ToString(null, new System.Globalization.CultureInfo("ja-JP"));

Expression<T> 
式形式のラムダから暗黙の型変換が可能である。これによって、式木をシンプルに記述できる。

// 本体が式のラムダ式をExpression型に格納することができる。
System.Linq.Expressions.Expression<Func<int, int>> expression = x => x * x;
// 本体がステートメントのラムダ式はExpressionとして扱えない。
//System.Linq.Expressions.Expression<Func<int, int>> expression = x => { return x * x; };

Span<T> 
stackallocで割り当てたメモリの代入が可能である。

// C# 7.2から可能
Span<int> span1 = stackalloc int[4];
// C# 7.3から初期化子にも対応
Span<int> span2 = stackalloc[] { 10, 20, 30, 40 };

特別扱いされるメソッド 
C#において、ダック・タイピング的な扱いを受ける例を下に示す。
特定のメソッドが定義されていれば、クラス間の継承関係に関わらずC#の文法の恩恵を受けることができる。

Add() 
IEnumerableが実装されAdd()メソッドが定義された型では、コレクション初期化の構文を利用することができる。

/// <summary>IEnumerableを実装、かつ、Addメソッドを持つ</summary>
class MultiplyList : IEnumerable {
    List<int> _list = new List<int>();
    public void Add(int x) => _list.Add(x);
    public void Add(int x, int y) => _list.Add(x * y);
    public void Add(int x, int y, int z) => _list.Add(x * y * z);
    IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator();
}

// Add()のオーバーロードがそれぞれ呼び出される。
var list = new MultiplyList() {
    { 1 },
    { 2, 3 },
    { 4, 5, 6 },
};

GetEnumerator() 
GetEnumerator()メソッドが定義された型では、foreach構文を利用することができる。

/// <summary>GetEnumeratorメソッドを実装する型</summary>
class DaysInMonths {
    public IEnumerator<int> GetEnumerator() {
        yield return 31; yield return 28; yield return 31;
        yield return 30; yield return 31; yield return 30;
        yield return 31; yield return 31; yield return 30;
        yield return 31; yield return 30; yield return 31;
    }
}

var months = new DaysInMonths();
// foreach構文
foreach (var days in months) {
    Console.WriteLine(days);
}

Deconstruct() 
Deconstruct()メソッドが定義された型では、分解構文を利用することができる。拡張メソッドによる定義でも構わない。

/// <summary>Deconstructメソッドを実装する型</summary>
class Person {
    public string Name { get; set; }
    public int Age { get; set; }
    /// <summary>分解メソッド</summary>
    public void Deconstruct(out string name, out int age) {
        name = Name;
        age = Age;
    }
}

var person = new Person() { Name = "Taro", Age = 18 };
// 分解構文
var (name, age) = person;

// 以下のDeconstructメソッド呼び出しと同じ
person.Deconstruct(out var name, out var age);

GetAwaiter() 
GetAwaiter()メソッドが定義された型では、await演算子を利用することができる。拡張メソッドによる定義でも構わない。

GetPinnableReference() 
GetPinnableReference()メソッドが定義された型では、fixedステートメント内でのポインタ指定時に構文糖の恩恵を得る。

型情報 

System.Type型を利用することで、様々な型情報にアクセスすることができる。

この型情報からリフレクションによるプログラミングに対応する。

次のコードはSystem.Type型の例である。

// コンパイル時型情報
Type typeAtCompiling = typeof(int); /* System.Int32 */
// 実行時型情報
object obj = "Hello!";
Type typeAtExecuting = obj.GetType(); /* System.String */
// 指定した名前の型情報を取得
Type typeByNominate = Type.GetType("System.EventHandler");

CheckType(typeAtCompiling);
CheckType(typeAtExecuting);
CheckType(typeByNominate);

static void CheckType(Type type) {
    Console.WriteLine(type);
    Console.WriteLine("* 参照型: " + type.IsClass);
    Console.WriteLine("** インターフェイス型: " + type.IsInterface);
    Console.WriteLine("** 配列型: " + type.IsArray);
    Console.WriteLine("** デリゲート型: " + type.IsSubclassOf(typeof(MulticastDelegate)));
    Console.WriteLine("* 値型: " + type.IsValueType);
    Console.WriteLine("** 列挙型: " + type.IsEnum);
    Console.WriteLine("** プリミティブ: " + type.IsPrimitive);
    Console.WriteLine("* 総称型: " + type.IsGenericType);
    Console.WriteLine("* ポインタ型: " + type.IsPointer);
}

 typeof(T)はTの型情報を取得する。
 obj.GetType()はobjの実行時型情報を取得する。
 Type.GetType(完全修飾型名)は指定した完全修飾型名の型情報を取得する。

脚注

関連項目 
 C#
 共通型システム

csharp
てたかた