【C#】組み込み型の種類一覧と特徴・選び方をわかりやすく解説

C#の組み込み型は、整数や浮動小数点、真偽値、文字列、オブジェクトなど基本データを扱うために用意された核となる型群です。

intdoubleなど値型はメモリ効率と高速計算を支え、stringobjectなど参照型はデータ構造の柔軟性を担います。

状況に応じて選択すれば、型安全とパフォーマンスを両立でき、C#の型システムを最大限に活かせます。

目次から探す
  1. 組み込み型とは
  2. 整数型
  3. 浮動小数点型
  4. 文字と文字列
  5. 真偽値型
  6. オブジェクト型
  7. 特殊な組み込み型
  8. 拡張概念としてのNullable<T>
  9. タプルと匿名型
  10. enum列挙型
  11. 構造体(struct)とレコード(struct record)
  12. 共通言語仕様(CLS)と互換性
  13. 型変換とキャスト
  14. 文字列表記とリテラル
  15. メモリ配置とパフォーマンス
  16. テストとデバッグ視点の型選択
  17. よくある落とし穴
  18. バージョン別の追加・変更点
  19. 型安全を高める設計指針
  20. まとめ

組み込み型とは

C#における組み込み型とは、プログラムでよく使われる基本的なデータ型のことを指します。

これらは整数や小数、文字、論理値など、日常的なデータを表現するために用意されている型で、言語仕様に組み込まれているため「組み込み型」と呼ばれます。

組み込み型は、プログラムのパフォーマンスやメモリ効率に大きく影響するため、適切に理解し使い分けることが重要です。

C#の組み込み型は、実は.NETの構造体structとして実装されているものが多く、単なる言語仕様の一部というだけでなく、.NETの型システムの一環として設計されています。

これにより、型安全性やパフォーマンスの最適化が図られています。

プリミティブ型と.NET構造体の違い

「プリミティブ型」という言葉は、C#の組み込み型を指すことが多いですが、厳密にはC#の言語仕様上の基本型を意味します。

例えば、intboolcharなどがこれに該当します。

これらは言語レベルで特別に扱われ、リテラル表現や演算子のオーバーロードが標準で用意されています。

一方、.NET構造体structは、値型として定義されるユーザー定義型や組み込み型の実装単位です。

C#の組み込み型の多くは、実際には.NETのSystem.Int32System.Booleanなどの構造体として実装されています。

つまり、intSystem.Int32のエイリアス(別名)であり、同じ型を指しています。

この違いをまとめると以下のようになります。

