データ型

C#で扱う「値」の種類

変数のページでも軽く説明しましたが、C#で扱う「値」にはいくつかの種類があります。
整数を扱うなら整数型、文字列を扱うなら文字列型、といった具合です。
これを詳しく見ていきます。

基本的なデータ型

bool
論理型(ブーリアン型)。
「true」か「false」かの二通りの値を取る。
(「真」か「偽」かを表す)
byte
符号なし1バイト整数型
0~255の値を扱う。
sbyte
符号あり1バイト整数型
-128~127の値を扱う。
short
符号あり2バイト整数型
-32,768~32,767の値を扱う。
ushort
符号なし2バイト整数型
0~65,535の値を扱う。
int
符号あり4バイト整数型
-2,147,483,648~2,147,483,647の値を扱う。
(21億)
uint
符号なし4バイト整数型
0~4,294,967,295の値を扱う。
(42億)
long
符号あり8バイト整数型
-9,223,372,036,854,775,808~9,223,372,036,854,775,807の値を扱う。
(922京)
ulong
符号なし8バイト整数型
0~18,446,744,073,709,551,615の値を扱う。
(1844京)
float
32ビット浮動小数点数。
-3.402823×10の38乗~3.402823×10の38乗の値を扱う。
double
64ビット浮動小数点数。
-1.79769313486232×10の308乗~1.79769313486232×10の308乗の値を扱う。
decimal
128ビット10進浮動小数点数。
-79228162514264337593543950335~79228162514264337593543950335の値を扱う。
整数型のように見えるが小数も扱える。
(doubleよりも高精度)
char
Unicode 16ビット文字型。
内部的には0~65535の数値。
(2バイト)
string
文字列型。

int以降は値が大きすぎてわけがわかりませんが、あまり気にする必要はありません。
普通に整数値を扱いたい場合はint型を使用しておけばまず間違いはありません。
他の整数型はint型では扱いきれないような大きな数値が必要な場合や、特定の関数で使用する場合など、用途が限られます。
メモリ等の容量節約のためにbyte型等の小さなデータ型を使用することもありますが、後述するオーバーフローには気を付ける必要があります。

bool型はtruefalseかのどちらかの状態を取るデータ型です。
例えばチェックボックスがオンかオフか、関数の処理に成功したか失敗したか、などでよく使われる型です。

float型、double型は小数(実数)を扱うことができるデータ型です。
int型などの整数型の変数に小数を含む値を代入すると値が変化してしまうので、これらの専用のデータ型を使用する必要があります。

decimal型は10進数の数値を表現するデータ型です。
double型よりも大きな128ビットで表すので、double型よりもより大きく精密な値を扱うことができます。
その分多くのメモリを消費し、計算速度が劣ります。

なお、「int」や「new」などはC#があらかじめ用意している語で、ユーザー(プログラマ)はこれらを変数名等として使用することはできません。
これらを予約語と言います。

小数値の誤差

C#をはじめとした多くのプログラミング言語では、小数同士の計算時に「誤差」が発生することがあります。
この誤差は精密な演算を行う場合だけに発生するというわけではなく、割と頻繁に発生します。

誤差は値を保存しておくメモリ領域が有限であることが原因で発生します。
普通に紙と鉛筆で計算した場合と、プログラミングで計算した場合とでは計算結果が一致しないこともあるのです。
double型はfloat型よりも精度の高い値を扱えますが、それでも微妙に誤差がある可能性を考慮しなければなりません。

ビットとバイト

コンピュータで扱えるデータの最小単位をビットといいます。

1ビットは「0」と「1」の2通りの情報を扱えます。
2ビットあれば「00」「01」「10」「11」の4通りの情報を扱えます。
(2の2乗)
3ビットで8通り(2の3乗)、4ビットで16通り(2の4乗)です。

8ビットで256通りの情報を扱えます。
これで1バイトとなります。
1バイトでアルファベットの大文字小文字すべてと数字、主要な記号や命令を一通り扱えるだけの情報量となります。

C言語では「文字型(char)」は1バイトですが(正確には1バイト以上)、C#では日本語などを扱えるように2バイトとなっています。
ただし2バイト(65536通り)でも漢字をすべて表すことはできず、char型ひとつでは扱えない文字も存在します。

「符号なし」と「符号あり」