項目プリミティブ型(C#言語仕様).NET構造体(実装)
定義場所C#言語仕様.NET Framework / .NET Core
int, bool, charSystem.Int32, System.Boolean
特徴リテラル表現や演算子が言語で特別扱い値型でメモリ効率が良い
ユーザー定義可能か不可可能(自分でstructを定義できる)

このように、C#のプリミティブ型は.NETの構造体として実装されているため、言語の利便性と.NETの型システムの強力さを両立しています。

値型(ValueType)と参照型(ReferenceType)

C#の型は大きく分けて「値型」と「参照型」の2種類に分類されます。

組み込み型の多くは値型に属しており、これがプログラムの動作やメモリ管理に大きな影響を与えます。

値型(ValueType)とは

値型は、変数が直接データの値を保持する型です。

例えば、int型の変数には整数の値そのものが格納されます。

値型の特徴は以下の通りです。

  • メモリ配置: スタック領域に直接値が格納されることが多い(ただし、ボックス化されるとヒープに移動する場合もあります)
  • コピー動作: 変数を別の変数に代入すると、値がコピーされます。つまり、2つの変数は独立した値を持ちます
  • パフォーマンス: 小さなデータを扱う場合は高速で効率的です
  • 代表的な型: int, float, bool, char, struct(ユーザー定義の構造体)など

参照型(ReferenceType)とは

参照型は、変数がデータの実体ではなく、そのデータが格納されているメモリの「参照(アドレス)」を保持する型です。

例えば、stringやクラスclassは参照型です。

特徴は以下の通りです。

  • メモリ配置: ヒープ領域にデータが格納され、変数はそのヒープ上のデータの参照を持ちます
  • コピー動作: 変数を別の変数に代入すると、参照がコピーされるため、両方の変数が同じデータを指します
  • パフォーマンス: 大きなデータや複雑なオブジェクトを扱う場合に適していますが、ガベージコレクションの影響を受けます
  • 代表的な型: string, object, class(ユーザー定義のクラス)など

値型と参照型の違いを理解するサンプルコード

以下のサンプルコードは、値型と参照型の代入時の挙動の違いを示しています。

using System;
class Program
{
    struct PointStruct
    {
        public int X;
        public int Y;
    }
    class PointClass
    {
        public int X;
        public int Y;
    }
    static void Main()
    {
        // 値型の例
        PointStruct p1 = new PointStruct { X = 10, Y = 20 };
        PointStruct p2 = p1; // 値がコピーされる
        p2.X = 30;
        Console.WriteLine($"p1: X={p1.X}, Y={p1.Y}"); // 10, 20
        Console.WriteLine($"p2: X={p2.X}, Y={p2.Y}"); // 30, 20
        // 参照型の例
        PointClass c1 = new PointClass { X = 10, Y = 20 };
        PointClass c2 = c1; // 参照がコピーされる
        c2.X = 30;
        Console.WriteLine($"c1: X={c1.X}, Y={c1.Y}"); // 30, 20
        Console.WriteLine($"c2: X={c2.X}, Y={c2.Y}"); // 30, 20
    }
}
p1: X=10, Y=20
p2: X=30, Y=20
c1: X=30, Y=20
c2: X=30, Y=20

このコードでは、PointStructは値型の構造体、PointClassは参照型のクラスです。

p2 = p1の代入では値がコピーされるため、p1の値は変わりません。

一方、c2 = c1の代入では参照がコピーされるため、c1c2は同じオブジェクトを指し、c2.Xを変更するとc1.Xも変わります。

このように、値型と参照型の違いを理解することは、C#の組み込み型を正しく使いこなす上で非常に重要です。

特にパフォーマンスやメモリ管理、バグの原因となる参照の共有を避けるために役立ちます。

整数型

符号付き整数

sbyte

sbyteは符号付き8ビット整数型で、-128から127までの範囲の整数を扱います。

メモリ使用量が1バイトと非常に小さいため、メモリ制約のある環境や小さな数値範囲での計算に適しています。

ただし、扱える値の範囲が狭いため、一般的な用途ではあまり使われません。

using System;
class Program
{
    static void Main()
    {
        sbyte smallNumber = -100;
        Console.WriteLine($"sbyteの値: {smallNumber}");
    }
}
sbyteの値: -100

short

shortは符号付き16ビット整数型で、-32,768から32,767までの範囲の整数を扱います。

sbyteよりも広い範囲を扱えますが、intよりは狭い範囲です。

メモリ使用量は2バイトです。

主にメモリ効率を重視しつつ、比較的小さな整数範囲を扱う場合に使われます。

using System;
class Program
{
    static void Main()
    {
        short temperature = -273;
        Console.WriteLine($"shortの値: {temperature}");
    }
}
shortの値: -273

int

intは符号付き32ビット整数型で、-2,147,483,648から2,147,483,647までの範囲の整数を扱います。

C#で最も一般的に使われる整数型であり、ほとんどの整数演算に適しています。

メモリ使用量は4バイトです。

using System;
class Program
{
    static void Main()
    {
        int population = 1400000000;
        Console.WriteLine($"intの値: {population}");
    }
}
intの値: 1400000000

long

longは符号付き64ビット整数型で、-9,223,372,036,854,775,808から9,223,372,036,854,775,807までの範囲の整数を扱います。

非常に大きな整数を扱う必要がある場合に使われます。

メモリ使用量は8バイトです。

using System;
class Program
{
    static void Main()
    {
        long distanceToSun = 149600000000; // 約1億4960万km
        Console.WriteLine($"longの値: {distanceToSun}");
    }
}
longの値: 149600000000

符号なし整数

byte

byteは符号なし8ビット整数型で、0から255までの範囲の整数を扱います。

メモリ使用量は1バイトで、主にバイナリデータの処理や小さな正の整数を扱う場合に使われます。

using System;
class Program
{
    static void Main()
    {
        byte age = 25;
        Console.WriteLine($"byteの値: {age}");
    }
}
byteの値: 25

ushort

ushortは符号なし16ビット整数型で、0から65,535までの範囲の整数を扱います。

メモリ使用量は2バイトで、比較的小さな正の整数を扱う場合に使われます。

using System;
class Program
{
    static void Main()
    {
        ushort portNumber = 8080;
        Console.WriteLine($"ushortの値: {portNumber}");
    }
}
ushortの値: 8080

uint

uintは符号なし32ビット整数型で、0から4,294,967,295までの範囲の整数を扱います。

メモリ使用量は4バイトで、正の整数を広い範囲で扱いたい場合に使われます。

intと異なり負の値を扱えないため、負の値が不要な場合に適しています。

using System;
class Program
{
    static void Main()
    {
        uint fileSize = 4294967295;
        Console.WriteLine($"uintの値: {fileSize}");
    }
}
uintの値: 4294967295

ulong

ulongは符号なし64ビット整数型で、0から18,446,744,073,709,551,615までの範囲の整数を扱います。

メモリ使用量は8バイトで、非常に大きな正の整数を扱う場合に使われます。

using System;
class Program
{
    static void Main()
    {
        ulong maxValue = 18446744073709551615;
        Console.WriteLine($"ulongの値: {maxValue}");
    }
}
ulongの値: 18446744073709551615

利用シーン別の選び方

整数型を選ぶ際は、扱う値の範囲とメモリ効率、パフォーマンスを考慮します。

以下のポイントを参考にしてください。

  • 値の範囲を確認する

まず、扱う数値の最小値と最大値を把握します。

例えば、年齢やカウントなどの小さな正の整数ならbyteushortで十分です。

大きな数値や負の値を扱う場合はintlongを選びます。

  • 符号の有無を考慮する

負の値が不要な場合は符号なし整数byte, ushort, uint, ulongを使うと、同じビット数でより大きな正の値を扱えます。

ただし、符号なし整数は他の型との互換性や演算時の注意点があるため、使いどころを見極める必要があります。

  • パフォーマンスとメモリ効率

小さなデータを大量に扱う場合は、byteshortなどの小さい型を使うことでメモリ使用量を抑えられます。

ただし、CPUの処理効率はintが最適化されていることが多いため、特に理由がなければintを使うのが無難です。

  • APIやフレームワークの仕様に合わせる

多くの.NET APIはintを標準としているため、APIとの互換性を考慮してintを使うことが多いです。

特定のAPIがuintlongを要求する場合はそれに従います。

  • オーバーフローのリスク管理

値の範囲を超えるとオーバーフローが発生します。

checkedキーワードを使ってオーバーフロー検出を有効にしたり、適切な型を選んでリスクを減らしましょう。

以下は利用シーン別の簡単な目安表です。

利用シーン推奨型理由
小さな正の整数(0~255)byteメモリ効率が良い
小さな整数(-32,768~32,767)short範囲が広くメモリも節約できる
一般的な整数(-2億~2億)intCPU最適化されており汎用性が高い
大きな整数(64ビット範囲)long非常に大きな値を扱う場合
小さな正の整数(0~65,535)ushort符号なしで範囲が広い
大きな正の整数(32ビット)uint負の値不要で範囲を広げたい場合
非常に大きな正の整数(64ビット)ulong最大値が非常に大きい

このように、整数型は用途に応じて適切に選ぶことで、メモリ効率やパフォーマンスを最適化できます。

特にintはデフォルトで使われることが多いため、特別な理由がない限りはintを選ぶのが安全です。

浮動小数点型

float(単精度)

floatは単精度浮動小数点数を表す型で、4バイト(32ビット)のメモリを使用します。

約7桁の有効数字を持ち、科学技術計算やグラフィックス処理など、メモリやパフォーマンスを重視しつつ小数点数を扱いたい場合に使われます。

floatはIEEE 754規格に準拠しており、指数部と仮数部で数値を表現します。

小数点以下の精度は限定的で、非常に大きな数や非常に小さな数を扱うことができますが、丸め誤差が発生しやすい点に注意が必要です。

using System;
class Program
{
    static void Main()
    {
        float pi = 3.1415927f; // floatリテラルは末尾にfを付ける
        Console.WriteLine($"floatの値: {pi}");
    }
}
floatの値: 3.1415927

double(倍精度)

doubleは倍精度浮動小数点数を表す型で、8バイト(64ビット)のメモリを使用します。

約15桁の有効数字を持ち、floatよりも高い精度で小数点数を扱えます。

C#で最も一般的に使われる浮動小数点型です。

doubleもIEEE 754規格に準拠しており、科学技術計算や金融以外の多くの用途で使われます。

floatよりも精度が高いため、丸め誤差が少なくなりますが、完全に誤差がないわけではありません。

using System;
class Program
{
    static void Main()
    {
        double e = 2.718281828459045;
        Console.WriteLine($"doubleの値: {e}");
    }
}
doubleの値: 2.718281828459045

decimal(高精度10進)

decimalは高精度の10進数を扱う型で、16バイト(128ビット)のメモリを使用します。

約28~29桁の有効数字を持ち、金融計算や通貨計算など、誤差を極力避けたい場合に適しています。

decimalは2進数ではなく10進数で内部表現されているため、10進数の小数点計算で丸め誤差が非常に少なくなります。

ただし、floatdoubleに比べて計算速度は遅く、メモリ消費も大きいです。

using System;
class Program
{
    static void Main()
    {
        decimal price = 12345.6789m; // decimalリテラルは末尾にmを付ける
        Console.WriteLine($"decimalの値: {price}");
    }
}
decimalの値: 12345.6789

精度と丸め誤差の比較

浮動小数点型は有限のビット数で実数を表現するため、丸め誤差が避けられません。

floatdoubleは2進数で数値を表すため、10進数の小数を正確に表現できない場合があります。

これにより、計算結果に微小な誤差が生じることがあります。

一方、decimalは10進数で表現するため、10進数の小数点計算においては丸め誤差が非常に少なくなります。

以下のサンプルは、doubledecimalで同じ計算をした場合の誤差の違いを示しています。

using System;
class Program
{
    static void Main()
    {
        double doubleSum = 0.1 + 0.2;
        decimal decimalSum = 0.1m + 0.2m;
        Console.WriteLine($"doubleの計算結果: {doubleSum}");
        Console.WriteLine($"decimalの計算結果: {decimalSum}");
    }
}
doubleの計算結果: 0.30000000000000004
decimalの計算結果: 0.3

このように、doubleでは0.1と0.2の和が厳密に0.3にならず、わずかな誤差が生じています。

decimalは10進数の計算に適しているため、金融計算など誤差を許容できない場面で使われます。

金額計算に向く型はどれか

金額計算や通貨計算では、計算結果の誤差が許されないため、decimal型が最も適しています。

decimalは10進数で表現されるため、通貨単位の小数点以下の桁数を正確に扱えます。

floatdoubleは丸め誤差が発生しやすく、金額計算で使うと誤差が蓄積し、結果が不正確になるリスクがあります。

特に、複数回の加算や減算を繰り返す場合は注意が必要です。

以下は金額計算でdecimalを使った例です。

using System;
class Program
{
    static void Main()
    {
        decimal price = 1999.99m;
        decimal taxRate = 0.08m; // 8%の消費税
        decimal tax = price * taxRate;
        decimal total = price + tax;
        Console.WriteLine($"価格: {price}円");
        Console.WriteLine($"消費税: {tax}円");
        Console.WriteLine($"合計: {total}円");
    }
}
価格: 1999.99円
消費税: 159.9992円
合計: 2159.9892円

このように、decimalを使うことで金額計算の精度を保ちながら安全に計算できます。

金融系のアプリケーションや会計処理では、decimalが標準的に使われています。

文字と文字列

charの特性

charは単一のUnicode文字を表す組み込み型で、2バイト(16ビット)のメモリを使用します。

C#のcharはUTF-16エンコーディングに基づいており、基本多言語面(BMP)に含まれる文字を1つ表現できます。

つまり、ほとんどの一般的な文字は1つのcharで表せますが、一部の補助文字(サロゲートペア)は2つのcharで表現されます。

charは文字リテラルとしてシングルクォーテーションで囲みます。

例えば、'A''あ'などです。

数値としても扱え、Unicodeコードポイントの整数値を取得したり比較したりできます。

using System;
class Program
{
    static void Main()
    {
        char letter = 'A';
        char japaneseChar = 'あ';
        Console.WriteLine($"文字: {letter}, Unicodeコード: {(int)letter}");
        Console.WriteLine($"文字: {japaneseChar}, Unicodeコード: {(int)japaneseChar}");
    }
}
文字: A, Unicodeコード: 65
文字: あ, Unicodeコード: 12354

このように、charは単一の文字を効率的に扱うのに適していますが、複数文字や文字列全体を扱う場合はstringを使います。

stringの特性

stringは文字のシーケンスを表す参照型で、可変長のテキストデータを扱います。

C#のstringは不変(イミュータブル)であり、一度作成された文字列は変更できません。

文字列の変更操作は新しい文字列を生成する形で行われます。

文字列リテラルはダブルクォーテーションで囲みます。

例えば、"Hello""こんにちは"などです。

不変(イミュータブル)によるメリット

stringが不変であることにはいくつかのメリットがあります。

  • スレッドセーフ

複数のスレッドから同じ文字列を参照しても、文字列の内容が変わらないため安全に共有できます。

  • ハッシュコードの安定性

文字列の内容が変わらないため、ハッシュコードが一定であり、辞書やハッシュセットのキーとして使いやすいです。

  • メモリ効率の向上

同じ文字列リテラルは内部的に共有されるため、重複した文字列のメモリ消費を抑えられます。

ただし、文字列の連結や置換を頻繁に行う場合は、新しい文字列が毎回生成されるためパフォーマンスに影響が出ることがあります。

その場合はStringBuilderクラスの利用が推奨されます。

文字列補間と結合性能

C#では文字列の結合に複数の方法がありますが、近年は文字列補間(interpolation)がよく使われています。

文字列補間は$記号を使い、変数や式を文字列内に埋め込める便利な機能です。

using System;
class Program
{
    static void Main()
    {
        string name = "太郎";
        int age = 30;
        // 文字列補間
        string message = $"名前は{name}、年齢は{age}歳です。";
        Console.WriteLine(message);
    }
}
名前は太郎、年齢は30歳です。

文字列の結合性能は、結合回数や文字列の長さによって変わります。

単純な少数回の結合なら+演算子や文字列補間で十分ですが、多数回の連結を繰り返す場合はStringBuilderを使うと効率的です。

using System;
using System.Text;
class Program
{
    static void Main()
    {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 5; i++)
        {
            sb.Append(i);
            sb.Append(", ");
        }
        string result = sb.ToString();
        Console.WriteLine(result);
    }
}
0, 1, 2, 3, 4,

文字列処理で失敗しない型選択

文字列処理で失敗しないためには、charstringの特性を理解し、適切に使い分けることが重要です。

  • 単一文字の操作はcharを使う

文字の比較やUnicodeコードポイントの取得、文字単位の処理はcharが適しています。

  • 複数文字やテキスト全体はstringを使う

文章や単語、複数文字のデータはstringで扱います。

stringは不変なので、頻繁に文字列を変更する場合はStringBuilderを検討してください。

  • Unicodeの補助文字に注意する

charは16ビットで1文字を表しますが、サロゲートペアを使う補助文字は2つのcharで表現されます。

絵文字や一部の特殊文字を扱う場合は、System.Text.StringInfoクラスを使って文字単位の操作を行うことが推奨されます。

  • 文字列の比較は文化依存に注意

文字列の比較はStringComparison列挙体を使って、文化や大文字小文字の違いを考慮した比較を行うと安全です。

using System;
class Program
{
    static void Main()
    {
        string s1 = "straße";
        string s2 = "strasse";
        bool equalOrdinal = s1.Equals(s2, StringComparison.Ordinal);
        bool equalIgnoreCase = s1.Equals(s2, StringComparison.OrdinalIgnoreCase);
        bool equalCulture = s1.Equals(s2, StringComparison.CurrentCultureIgnoreCase);
        Console.WriteLine($"Ordinal比較: {equalOrdinal}");
        Console.WriteLine($"OrdinalIgnoreCase比較: {equalIgnoreCase}");
        Console.WriteLine($"CultureIgnoreCase比較: {equalCulture}");
    }
}
Ordinal比較: False
OrdinalIgnoreCase比較: False
CultureIgnoreCase比較: True

この例では、ドイツ語の「ß」と「ss」の違いを文化依存の比較で考慮しています。

文字列処理で失敗しないためには、こうした文化やケースの違いを意識した比較を行うことが大切です。

真偽値型

boolの内部表現

C#のbool型は、真偽値trueまたはfalseを表す組み込み型で、論理演算や条件分岐に使われます。

内部的には、boolは1バイト(8ビット)で表現されることが多いですが、CLR(Common Language Runtime)レベルでは実際に1バイトのメモリを使うかどうかは状況によって異なります。

boolの値は、trueが1、falseが0として扱われます。

これは、条件式の評価やビット演算の基礎となっています。

ただし、C#ではbool型は整数型とは異なる独立した型であり、整数値との暗黙的な変換はできません。

例えば、intの0や1を直接boolに代入することはできません。

using System;
class Program
{
    static void Main()
    {
        bool flagTrue = true;
        bool flagFalse = false;
        Console.WriteLine($"flagTrueの値: {flagTrue}");
        Console.WriteLine($"flagFalseの値: {flagFalse}");
        // boolは整数と暗黙変換できないため、以下はコンパイルエラー
        // bool invalid = 1;
    }
}
flagTrueの値: True
flagFalseの値: False

また、boolは条件分岐やループの制御に使われるため、非常に頻繁に利用されます。

メモリ効率を考慮すると、複数のbool値をまとめて扱う場合はビットフィールドやビット演算を使うことが多いです。

フラグ設計での実践ポイント

複数の真偽値を管理する場合、個別にbool変数を用意するとメモリ効率が悪くなり、コードの管理も煩雑になります。

そこで、ビット演算を活用して1つの整数型変数で複数のフラグを管理する方法がよく使われます。

enumとFlags属性の活用

C#ではenum[Flags]属性を付けることで、ビットごとのフラグを表現しやすくなります。

これにより、複数の状態を1つの変数で管理し、ビット演算でON/OFFを切り替えられます。

using System;
[Flags]
enum FileAccessPermissions : byte
{
    None = 0,
    Read = 1 << 0,    // 00000001
    Write = 1 << 1,   // 00000010
    Execute = 1 << 2, // 00000100
    Delete = 1 << 3   // 00001000
}
class Program
{
    static void Main()
    {
        FileAccessPermissions permissions = FileAccessPermissions.Read | FileAccessPermissions.Write;
        Console.WriteLine($"現在の権限: {permissions}");
        // 書き込み権限があるかチェック
        bool canWrite = (permissions & FileAccessPermissions.Write) == FileAccessPermissions.Write;
        Console.WriteLine($"書き込み権限あり: {canWrite}");
        // 実行権限を追加
        permissions |= FileAccessPermissions.Execute;
        Console.WriteLine($"権限更新後: {permissions}");
        // 読み取り権限を削除
        permissions &= ~FileAccessPermissions.Read;
        Console.WriteLine($"読み取り権限削除後: {permissions}");
    }
}
現在の権限: Read, Write
書き込み権限あり: True
権限更新後: Read, Write, Execute
読み取り権限削除後: Write, Execute

実践ポイント

  • ビット演算で効率的に管理

複数のboolを1つの整数型で管理することで、メモリ使用量を削減し、状態の一括管理が可能です。

  • 意味のある名前を付ける

enumの各値に意味のある名前を付けることで、コードの可読性が向上します。

  • Flags属性の利用

[Flags]属性を付けると、ToString()で複数のフラグがカンマ区切りで表示されるため、デバッグやログ出力がわかりやすくなります。

  • ビットの重複に注意

各フラグは重複しないビット位置を割り当てる必要があります。

重複すると正しく判定できません。

  • フラグの初期値は0(None)にする

すべてのフラグがOFFの状態を表すため、None = 0を定義しておくと便利です。

このように、bool型単体での管理が難しい場合は、ビット演算とenumの組み合わせで効率的かつ可読性の高いフラグ設計を行うことが推奨されます。

オブジェクト型

objectが全型の基底である理由

C#におけるobject型は、すべての型の基底型(ルート型)として設計されています。

これは、C#が.NETの共通言語ランタイム(CLR)上で動作しているためで、CLRの型システムにおいてすべての型はSystem.Objectを継承しています。

この設計により、どんな型の値でもobject型の変数に格納でき、共通のインターフェースとして扱うことが可能になります。

例えば、整数型のintや文字列型のstring、ユーザー定義のクラスや構造体もすべてobjectの派生型です。

using System;
class Program
{
    static void Main()
    {
        object obj1 = 123;          // intをobjectに代入(ボックス化される)
        object obj2 = "Hello";      // stringは参照型なのでそのまま代入
        object obj3 = 3.14;         // doubleをobjectに代入(ボックス化される)
        Console.WriteLine(obj1);
        Console.WriteLine(obj2);
        Console.WriteLine(obj3);
    }
}
123
Hello
3.14

このように、object型は異なる型の値を一時的にまとめて扱いたい場合に便利です。

例えば、コレクションの要素をobject型で受け取ることで、異なる型のオブジェクトを同じリストに格納できます。

ただし、object型に格納された値を元の型に戻す際にはキャストが必要であり、型安全性を損なう可能性があるため注意が必要です。

BoxingとUnboxingのコスト

値型(intstructなど)をobject型に代入するとき、CLRは「ボックス化(Boxing)」という処理を行います。

これは、値型のデータをヒープ上のオブジェクトとしてラップし、参照型として扱えるようにする操作です。

逆に、object型から元の値型に戻す操作は「アンボックス化(Unboxing)」と呼ばれます。

アンボックス化では、ヒープ上のオブジェクトから値型のデータを取り出し、スタック上の変数にコピーします。

using System;
class Program
{
    static void Main()
    {
        int value = 42;
        // Boxing: 値型をobjectに代入
        object boxedValue = value;
        // Unboxing: objectから値型にキャスト
        int unboxedValue = (int)boxedValue;
        Console.WriteLine($"元の値: {value}");
        Console.WriteLine($"ボックス化された値: {boxedValue}");
        Console.WriteLine($"アンボックス化された値: {unboxedValue}");
    }
}
元の値: 42
ボックス化された値: 42
アンボックス化された値: 42

パフォーマンスへの影響

ボックス化とアンボックス化は、以下の理由でパフォーマンスに影響を与えます。

  • メモリ割り当て

ボックス化では、ヒープ上に新しいオブジェクトが生成されるため、ガベージコレクションの負荷が増えます。

  • コピーコスト

アンボックス化では、ヒープ上のオブジェクトから値をコピーする処理が発生します。

  • CPU負荷