整数のデータ型には符号なし符号ありという分類があります。
「符号」というのはプラス記号、マイナス記号のことです。
符号なしのデータ型はマイナスを付けられないので、全てプラスの値(正数)を扱います。
つまりゼロ未満の数値(負数)を扱うことはできません。
その代わり、プラス側は符号ありのデータ型の倍の幅を扱えます。

「byte型」は符号なしであることに注意してください。
例えばint/uint型は接頭語のある方が符号なしで、接頭語は「u」です。
byte/sbyte型は、他のデータ型と違って接頭語のある方が符号ありで、接頭語は「s」です。

「uint」「ulong」などの接頭語「u」は「unsigned」の略です。
「sbyte」の接頭語「s」は「signed」の略です。

「sign(サイン)」というのは「符号」の意味です。
「+2」「-5」などの数値の手前に付けられる記号を符号といいます。
つまり「signed」は「符号あり」、「unsigned」は符号なしを意味します。

MaxValue、MinValue

数値型のデータ型は、それぞれが扱える最大値と最小値があらかじめ定義されています。


int intMax = int.MaxValue;
int intMin = int.MinValue;

float floatMax = float.MaxValue;
float floatMin = float.MinValue;

Console.WriteLine(intMax);
Console.WriteLine(intMin);
Console.WriteLine(floatMax);
Console.WriteLine(floatMin);
214748647
-214748648
3.402823E+38
-3.402823E+38

データ型名に続いて.(ドット)を記述し、MaxValueプロパティで最大値、MinValueプロパティで最小値が得られます。
(どちらも読み取り専用プロパティです)

数値末尾の「E+38」は指数表記といい、10のn乗を意味します。
この場合は「3.402823 × 10の38乗」です。

オーバーフロー(桁あふれ)

数値型のデータ型には扱える値の上限と下限があります。
では、その範囲を超えた値を扱おうとするとどうなるでしょうか。

例えばsbyte型は「-128~127」の範囲の整数を扱えます。
これに1を加算してみます。


sbyte sb = sbyte.MaxValue;  //127
sb++;                       //値をひとつ増やす
Console.WriteLine(sb);
-128

正の値に1を加算したのに、結果はなんとマイナス値になってしまいます。
「-128」はsbyte型が表せる最低値で、sbyte型の最大値を超えると値が一巡してしまうのです。
これをオーバーフロー(桁あふれ)といいます。

オーバーフローを見落とすと値が意図しないものとなり、バグの原因となってしまいます。
意図的なものを除いてオーバーフローは避けるべきです。

checkedキーワード

オーバーフローが起こりそうな計算をする場合、もしオーバーフローが発生したら例外を発生させることができます。


sbyte sb = sbyte.MaxValue;
checked
{
    sb++; //例外発生
}

Console.WriteLine(sb);

checkedブロックの中の演算でオーバーフローが発生すると、そこで例外が発生します。
例外が発生するとプログラムは停止しますが、適切に処理することでプログラムの実行を停止させず、かつデータの予期せぬ変化に対応することができます。


sbyte sb = sbyte.MaxValue;
try
{
    checked
    {
        sb++; //例外発生
    }
}
catch (Exception e) { }

Console.WriteLine(sb);

その他、すべての演算でオーバーフローチェックをするようにコンパイラの設定を変更することも可能です。
その場合に特定の演算だけオーバーフローのチェックを省くためのuncheckedキーワードもあります。

例外については例外を参照してください。

object型

上記のリスト掲載のデータ型のほかに、C#にはもう一つobject型というデータ型があります。
object型は、一言で言えば「何でも入れられるデータ型」です。


static void Main(string[] args)
{
    object obj;

    obj = 123;
    obj = "abc";
    obj = new int[] { 1, 2, 3 };
}

上記コードは同じ変数に数値、文字列、配列、と異なるデータ型のデータを次々に代入していますが、エラーにはなりません。
このような通常ではありえない変数を実現するのがobject型です。

何でも代入可能というと便利に聞こえますが、逆に言えば「何が入っているかわからない」ということです。
中身は数値かもしれないし、文字列かもしれないし、それ以外かもしれません。

データ型というのは、扱える値を制限する代わりに「できる事」を明確になるメリットがあります。
例えば整数型ならば四則計算が可能なことが保証されていますし、文字列型ならば文字列操作が可能なことが保証されています。

値をobject型で扱うということは、このようなメリットを捨ててしまうということです。
例えばobject型のままでは中身に数値が入っていたとしても足し算すらできません。
中身が「足し算ができるデータ型」なのかが不明なためです。