ボックス化・アンボックス化の処理はCPU時間を消費し、頻繁に行うとパフォーマンス低下の原因になります。

そのため、頻繁に値型とobject型の間で変換を行うコードは避けるべきです。

代わりに、ジェネリクス(List<T>など)を使うことで、型安全かつボックス化を回避した効率的なコードを書くことが推奨されます。

using System;
using System.Collections.Generic;
class Program
{
    static void Main()
    {
        List<int> numbers = new List<int>();
        numbers.Add(10);
        numbers.Add(20);
        foreach (int num in numbers)
        {
            Console.WriteLine(num);
        }
    }
}
10
20

このように、ジェネリクスを使うことでボックス化・アンボックス化のコストを抑えつつ、型安全なプログラムが実現できます。

特殊な組み込み型

dynamic

dynamicはC# 4.0で導入された特殊な型で、コンパイル時の型チェックを行わず、実行時に型を解決する動的型付けを可能にします。

dynamic型の変数は、どんな型の値でも代入でき、メソッド呼び出しやプロパティアクセスも実行時に解決されます。

using System;
class Program
{
    static void Main()
    {
        dynamic value = 10;
        Console.WriteLine($"値: {value}, 型: {value.GetType()}");
        value = "Hello";
        Console.WriteLine($"値: {value}, 型: {value.GetType()}");
        // 実行時に存在しないメソッドを呼ぶと例外になる
        try
        {
            value.NonExistentMethod();
        }
        catch (Microsoft.CSharp.RuntimeBinder.RuntimeBinderException ex)
        {
            Console.WriteLine($"例外発生: {ex.Message}");
        }
    }
}
値: 10, 型: System.Int32
値: Hello, 型: System.String
例外発生: 'string' に 'NonExistentMethod' という名前のメソッドが存在しません

dynamicは柔軟性が高い反面、コンパイル時の型安全性が失われるため、実行時エラーのリスクが増えます。

COMオブジェクトやリフレクション、スクリプト言語との連携など、型が不明確な場面で便利に使われます。

pointer(unsafeコンテキスト)

C#では通常、安全なコード(safe code)としてメモリ管理が自動化されていますが、unsafeキーワードを使うことでポインタ操作が可能になります。

ポインタはメモリのアドレスを直接扱うため、CやC++のような低レベルの操作ができます。

using System;
class Program
{
    unsafe static void Main()
    {
        int number = 42;
        int* pointer = &number;
        Console.WriteLine($"numberの値: {number}");
        Console.WriteLine($"pointerが指す値: {*pointer}");
        *pointer = 100;
        Console.WriteLine($"変更後のnumberの値: {number}");
    }
}
numberの値: 42
pointerが指す値: 42
変更後のnumberの値: 100

ポインタを使うには、プロジェクトの設定で「unsafeコードの許可」を有効にし、unsafeキーワードで囲む必要があります。

ポインタ操作は高速で柔軟ですが、メモリ破壊やセキュリティリスクがあるため、使用は必要最低限にとどめるべきです。

void

voidは戻り値がないことを示す特殊な型で、メソッドの戻り値の型として使われます。

void自体は値を持たず、変数として宣言することはできません。

using System;
class Program
{
    static void PrintMessage()
    {
        Console.WriteLine("これはvoidメソッドです。");
    }
    static void Main()
    {
        PrintMessage();
    }
}
これはvoidメソッドです。

また、void*という形でポインタ型として使うこともありますが、これはunsafeコンテキスト内でのみ許可され、任意の型のメモリアドレスを指すために使われます。

voidは型安全性の観点からも特殊な役割を持つ型です。

拡張概念としてのNullable<T>

null許容値型の設計

C#の値型(intboolなど)は通常、nullを許容しません。

つまり、値型の変数は必ず何らかの値を持つ必要があります。

しかし、データベースの値やユーザー入力など、値が存在しない可能性がある場合にはnullを扱いたいことがあります。

この問題を解決するために、C#ではNullable<T>構造体が用意されており、T?というシンタックスシュガーで簡単に使えます。

これにより、値型でもnullを許容できるようになります。

using System;
class Program
{
    static void Main()
    {
        int? nullableInt = null;
        Console.WriteLine($"nullableIntの値: {nullableInt}");
        nullableInt = 100;
        Console.WriteLine($"nullableIntの値: {nullableInt}");
    }
}
nullableIntの値:
nullableIntの値: 100

Nullable<T>は内部的に値の有無を示すHasValueプロパティと、実際の値を保持するValueプロパティを持っています。

HasValuefalseの場合、Valueにアクセスすると例外が発生します。

using System;
class Program
{
    static void Main()
    {
        int? nullableInt = null;
        if (nullableInt.HasValue)
        {
            Console.WriteLine($"値は {nullableInt.Value} です。");
        }
        else
        {
            Console.WriteLine("値は存在しません(nullです)。");
        }
    }
}
値は存在しません(nullです)。

このように、Nullable<T>を使うことで、値型でもnullを安全に扱う設計が可能になります。

Null合体演算子と安全な利用

Nullable<T>を使う際に便利なのが、C#の??(Null合体演算子)です。

これは左辺の値がnullでなければその値を返し、nullであれば右辺の値を返します。

これにより、nullチェックと代替値の指定を簡潔に書けます。

using System;
class Program
{
    static void Main()
    {
        int? nullableInt = null;
        int value = nullableInt ?? -1; // nullableIntがnullなら-1を代入
        Console.WriteLine($"値: {value}");
        nullableInt = 50;
        value = nullableInt ?? -1;
        Console.WriteLine($"値: {value}");
    }
}
値: -1
値: 50

また、?.(Null条件演算子)と組み合わせることで、nullの可能性があるオブジェクトのメンバーアクセスを安全に行えます。

using System;
class Person
{
    public string? Name { get; set; }
}
class Program
{
    static void Main()
    {
        Person? person = null;
        // Null条件演算子でnullチェックを省略
        string? name = person?.Name ?? "名前不明";
        Console.WriteLine($"名前: {name}");
    }
}
名前: 名前不明

このように、Nullable<T>???.を組み合わせることで、nullを安全かつ簡潔に扱うコードが書けます。

null許容値型の設計では、これらの機能を活用してnullチェックの煩雑さを減らし、バグの発生を防ぐことが重要です。

タプルと匿名型

(int, string)形式タプル

C#では複数の値をまとめて返したり扱ったりする際に、タプルを使うことができます。

特に(int, string)のような形式のタプルは、複数の異なる型の値を1つのまとまりとして扱うのに便利です。

using System;
class Program
{
    static (int, string) GetPerson()
    {
        int age = 25;
        string name = "山田太郎";
        return (age, name);
    }
    static void Main()
    {
        var person = GetPerson();
        Console.WriteLine($"年齢: {person.Item1}, 名前: {person.Item2}");
        // 名前付きタプルで可読性向上
        (int Age, string Name) namedPerson = GetPerson();
        Console.WriteLine($"年齢: {namedPerson.Age}, 名前: {namedPerson.Name}");
    }
}
年齢: 25, 名前: 山田太郎
年齢: 25, 名前: 山田太郎

このように、(int, string)形式のタプルは複数の値を簡単に返せるため、メソッドの戻り値としてよく使われます。

名前付きタプルを使うと、Item1Item2の代わりに意味のある名前でアクセスでき、コードの可読性が向上します。

ValueTupleとSystem.Tupleの違い

C#にはSystem.TupleSystem.ValueTupleという2種類のタプル型がありますが、両者には大きな違いがあります。

項目System.TupleSystem.ValueTuple
型の種類参照型(class)値型(struct)
パフォーマンスヒープ割り当てが発生しやすいスタック上に割り当てられ高速
可変性不変(immutable)不変(immutable)
名前付き要素サポートなし(Item1, Item2など)名前付き要素をサポート
C#言語サポート古い形式で、言語サポートが限定的C# 7.0以降で言語組み込みサポート

System.Tupleは.NET Framework 4.0で導入された参照型のタプルで、Item1Item2といったプロパティ名でアクセスします。

参照型のため、ヒープに割り当てられ、ガベージコレクションの負荷が増えることがあります。

一方、System.ValueTupleはC# 7.0で導入された値型のタプルで、スタック上に割り当てられるためパフォーマンスが向上します。

また、名前付き要素をサポートし、言語の構文としても自然に使えます。

using System;
class Program
{
    static Tuple<int, string> GetOldTuple()
    {
        return Tuple.Create(1, "古いタプル");
    }
    static (int Id, string Name) GetValueTuple()
    {
        return (2, "新しいタプル");
    }
    static void Main()
    {
        var oldTuple = GetOldTuple();
        Console.WriteLine($"Old Tuple: {oldTuple.Item1}, {oldTuple.Item2}");
        var newTuple = GetValueTuple();
        Console.WriteLine($"ValueTuple: {newTuple.Id}, {newTuple.Name}");
    }
}
Old Tuple: 1, 古いタプル
ValueTuple: 2, 新しいタプル

パフォーマンスや可読性を考慮すると、C# 7.0以降ではValueTupleを使うことが推奨されます。

System.Tupleはレガシーコードや特定のAPIとの互換性のために使われることが多いです。