object obj1 = 123;
object obj2 = 456;

//エラー
int num = obj1 + obj2;

object型は、扱うデータ型をあらかじめ特定できない場合に使用します。
中身のデータ型が明確な場合はキャストという方法で目的のデータ型に変換することができます。


object obj1 = 123;
object obj2 = 456;

int num = (int)obj1 + (int)obj2;

配列型

データ型はすべて配列にして扱うことができます。
この時の配列変数のデータ型は配列型というデータ型になります。

配列型は参照型に分類されます。
詳しくは値型と参照型で説明します。

その他のデータ型

上記のデータ型はすべてC#にあらかじめ用意されているデータ型で、プログラムで扱う様々なデータの基本となる型です。
これらは組み込み型と言います。
C#は自分で新しいデータ型を作ることもでき、これをユーザー定義型と言います。

ユーザー定義型には列挙型構造体クラスなどが該当します。

既定値

データ型には既定値というものが存在します。

変数は基本的に任意の値で初期化してからでないと使用できませんが、例えば配列の各要素は任意の値で初期化せずとも値を取り出すことができます。


int num;

//初期化しないと使用できない
//Console.WriteLine(num);

int[] nums = new int[3];

//OK
Console.WriteLine(nums[0]);
Console.WriteLine(nums[1]);
Console.WriteLine(nums[2]);
0
0
0

配列の各要素は、配列初期化子で初期化しない場合は自動的に既定値がセットされるようになっています。

既定値は

  • 数値型は「0」
  • bool型は「false」
  • char型は「\0」(Null文字)
  • 参照型は「null」

となっています。

参照型については値型と参照型を参照してください。

default演算子

データ型の既定値はdefault演算子で取得することができます。


//構造体(値型)
struct MyStruct
{
    public int Num;
    public string Str;
}

//クラス(参照型)
class MyClass
{
    public int Num;
    public string Str;

    public MyClass() { 
        Num = 1;
        Str = "a";
    }
}

static void Print(object x)
{
    Console.WriteLine(x == null ? "(null)" : x);
}

static void Main(string[] args)
{
    int a = default(int);
    string b = default(string);
    MyStruct c = default(MyStruct);
    MyClass d = default(MyClass);

    Print(a);
    Print(b);
    Print(c);
    Print(d);
}
0
(null)
Program+MyStruct
(null)

defaultというキーワードに続いてデータ型名を丸括弧内に記述すると、そのデータ型の既定値を取得することができます。

上記コードのPrintメソッドは、object型の引数の中身がnullであった場合は「(null)」という文字列を、それ以外の場合は中身の値を表示します。
Console.WriteLineメソッドの引数の処理については三項条件演算子で改めて説明します。

defaultリテラル

C#7.1以降、コードの文脈からデータ型が推測可能な場合は、default演算子のデータ型の指定を省略できます。
これはdefaultリテラルといいます。

具体的には以下の場面でdefaultリテラルを使用できます。


//デフォルト引数の値
static int Test(int a, int b = default)
{
    //データ型が明示的な変数の初期化/代入
    int c = default;

    //return文の値
    return default;
}

static void Main(string[] args)
{
    //実引数の値
    Test(default, 1);
}

型推論

C#では型推論という記法が導入されています。
これは、変数の宣言と同時に初期化を行う時、初期化に用いる値のデータ型が明らかならばデータ型の指定をvarというキーワードで代用できる、というものです。


static void Main(string[] args)
{
    int num1 = 10;
    double num2 = 1.23;

    var num3 = num1;  //int型
    var num4 = num2;  //double型
    var str = Func(); //string型

    Console.ReadLine();
}

static string Func()
{
    return "abc";
}

6~8行目はすべてvarで変数の宣言および初期化を行っています。
右辺値(初期化する値)のデータ型に応じて、それぞれ「int型」「double型」「string型」の変数が作成されます。
あくまでも「型を類推して決定する」機能で、「var」という何でも入れられるデータ型が作れるわけではありません。

以下のように変数の宣言のみでは型が決定できないのでエラーになります。


var num;

int型よりも小さい値の場合、型推論はint型となります。


short num1 = 10;
short num2 = 20;

var num3 = num1 + num2; //int型

単に整数値を記述した場合は「int型」、小数値の場合は「double型」になります。


var num1 = 10; //int型
var num2 = 1.23; //double型