enum列挙型

列挙型の定義と用途

enum(列挙型)は、関連する定数の集合に名前を付けて扱いやすくするための型です。

整数型の定数に意味のある名前を割り当てることで、コードの可読性や保守性を向上させます。

列挙型はenumキーワードを使って定義し、デフォルトでは基になる型はintです。

各メンバーには自動的に0から始まる連続した整数値が割り当てられますが、明示的に値を指定することも可能です。

using System;
enum Weekday
{
    Sunday,     // 0
    Monday,     // 1
    Tuesday,    // 2
    Wednesday,  // 3
    Thursday,   // 4
    Friday,     // 5
    Saturday    // 6
}
class Program
{
    static void Main()
    {
        Weekday today = Weekday.Wednesday;
        Console.WriteLine($"今日は {today} です。");
        Console.WriteLine($"数値としての値: {(int)today}");
    }
}
今日は Wednesday です。
数値としての値: 3

列挙型の主な用途は以下の通りです。

  • 状態やモードの表現

例:曜日、月、色、操作モードなど

  • コードの可読性向上

数値のまま扱うよりも意味のある名前で表現することで、意図が明確になります。

  • 型安全な定数管理

間違った値の代入を防ぎ、コンパイル時にエラーを検出しやすくなります。

また、列挙型は整数型なので、比較やスイッチ文での分岐に使いやすい特徴があります。

Flags属性でビット演算を活かす

enum[Flags]属性を付けると、ビットごとのフラグとして複数の値を組み合わせて扱うことができます。

これにより、1つの変数で複数の状態を表現でき、ビット演算(AND、OR、XOR、NOT)を活用して効率的に管理できます。

using System;
[Flags]
enum FileAccess
{
    None = 0,
    Read = 1 << 0,    // 1
    Write = 1 << 1,   // 2
    Execute = 1 << 2, // 4
    Delete = 1 << 3   // 8
}
class Program
{
    static void Main()
    {
        FileAccess permissions = FileAccess.Read | FileAccess.Write;
        Console.WriteLine($"権限: {permissions}");
        // 書き込み権限があるかチェック
        bool canWrite = (permissions & FileAccess.Write) == FileAccess.Write;
        Console.WriteLine($"書き込み権限あり: {canWrite}");
        // 実行権限を追加
        permissions |= FileAccess.Execute;
        Console.WriteLine($"更新後の権限: {permissions}");
        // 読み取り権限を削除
        permissions &= ~FileAccess.Read;
        Console.WriteLine($"読み取り権限削除後: {permissions}");
    }
}
権限: Read, Write
書き込み権限あり: True
更新後の権限: Read, Write, Execute
読み取り権限削除後: Write, Execute

Flags属性を使う際のポイント

  • 値は2のべき乗で定義する

各フラグは重複しないビット位置(1, 2, 4, 8, 16…)を割り当てる必要があります。

  • Noneは0に設定する

すべてのフラグがOFFの状態を表すため、None = 0を定義しておくと便利です。

  • ビット演算で状態を操作する

|(OR)でフラグを追加、&(AND)でフラグの有無をチェック、& ~でフラグを削除します。

  • ToString()で複数フラグがカンマ区切りで表示される

[Flags]属性があると、複数のフラグが設定されている場合に名前がわかりやすく表示されます。

このように、[Flags]属性を活用することで、複数の状態を効率的に管理でき、コードの可読性と保守性が向上します。

構造体(struct)とレコード(struct record)

小さなデータパッケージで役立つ理由

C#のstruct(構造体)とstruct record(構造体レコード)は、複数の関連するデータをひとまとめにして扱うための値型です。

これらは特に「小さなデータパッケージ」として役立ち、効率的にメモリを使いながらデータのまとまりを表現できます。

値型としての効率性

構造体は値型であるため、変数に直接データが格納されます。

これにより、ヒープへの割り当てやガベージコレクションの負荷が軽減され、パフォーマンスが向上します。

特に小さなデータの集合を大量に扱う場合に効果的です。

using System;
struct Point
{
    public int X;
    public int Y;
}
class Program
{
    static void Main()
    {
        Point p1 = new Point { X = 10, Y = 20 };
        Point p2 = p1; // 値のコピー
        p2.X = 30;
        Console.WriteLine($"p1: X={p1.X}, Y={p1.Y}"); // 10, 20
        Console.WriteLine($"p2: X={p2.X}, Y={p2.Y}"); // 30, 20
    }
}
p1: X=10, Y=20
p2: X=30, Y=20

この例では、p2p1のコピーであり、独立した値を持つため、p2の変更はp1に影響しません。

クラスの参照型とは異なる挙動です。

struct recordの特徴

C# 10以降では、struct recordという構造体のレコード型が導入されました。

これは構造体の値型の特性を持ちながら、レコードの便利な機能(イミュータブル性、値の比較、with式など)を利用できます。

using System;
struct record PointRecord(int X, int Y);
class Program
{
    static void Main()
    {
        var p1 = new PointRecord(10, 20);
        var p2 = p1 with { X = 30 };
        Console.WriteLine($"p1: X={p1.X}, Y={p1.Y}"); // 10, 20
        Console.WriteLine($"p2: X={p2.X}, Y={p2.Y}"); // 30, 20
        Console.WriteLine($"p1 == p2: {p1 == p2}"); // False
    }
}
p1: X=10, Y=20
p2: X=30, Y=20
p1 == p2: False

struct recordは値の比較が自動的に実装され、with式で簡単に新しい値を作成できるため、データの不変性を保ちつつ効率的に扱えます。

小さなデータパッケージとしての利点

  • メモリ効率が良い

値型なのでスタックに割り当てられ、ヒープ割り当てやGCの負荷が減ります。

  • コピーが高速

小さなサイズのデータならコピーコストが低く、参照型のような参照の追跡が不要です。

  • イミュータブル設計がしやすい

struct recordを使うと、イミュータブルな小さなデータ構造を簡単に作れます。

  • 値の比較が直感的

レコードは値の内容で比較されるため、等価判定が簡単です。

  • API設計での明確な意図表現

小さなデータパッケージを使うことで、メソッドの引数や戻り値の意味が明確になります。

このように、structstruct recordは小さなデータのまとまりを効率的かつ安全に扱うための強力なツールです。

特にパフォーマンスが求められる場面や不変性を重視する設計で役立ちます。

共通言語仕様(CLS)と互換性

他言語と共有できる型

共通言語仕様(Common Language Specification、CLS)は、.NETプラットフォーム上で異なるプログラミング言語間の相互運用性を確保するためのルールセットです。

CLSに準拠した型や機能は、C#だけでなくVB.NETやF#など他の.NET対応言語でも共通して利用できます。

CLSに準拠した組み込み型は、基本的に以下のようなものが含まれます。

型名説明
bool真偽値
byte符号なし8ビット整数
charUnicode文字
decimal高精度10進数
double倍精度浮動小数点数
float単精度浮動小数点数
int符号付き32ビット整数
long符号付き64ビット整数
objectすべての型の基底
sbyte符号付き8ビット整数
short符号付き16ビット整数
string文字列
uint符号なし32ビット整数
ulong符号なし64ビット整数
ushort符号なし16ビット整数

これらの型はCLS準拠であるため、異なる言語間でのデータ受け渡しやAPIの呼び出しにおいて問題が起きにくいです。

例えば、C#で作成したライブラリのメソッドがVB.NETから呼び出される場合でも、CLS準拠の型を使っていればスムーズに連携できます。

非CLS準拠型の注意点

一方で、CLSに準拠していない型や機能を使うと、他の言語からの利用時に問題が発生する可能性があります。

代表的な非CLS準拠型には以下のようなものがあります。

  • 符号なし整数型の一部

uint, ulong, ushort, byte(符号なしの一部)はCLS非準拠とされています。

特にuintulongは他言語でサポートされていない場合があり、APIの公開時には注意が必要です。

  • ポインタ型

unsafeコードで使うポインタ型はCLS非準拠であり、他言語からは利用できません。

  • 特殊な型や機能

dynamic型や一部のジェネリック制約など、CLS仕様外の機能もあります。

非CLS準拠の型を使う場合は、以下の点に注意してください。

  • APIの公開範囲を限定する

ライブラリの公開APIで非CLS準拠型を使うと、他言語からの利用が制限されるため、必要に応じてCLS準拠の型に置き換えるか、非公開にすることが望ましいです。

  • [CLSCompliant(false)]属性の付与

非CLS準拠の型やメンバーには[CLSCompliant(false)]属性を付けて明示し、利用者に注意を促します。

using System;
[assembly: CLSCompliant(true)]
public class Sample
{
    // CLS準拠のメソッド
    public int Add(int a, int b) => a + b;
    // CLS非準拠のメソッド(uintを使用)
    [CLSCompliant(false)]
    public uint Multiply(uint a, uint b) => a * b;
}
  • ドキュメントでの明示

非CLS準拠の部分がある場合は、ドキュメントで明確に説明し、他言語からの利用時の注意点を伝えましょう。

このように、CLS準拠の型を使うことで.NETの多言語環境での互換性が保たれますが、非準拠型を使う場合は影響範囲を理解し、適切に管理することが重要です。

型変換とキャスト

暗黙的変換

暗黙的変換(implicit conversion)は、プログラマが明示的に指定しなくても、コンパイラが自動的に型を変換してくれる仕組みです。

主に「安全な変換」として、情報の損失や例外が発生しない場合に適用されます。

例えば、intからlongへの変換は暗黙的に行われます。

intは32ビットの符号付き整数、longは64ビットの符号付き整数なので、intの値はlongに安全に収まるためです。

using System;
class Program
{
    static void Main()
    {
        int intValue = 100;
        long longValue = intValue; // 暗黙的変換
        Console.WriteLine($"int値: {intValue}");
        Console.WriteLine($"long値: {longValue}");
    }
}
int値: 100
long値: 100

他にも、floatからdoubleへの変換や、派生クラスから基底クラスへの参照型の変換も暗黙的に行われます。

明示的変換

明示的変換(explicit conversion)は、プログラマがキャスト演算子を使って明示的に型変換を指示する方法です。

情報の損失や例外が発生する可能性がある場合に使います。

例えば、longからintへの変換は明示的にキャストしなければなりません。

longの値がintの範囲を超えるとデータが切り捨てられる可能性があるためです。

using System;
class Program
{
    static void Main()
    {
        long longValue = 100000;
        int intValue = (int)longValue; // 明示的変換
        Console.WriteLine($"long値: {longValue}");
        Console.WriteLine($"int値: {intValue}");
    }
}
long値: 100000
int値: 100000

ただし、longValueintの範囲を超えると、値が不正確になることがあります。

using System;
class Program
{
    static void Main()
    {
        long longValue = 3000000000; // intの最大値を超える
        int intValue = (int)longValue; // 明示的変換
        Console.WriteLine($"long値: {longValue}");
        Console.WriteLine($"int値: {intValue}"); // オーバーフローによる不正な値
    }
}
long値: 3000000000
int値: -1294967296

このように、明示的変換は注意が必要で、オーバーフローやデータ損失のリスクがあります。

checked / uncheckedでオーバーフロー制御

C#では、数値型の演算や型変換でオーバーフローが発生した場合の動作を制御するために、checkeduncheckedキーワードが用意されています。

  • checked

オーバーフローが発生するとOverflowExceptionをスローし、エラーとして検出します。

  • unchecked

オーバーフローが発生しても例外をスローせず、値がラップアラウンド(循環)します。

checkedの例

using System;
class Program
{
    static void Main()
    {
        try
        {
            int max = int.MaxValue;
            int result = checked(max + 1); // オーバーフロー検出
            Console.WriteLine($"結果: {result}");
        }
        catch (OverflowException ex)
        {
            Console.WriteLine($"オーバーフロー例外発生: {ex.Message}");
        }
    }
}
オーバーフロー例外発生: 算術演算でオーバーフローが発生しました。

uncheckedの例

using System;
class Program
{
    static void Main()
    {
        int max = int.MaxValue;
        int result = unchecked(max + 1); // オーバーフローを無視
        Console.WriteLine($"結果: {result}"); // ラップアラウンドした値
    }
}
結果: -2147483648

コンパイル時の設定

Visual Studioなどの開発環境では、プロジェクトのビルド設定で「整数のオーバーフローをチェックする」オプションを有効にすると、デフォルトでchecked状態になります。

無効にするとuncheckedがデフォルトです。

  • 暗黙的変換は安全な型変換で自動的に行われます
  • 明示的変換はキャスト演算子を使い、情報損失の可能性がある場合に必要でしょう
  • checked/uncheckedでオーバーフロー時の動作を制御し、バグの早期発見や意図的なラップアラウンドを実現できます

これらを理解し適切に使い分けることで、安全かつ効率的な型変換が可能になります。

文字列表記とリテラル

数値リテラルの書き方

C#では数値をコード内に直接記述する際に「リテラル」として表現します。

数値リテラルは整数や浮動小数点数などの型に応じて様々な書き方があり、型指定のための接尾辞や桁区切りのアンダースコアを使うこともできます。

整数リテラル

整数リテラルは基本的に10進数で書きますが、型を明示するために接尾辞を付けることができます。

リテラル例説明
123intデフォルトはint
123L または 123llong64ビット整数
123U または 123uuint符号なし32ビット整数
123ULulong符号なし64ビット整数

また、桁区切りにアンダースコア_を使うことで読みやすくできます。

int largeNumber = 1_000_000;
long bigNumber = 9_223_372_036_854_775_807L;
Console.WriteLine(largeNumber);
Console.WriteLine(bigNumber);
1000000
9223372036854775807

浮動小数点リテラル

浮動小数点数は小数点を含むか、指数表記を使います。

型指定のために接尾辞を使うこともあります。

リテラル例説明
3.14doubleデフォルトはdouble
3.14F または 3.14ffloat単精度浮動小数点数
3.14M または 3.14mdecimal高精度10進数
1.5e2double指数表記(1.5 × 10^2 = 150)
float f = 3.14f;
double d = 1.5e2;
decimal m = 3.14m;
Console.WriteLine(f);
Console.WriteLine(d);
Console.WriteLine(m);
3.14
150
3.14

バイナリ・十六進リテラル

C# 7.0以降では、整数リテラルをバイナリ(2進数)や十六進数(16進数)で表記できます。

これにより、ビット操作やフラグの設定が直感的に書けるようになりました。

十六進リテラル

0xまたは0Xを接頭辞として使います。

int hexValue = 0xFF; // 255
Console.WriteLine(hexValue);
255

バイナリリテラル

0bまたは0Bを接頭辞として使います。

int binaryValue = 0b1010_1010; // 170
Console.WriteLine(binaryValue);
170

桁区切りのアンダースコアも使えるため、長いビット列も読みやすくなります。

文字列リテラルとエスケープ

文字列リテラルはダブルクォーテーション"で囲みます。

文字列内に特殊文字や改行、タブなどを含める場合はエスケープシーケンスを使います。

基本的なエスケープシーケンス

エスケープシーケンス意味
\\バックスラッシュ
\"ダブルクォーテーション
\n改行
\r復帰
\tタブ
\0ヌル文字
\uXXXXUnicode文字(16進4桁)
string path = "C:\\Users\\Public";
string quote = "彼は言った、\"こんにちは\"";
string multiline = "1行目\n2行目";
Console.WriteLine(path);
Console.WriteLine(quote);
Console.WriteLine(multiline);
C:\Users\Public
彼は言った、"こんにちは"
1行目
2行目

@記号を使った逐語的文字列リテラル

@を文字列の前に付けると、エスケープシーケンスを無効にし、改行やバックスラッシュをそのまま文字列として扱えます。

主にファイルパスや複数行文字列で使います。

string verbatimPath = @"C:\Users\Public";
string multiLine = @"1行目
2行目";
Console.WriteLine(verbatimPath);
Console.WriteLine(multiLine);
C:\Users\Public
1行目
2行目

ただし、"を文字列内に含めたい場合は""と2つ連続で書きます。

string quote = @"彼は言った、""こんにちは""";
Console.WriteLine(quote);
彼は言った、"こんにちは"

このように、C#の文字列表記とリテラルは多彩な表現が可能で、用途に応じて使い分けることでコードの可読性や保守性が向上します。

メモリ配置とパフォーマンス

スタックとヒープの使い分け

C#のメモリ管理は主に「スタック」と「ヒープ」の2つの領域で行われます。

これらの使い分けを理解することは、パフォーマンス最適化において非常に重要です。

  • スタック

スタックは関数呼び出し時にローカル変数や値型のデータを格納する高速なメモリ領域です。

LIFO(後入れ先出し)構造で管理され、割り当てと解放が非常に高速に行われます。

値型(intstructなど)は基本的にスタックに配置されます。

  • ヒープ

ヒープは参照型のオブジェクトが格納されるメモリ領域で、動的に割り当てられます。

ガベージコレクション(GC)によって不要になったオブジェクトが自動的に解放されます。

ヒープはスタックに比べて割り当てや解放にコストがかかります。

using System;
struct Point
{
    public int X;
    public int Y;
}
class Program
{
    static void Main()
    {
        Point p = new Point { X = 10, Y = 20 }; // スタックに配置
        object obj = p; // ボックス化されヒープに配置
        Console.WriteLine($"Point: X={p.X}, Y={p.Y}");
        Console.WriteLine($"Boxed object: {obj}");
    }
}
Point: X=10, Y=20
Boxed object: Point

この例では、Point構造体はスタックに配置されますが、objectに代入するとボックス化されヒープに配置されます。

スタックは高速ですがサイズが小さく、ヒープは大きいが管理コストが高いという特徴があります。

StructLayoutで制御できるケース

StructLayout属性を使うと、構造体のメモリ配置を制御できます。

特にアンマネージドコードとの相互運用やパフォーマンスチューニングで役立ちます。

主なStructLayoutのオプションは以下の通りです。

  • LayoutKind.Sequential

フィールドを宣言順にメモリに並べます。

デフォルトで多くの構造体に使われます。

  • LayoutKind.Explicit

フィールドごとに明示的にメモリ上のオフセットを指定できます。

  • LayoutKind.Auto