詳しくはリテラルの項で説明します。

使用できるのはローカル変数のみ

型推論を使用できるのはローカル変数のみで、フィールド(メンバー変数)には使用できません。


class Program
{
    //これはエラー
    //var m_num = 0;

    static void Main(string[] args)
    {
        //これはOK
        var num = 0;
    }
}

ローカル変数とフィールド(メンバー変数)に関してはまだ説明していません。
詳しくはローカル変数とフィールド(メンバー変数)を参照してください。

匿名型

変数のデータ型はint型やstring型、その他自分であらかじめ定義したデータ型を使用しますが、匿名型という特殊なデータ型を使用することもできます。


var person = new { ID = 1, Name = "A山B太" };

Console.WriteLine(person.ID);
Console.WriteLine(person.Name);
1
A山B太

匿名型はvarキーワードで宣言します。

このように記述すると、変数personは内部に「IDという名前のint型の値」と「Nameという名前のstring型の値」を持っている型となります。
「ID」「Name」などはプロパティといいます。
この型はその場限りのデータ型で、具体的な名前を持たないので匿名型というわけです。

匿名型はそれ単体ではあまり使うことはありませんが、LINQなどの機能では時々使用されます。

匿名型を配列にする場合は以下のようにします。


var persons = new[]
{
    new { ID = 1, Name = "A山B太"},
    new { ID = 2, Name = "C谷D男"},
    new { ID = 3, Name = "E下F子"}
};

Console.WriteLine(persons[0].ID);
Console.WriteLine(persons[2].Name);

値は変更できない

匿名型の内部の値は読み取り専用で、値の変更はできません。


var person = new { ID = 1, Name = "A山B太" };

//エラー
person.ID = 2;

プロパティ名の指定の省略

匿名型のプロパティに既存の変数を指定する場合、プロパティ名の指定は省略できます。
その場合、プロパティ名には変数名がそのまま使用されます。


int ID = 1;
string Name = "A山B太";

var person = new { ID, Name };

Console.WriteLine(person.ID);
Console.WriteLine(person.Name);
1
A山B太

C#のデータ型名と.NETのデータ型名

C#の組み込みのデータ型は、それぞれ別の名前を持っています。
例えばint型はSystem.Int32という名前でも使用することができます。


int num1 = 1;
System.Int32 num2 = 2;

num1 = num2;

int型はSystem.Int32型の別名で、両者は全く同じデータ型です。
同じなのでそれぞれ相互に代入等が可能ですし、コード上のintというキーワードはそのままSystem.Int32に置き換えることもできます。

System.Int32型は.NET対応のプログラミング言語で共通して使用されるデータ型のひとつで、名前の通り32ビット(4バイト)のサイズを持つ整数型です。
(IntはInteger=整数の意味)
.NET対応のプログラミング言語は特定の言語に依存しない共通の中間コードを生成しますが、そのために各プログラミング言語で使用されるデータ型の種類とサイズを揃える必要があり、その実装がSystem.Int32型などになります。
(共通型システムという)
System.Int32型は内部的には構造体で実装されていますが、C#ではこれを扱いやすくするためにintというキーワードを別名で用意しています。

.NETのデータ型名は以下のものがあります。

.NET型 C#型
値型
System.SByte sbyte
System.Byte byte
System.Int16 short
System.UInt16 ushort
System.Int32 int
System.UInt32 uint
System.Int64 long
System.UInt64 ulong
System.IntPtr nint
System.UIntPtr nuint
System.Single float
System.Double double
System.Decimal decimal
System.Boolean bool
System.Char char
参照型
System.String string
System.Object object

C#のキーワードと.NET型のどちらを使用しても問題ありませんが、混在すると読みにくくなるため統一したほうが良いでしょう。

nint型/nuint型

先ほどの表には、このページでは説明していなかった「nint型」と「nuint型」が記載されています。
C#組み込みのデータ型はサイズが固定ですが、この二つだけは例外で、プログラムが32ビットプロセスで実行される場合は32ビット長、64ビットプロセスで実行される場合は64ビット長になります。
(nint型は符号あり型、nuint型は符号なし型です)
これは.NET以外で作成されたライブラリなどと連携する場合に使用されることがあります。

System.IntPtr型とSystem.UIntPtr型は.NET Framework1.1(.NET Core1.0)から使用できますが、nint/nuintキーワードはC#9.0以降で使用できます。