コンパイラやランタイムに最適な配置を任せる(アンマネージドコードとの相互運用には不向き)。

using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Explicit)]
struct ExplicitLayout
{
    [FieldOffset(0)]
    public int IntValue;
    [FieldOffset(0)]
    public byte ByteValue;
}
class Program
{
    static void Main()
    {
        ExplicitLayout data = new ExplicitLayout();
        data.IntValue = 0x12345678;
        Console.WriteLine($"IntValue: 0x{data.IntValue:X}");
        Console.WriteLine($"ByteValue: 0x{data.ByteValue:X}");
    }
}
IntValue: 0x12345678
ByteValue: 0x78

この例では、IntValueByteValueが同じメモリ位置を共有しており、ByteValueIntValueの最下位バイトを参照しています。

こうした制御は低レベルのメモリ操作やネイティブAPIとの連携で重要です。

GCの影響を最小化するテクニック

ガベージコレクション(GC)はヒープ上の不要なオブジェクトを自動的に回収しますが、頻繁なGCはパフォーマンス低下の原因になります。

GCの影響を最小化するためのテクニックを紹介します。

  • 値型を活用する

値型はスタックに配置されるため、GCの対象になりません。

小さなデータは構造体で表現し、ボックス化を避けることでGC負荷を減らせます。

  • オブジェクトの再利用

頻繁に生成・破棄されるオブジェクトはプール(オブジェクトプーリング)を使って再利用し、GC発生を抑制します。

  • 大きなオブジェクトの管理

大きな配列や文字列は大オブジェクトヒープ(LOH)に割り当てられ、GCのコストが高いです。

必要以上に大きなオブジェクトを作らないように設計します。

  • usingDisposeでリソース解放

ネイティブリソースを持つオブジェクトはIDisposableを実装し、早期に解放してGC負荷を軽減します。

  • GC.Collect()の乱用を避ける

明示的なGC呼び出しはパフォーマンスを悪化させることが多いため、通常はランタイムに任せるべきです。

  • イミュータブルオブジェクトの適切な設計

不変オブジェクトは使い回しやすいですが、頻繁な新規生成はGC負荷を増やすため注意が必要です。

これらのテクニックを活用し、メモリ管理とGCの影響を最小限に抑えることで、C#アプリケーションのパフォーマンスを向上させられます。

テストとデバッグ視点の型選択

Unit Testでの境界値チェック

ソフトウェアの品質を保つために、Unit Test(単体テスト)は欠かせません。

特に数値型や文字列型などのデータを扱う際は、境界値チェックが重要です。

境界値とは、型が取りうる最小値や最大値、あるいは特定の閾値の近辺の値を指し、バグが発生しやすいポイントです。

例えば、整数型intの境界値はint.MinValue(-2,147,483,648)とint.MaxValue(2,147,483,647)です。

これらの値やその近辺での動作をテストすることで、オーバーフローや不正な計算を防げます。

using System;
using NUnit.Framework;
public class Calculator
{
    public int Add(int a, int b) => checked(a + b);
}
[TestFixture]
public class CalculatorTests
{
    private Calculator calculator;
    [SetUp]
    public void Setup()
    {
        calculator = new Calculator();
    }
    [Test]
    public void Add_WithMaxValues_ThrowsOverflowException()
    {
        Assert.Throws<OverflowException>(() => calculator.Add(int.MaxValue, 1));
    }
    [Test]
    public void Add_WithMinValues_ThrowsOverflowException()
    {
        Assert.Throws<OverflowException>(() => calculator.Add(int.MinValue, -1));
    }
    [Test]
    public void Add_NormalValues_ReturnsCorrectSum()
    {
        int result = calculator.Add(100, 200);
        Assert.AreEqual(300, result);
    }
}

この例では、Addメソッドに対して最大値や最小値を使った加算でオーバーフローが発生するかをテストしています。

境界値テストを行うことで、型の制約や例外処理が正しく機能しているかを検証できます。

境界値チェックは以下のようなケースで特に重要です。

  • 数値型の最大・最小値
  • 配列やコレクションのインデックスの範囲
  • 文字列の長さの上限や空文字列
  • Nullable型のnullと非nullの切り替え

適切な型選択と境界値テストの組み合わせで、堅牢なコードを実現しましょう。

デバッグ表示を読みやすくする属性

デバッグ時にオブジェクトの状態を確認しやすくするために、C#ではいくつかの属性を使ってデバッグ表示をカスタマイズできます。

これにより、複雑な型の内容を簡潔かつ分かりやすく表示でき、デバッグ効率が向上します。

[DebuggerDisplay]属性

[DebuggerDisplay]属性は、デバッガでオブジェクトを表示する際の文字列を指定できます。

プロパティやフィールドの値を埋め込むことも可能です。

using System;
using System.Diagnostics;
[DebuggerDisplay("Point: ({X}, {Y})")]
public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}
class Program
{
    static void Main()
    {
        Point p = new Point { X = 10, Y = 20 };
        Console.WriteLine(p);
    }
}

デバッガでPoint型の変数を確認すると、Point: (10, 20)と表示され、フィールドの中身が一目でわかります。

[DebuggerTypeProxy]属性

複雑な型の場合、[DebuggerTypeProxy]属性を使って、デバッグ時に別の「プロキシ」クラスを表示させることができます。

これにより、内部構造を整理して見やすくできます。

using System;
using System.Collections.Generic;
using System.Diagnostics;
[DebuggerTypeProxy(typeof(MyCollectionDebugView))]
public class MyCollection<T>
{
    private List<T> items = new List<T>();
    public void Add(T item) => items.Add(item);
    public int Count => items.Count;
    public T this[int index] => items[index];
    private class MyCollectionDebugView
    {
        private MyCollection<T> collection;
        public MyCollectionDebugView(MyCollection<T> collection)
        {
            this.collection = collection;
        }
        [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)]
        public T[] Items => collection.items.ToArray();
    }
}
class Program
{
    static void Main()
    {
        var collection = new MyCollection<int>();
        collection.Add(1);
        collection.Add(2);
        collection.Add(3);
        Console.WriteLine($"Count: {collection.Count}");
    }
}

デバッガでMyCollectionの中身を見ると、Items配列が直接展開され、要素が見やすくなります。

[DebuggerBrowsable]属性

[DebuggerBrowsable]属性は、デバッグ時に特定のメンバーの表示方法を制御します。

例えば、非表示にしたり、展開を制限したりできます。

using System;
using System.Diagnostics;
public class Person
{
    public string Name { get; set; }
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public string Secret { get; set; }
}
class Program
{
    static void Main()
    {
        var person = new Person { Name = "太郎", Secret = "秘密" };
        Console.WriteLine(person.Name);
    }
}

この場合、Secretプロパティはデバッガで表示されません。

これらの属性を活用することで、デバッグ時の情報把握がスムーズになり、問題の特定や修正が効率的に行えます。

型選択と合わせて、テストやデバッグの観点からも設計を考えることが重要です。

よくある落とし穴

オーバーフローとアンダーフロー

数値型の計算でよく起こる問題の一つがオーバーフローとアンダーフローです。

オーバーフローは、変数が表現できる最大値を超えた場合に発生し、アンダーフローは最小値を下回った場合に発生します。

オーバーフローの例

using System;
class Program
{
    static void Main()
    {
        int max = int.MaxValue;
        int result = unchecked(max + 1); // オーバーフローして値がラップアラウンド
        Console.WriteLine($"int.MaxValue: {max}");
        Console.WriteLine($"オーバーフロー後の値: {result}");
    }
}
int.MaxValue: 2147483647
オーバーフロー後の値: -2147483648

この例では、int.MaxValueに1を足すと、unchecked環境では値が最小値に戻る(ラップアラウンド)ため、予期しない負の値になります。

アンダーフローの例

アンダーフローは主に浮動小数点数で起こり、非常に小さい値がゼロに丸められる現象です。

using System;
class Program
{
    static void Main()
    {
        double small = 1e-320;
        double result = small / 10;
        Console.WriteLine($"元の値: {small}");
        Console.WriteLine($"アンダーフロー後の値: {result}");
    }
}
元の値: 1E-320
アンダーフロー後の値: 0

このように、非常に小さい値をさらに小さくするとゼロに丸められ、計算結果に影響を与えます。

対策

  • checkedキーワードを使い、オーバーフローを検出して例外を発生させます
  • 浮動小数点の計算では丸め誤差やアンダーフローを考慮し、適切な精度や型を選ぶ
  • 境界値テストを行い、異常値の扱いを確認します

浮動小数点の比較

浮動小数点数floatdoubleは内部的に2進数で表現されるため、10進数の値を正確に表現できず、比較で予期しない結果になることがあります。

using System;
class Program
{
    static void Main()
    {
        double a = 0.1 + 0.2;
        double b = 0.3;
        Console.WriteLine($"a == b: {a == b}");
        Console.WriteLine($"a: {a}");
        Console.WriteLine($"b: {b}");
    }
}
a == b: False
a: 0.30000000000000004
b: 0.3

この例では、0.1 + 0.2が厳密に0.3にならず、比較がfalseになります。

対策

  • 直接の等価比較を避け、許容誤差(イプシロン)を使った比較を行います
bool AreEqual(double x, double y, double epsilon = 1e-10)
{
    return Math.Abs(x - y) < epsilon;
}
  • 金額計算など誤差を許容できない場合はdecimal型を使います

文字列の参照比較違い

C#の文字列は参照型ですが、==演算子は内容の比較を行うようにオーバーロードされています。

一方で、Object.ReferenceEqualsは参照の同一性を比較します。

この違いを理解しないと、意図しない比較結果になることがあります。

using System;
class Program
{
    static void Main()
    {
        string s1 = "hello";
        string s2 = "hello";
        string s3 = new string(new char[] { 'h', 'e', 'l', 'l', 'o' });
        Console.WriteLine($"s1 == s2: {s1 == s2}"); // true(内容比較)
        Console.WriteLine($"s1 == s3: {s1 == s3}"); // true(内容比較)
        Console.WriteLine($"ReferenceEquals(s1, s2): {ReferenceEquals(s1, s2)}"); // true(同じリテラル)
        Console.WriteLine($"ReferenceEquals(s1, s3): {ReferenceEquals(s1, s3)}"); // false(別オブジェクト)
    }
}
s1 == s2: True
s1 == s3: True
ReferenceEquals(s1, s2): True
ReferenceEquals(s1, s3): False

対策

  • 文字列の内容比較には==string.Equalsを使います
  • 参照の同一性を比較したい場合はReferenceEqualsを使います
  • 文字列の比較で大文字小文字や文化依存を考慮する場合は、string.EqualsのオーバーロードでStringComparisonを指定します

これらの落とし穴はC#の型やメモリ表現の特性に起因するため、理解しておくことでバグの発生を防ぎ、正確で安全なプログラムを書くことができます。

バージョン別の追加・変更点

C# 7以降の新しい組み込み型機能

C# 7.0以降、組み込み型に関して多くの新機能や改善が導入され、より表現力豊かで効率的なコードが書けるようになりました。

特に注目すべき機能をいくつか紹介します。

タプルの強化(ValueTuple)

C# 7.0で導入されたValueTupleは、複数の値をまとめて返す際に使いやすい値型のタプルです。

従来のSystem.Tupleは参照型でパフォーマンス面で劣っていましたが、ValueTupleはスタック上に割り当てられ高速です。

var person = (Name: "山田太郎", Age: 30);
Console.WriteLine($"名前: {person.Name}, 年齢: {person.Age}");

パターンマッチング

C# 7.0以降、is演算子やswitch文で型や値に基づくパターンマッチングが可能になりました。

これにより、型チェックとキャストを簡潔に書けます。

object obj = 123;
if (obj is int number)
{
    Console.WriteLine($"整数値: {number}");
}

ローカル関数

メソッド内にローカル関数を定義できるようになり、スコープを限定した関数を作成可能です。

これにより、コードの可読性と保守性が向上します。

int Add(int x, int y) => x + y;

refローカル変数とref戻り値

C# 7.0では、変数を参照として扱うrefローカル変数や、参照を返すref戻り値がサポートされ、パフォーマンス向上やメモリ効率の改善に役立ちます。

ref int Find(int[] array, int value)
{
    for (int i = 0; i < array.Length; i++)
    {
        if (array[i] == value)
            return ref array[i];
    }
    throw new IndexOutOfRangeException();
}

nullable参照型(C# 8.0)

C# 8.0で導入されたnullable参照型機能により、参照型に対してnull許容か非許容かを明示的に指定できるようになりました。

これにより、null参照例外の発生をコンパイル時に検出しやすくなります。

string? nullableString = null; // null許容
string nonNullableString = "Hello"; // null非許容

最新バージョンで注目すべきポイント

C#の最新バージョン(9.0以降)では、組み込み型や型システムに関してさらに多くの機能強化が行われています。

レコード型(Record Types)

C# 9.0で導入されたレコード型は、イミュータブルなデータオブジェクトを簡単に定義できる新しい型です。

値の比較やコピーが容易で、DTOや設定オブジェクトに最適です。

public record Person(string Name, int Age);

initアクセサ

プロパティの初期化専用セッターinitが追加され、オブジェクト初期化時のみ値を設定可能に。

イミュータブルな設計がしやすくなりました。

public class Person
{
    public string Name { get; init; }
    public int Age { get; init; }
}

拡張されたパターンマッチング

最新のC#では、論理パターンand, or, notやリストパターンなど、より複雑なパターンマッチングが可能になり、条件分岐の表現力が向上しています。

if (obj is int i and > 0)
{
    Console.WriteLine($"正の整数: {i}");
}

with式

レコード型やstruct recordで、既存オブジェクトの一部だけを変更した新しいオブジェクトを簡単に作成できるwith式が導入されました。

var p1 = new Person("太郎", 30);
var p2 = p1 with { Age = 31 };

static abstractメンバー(C# 11)

インターフェースで静的抽象メンバーを定義できるようになり、ジェネリックプログラミングの表現力が向上しました。

これにより、型に依存した静的メソッドの呼び出しが可能になります。

これらの新機能は、組み込み型の使い勝手や型安全性、パフォーマンスを向上させるだけでなく、コードの可読性や保守性も大きく改善しています。

最新のC#バージョンを活用することで、よりモダンで堅牢なアプリケーション開発が可能になります。

型安全を高める設計指針

適切なデフォルト値設定

型安全な設計を行う上で、変数やフィールドに設定するデフォルト値は非常に重要です。

C#では値型は自動的にゼロやfalse'\0'などのデフォルト値が割り当てられますが、これが必ずしも意味のある値とは限りません。

適切なデフォルト値を設定することで、意図しない動作やバグを防ぎやすくなります。

明示的な初期化

クラスや構造体のフィールドは、コンストラクタや初期化子で明示的に初期化しましょう。

特に、意味のある初期値がある場合は必ず設定することが望ましいです。

class User
{
    public int Id { get; set; } = -1; // 未設定を示す特別な値
    public string Name { get; set; } = string.Empty; // null回避のため空文字列で初期化
    public User() { }
}

この例では、Id-1を設定して「未設定」を明示し、Namenullを避けるため空文字列で初期化しています。

こうすることで、後続の処理で未初期化の値を誤って使うリスクを減らせます。

Nullable型の活用

値型で「値が設定されていない」状態を表現したい場合は、Nullable<T>T?を使うのが適切です。

これにより、デフォルト値と区別してnullを扱えます。

int? optionalValue = null;
if (optionalValue.HasValue)
{
    Console.WriteLine($"値は {optionalValue.Value}");
}
else
{
    Console.WriteLine("値は未設定です");
}

デフォルト値の意味をドキュメント化

特別なデフォルト値を使う場合は、その意味をコメントやドキュメントで明確にしておくことが重要です。

これにより、他の開発者がコードを理解しやすくなります。

nullを許容しないAPI設計

API設計において、nullを許容するかどうかは型安全性に大きく影響します。

nullを許容すると、呼び出し側でnullチェックを怠った場合にNullReferenceExceptionが発生しやすくなります。

これを防ぐために、可能な限りnullを許容しない設計を心がけましょう。

参照型のnull許容性を明示する

C# 8.0以降のnullable参照型機能を活用し、nullを許容する型には?を付けて明示的に区別します。

public string Name { get; set; } = string.Empty; // null非許容
public string? Nickname { get; set; } // null許容

これにより、コンパイラがnullチェックを強制し、null参照のリスクを減らせます。

メソッドの引数と戻り値でnullを制御する

メソッドのパラメータや戻り値にnullを許容する場合は、明示的に?を付けて示します。

許容しない場合はnullを渡せないようにし、呼び出し側の安全性を高めます。

public void SetName(string name) // null非許容
{
    if (string.IsNullOrEmpty(name))
        throw new ArgumentException("名前は必須です。", nameof(name));
    Name = name;
}
public string? GetNickname() // null許容
{
    return Nickname;
}

nullチェックの徹底

API内部では、nullを許容しないパラメータに対しては早期にArgumentNullExceptionをスローするなど、堅牢なチェックを行います。

public void ProcessData(string data)
{
    if (data == null) throw new ArgumentNullException(nameof(data));
    // 処理続行
}

Optionalパラメータやオーバーロードでの工夫

nullを使わずにオプションの値を表現したい場合は、オーバーロードやNullable<T>を使う方法もあります。

public void Save(string data) => Save(data, overwrite: false);
public void Save(string data, bool overwrite)
{
    // 処理
}

これらの設計指針を守ることで、型安全性が向上し、null関連のバグを減らせます。

特に大規模開発やチーム開発では、明確なルールとコンパイラの支援を活用したAPI設計が重要です。

まとめ

この記事では、C#の組み込み型の基本から最新機能、メモリ管理、型安全設計まで幅広く解説しました。

整数型や浮動小数点型の特徴、文字列や真偽値型の扱い方、特殊型の使いどころを理解できます。

さらに、パフォーマンス最適化やテスト・デバッグ視点での型選択、よくある落とし穴の回避方法も紹介。

最新のC#機能や型安全を高める設計指針を押さえることで、堅牢で効率的なプログラム作成に役立ちます。

関連記事

Back to top button
目次へ