C# チートシート

cheatsheetプログラミング

変数とデータ型 🔢

変数の宣言と初期化、基本的なデータ型、型推論の使用法。

基本的な変数の宣言と初期化


// 型を明示的に指定
int age = 30;
string name = "山田 太郎";
double weight = 65.5;
bool isEnabled = true;
char initial = 'Y';
decimal price = 1980.50m; // 金額計算にはdecimal推奨

// 宣言と初期化を分ける
int score;
score = 100;

// 定数 (再代入不可)
const double Pi = 3.14159;
      

型推論 (varキーワード)

コンパイラが初期化式から型を推論します。ローカル変数でのみ使用可能です。


// コンパイラが int型と推論
var count = 10;

// コンパイラが string型と推論
var message = "こんにちは";

// オブジェクトのインスタンス化
var person = new Person("鈴木 次郎", 25);

// 注意: 初期化と同時に行う必要がある
// var value; // これはコンパイルエラー
// value = 100;

// nullでの初期化はできない (型が決定できないため)
// var data = null; // コンパイルエラー
      

Nullable 値型

値型 (int, bool, struct など) が null を許容できるようにします。


// Nullable<T> 構文
Nullable<int> nullableInt = null;

// 短縮構文 (?)
int? nullableAge = 30;
bool? isConfirmed = null;
double? nullableValue = null;

// 値の確認と取得
if (nullableAge.HasValue)
{
    Console.WriteLine($"年齢: {nullableAge.Value}");
}
else
{
    Console.WriteLine("年齢は未設定です。");
}

// null合体演算子 (??)
int ageOrDefault = nullableAge ?? 0; // nullなら0を代入

// null条件演算子 (?.) と組み合わせる
int? length = name?.Length; // nameがnullでなければLengthを取得、nullならnull
      

主要な組み込みデータ型

.NET 型 説明 デフォルト値
bool System.Boolean 真偽値 (true または false) false
byte System.Byte 8ビット符号なし整数 (0 から 255) 0
sbyte System.SByte 8ビット符号付き整数 (-128 から 127) 0
char System.Char 16ビット Unicode 文字 '\0'
decimal System.Decimal 128ビット高精度十進数 (金融計算に適) 0.0m
double System.Double 64ビット倍精度浮動小数点数 0.0d
float System.Single 32ビット単精度浮動小数点数 0.0f
int System.Int32 32ビット符号付き整数 0
uint System.UInt32 32ビット符号なし整数 0
long System.Int64 64ビット符号付き整数 0L
ulong System.UInt64 64ビット符号なし整数 0
short System.Int16 16ビット符号付き整数 0
ushort System.UInt16 16ビット符号なし整数 0
string System.String Unicode 文字列 (参照型) null
object System.Object 全ての型の基底クラス (参照型) null

制御構文 ⚙️

条件分岐、繰り返し、ジャンプステートメントの基本的な使い方。

条件分岐 (if-else)


int temperature = 25;

if (temperature >= 30)
{
    Console.WriteLine("真夏日です。🥵");
}
else if (temperature >= 25)
{
    Console.WriteLine("夏日です。☀️");
}
else if (temperature < 10)
{
    Console.WriteLine("寒いです。🥶");
}
else
{
    Console.WriteLine("過ごしやすい気温です。😊");
}

// 三項演算子 (条件 ? 真の場合 : 偽の場合)
string weather = temperature >= 25 ? "暑い" : "暑くない";
Console.WriteLine($"天気は {weather} です。");
      

条件分岐 (switch)

特定の変数の値に基づいて処理を分岐します。C# 7.0以降、パターンマッチングが強化されています。


// 基本的なswitch文
int dayOfWeek = 3; // 1:月, 2:火, ... 7:日
string dayName;
switch (dayOfWeek)
{
    case 1:
        dayName = "月曜日";
        break;
    case 2:
        dayName = "火曜日";
        break;
    case 3:
        dayName = "水曜日";
        break;
    // ... 他の曜日 ...
    case 6:
    case 7:
        dayName = "週末";
        break; // 複数のcaseをまとめる
    default:
        dayName = "不明な曜日";
        break;
}
Console.WriteLine(dayName);

// パターンマッチング (C# 7.0以降)
object shape = new Circle(5); // Circleは図形クラスとする
switch (shape)
{
    case Circle c when c.Radius > 10: // 型パターン + when句
        Console.WriteLine($"半径{c.Radius}の大きな円です。");
        break;
    case Circle c:
        Console.WriteLine($"半径{c.Radius}の円です。");
        break;
    case Square s when s.Side < 5: // Squareも図形クラスとする
         Console.WriteLine($"一辺{s.Side}の小さな正方形です。");
        break;
    case Square s:
        Console.WriteLine($"一辺{s.Side}の正方形です。");
        break;
    case null:
        Console.WriteLine("図形がnullです。");
        break;
    default:
        Console.WriteLine("未知の図形です。");
        break;
}

// switch式 (C# 8.0以降) - 結果を返す式形式
string description = shape switch
{
    Circle c when c.Radius > 10 => $"半径{c.Radius}の大きな円",
    Circle c                    => $"半径{c.Radius}の円",
    Square s when s.Side < 5    => $"一辺{s.Side}の小さな正方形",
    Square s                    => $"一辺{s.Side}の正方形",
    null                        => "図形がnull",
    _                           => "未知の図形" // defaultの代わり
};
Console.WriteLine(description);
      

繰り返し (for)

指定回数、処理を繰り返します。


// 0から4まで5回繰り返す
for (int i = 0; i < 5; i++)
{
    Console.WriteLine($"現在の i: {i}");
}

// 複数の初期化子と反復子 (あまり一般的ではない)
for (int i = 0, j = 10; i < j; i++, j--)
{
    Console.WriteLine($"i={i}, j={j}");
}

// 無限ループ (条件式を省略)
// for (;;) { /* 処理... breakで抜ける */ }
      

繰り返し (while)

条件が真 (true) の間、処理を繰り返します。最初に条件を評価します。


int counter = 0;
while (counter < 3)
{
    Console.WriteLine($"while ループ: {counter}");
    counter++;
}
      

繰り返し (do-while)

少なくとも1回は処理を実行し、その後、条件が真 (true) の間、処理を繰り返します。


int anotherCounter = 0;
do
{
    Console.WriteLine($"do-while ループ: {anotherCounter}");
    anotherCounter++;
} while (anotherCounter < 3);

// 条件が最初から偽でも1回は実行される
int yetAnotherCounter = 5;
do
{
     Console.WriteLine($"do-while 実行確認: {yetAnotherCounter}"); // これは1回実行される
     yetAnotherCounter++;
} while (yetAnotherCounter < 3);
      

繰り返し (foreach)

コレクション (配列、リストなど) の各要素に対して処理を繰り返します。


string[] fruits = { "りんご", "ばなな", "みかん" };
foreach (string fruit in fruits)
{
    Console.WriteLine($"果物: {fruit}");
}

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
foreach (var number in numbers)
{
    Console.WriteLine($"数字: {number}");
}

// Dictionaryの場合 (KeyとValueを取得)
Dictionary<string, string> capitals = new Dictionary<string, string>
{
    { "日本", "東京" },
    { "アメリカ", "ワシントンD.C." }
};
foreach (KeyValuePair<string, string> pair in capitals)
{
    Console.WriteLine($"国: {pair.Key}, 首都: {pair.Value}");
}
// var を使うとより簡潔に
foreach (var pair in capitals)
{
     Console.WriteLine($"国: {pair.Key}, 首都: {pair.Value}");
}
      

ジャンプステートメント

ステートメント 説明 主な使用箇所
break 現在のループ (for, while, do-while, foreach) または switch 文を終了します。 ループ、switch
continue 現在のループの残りの処理をスキップし、次のイテレーションに進みます。 ループ
return 現在のメソッドの実行を終了し、呼び出し元に制御を戻します。必要に応じて戻り値を返します。 メソッド
goto プログラム内のラベル付きステートメントに直接ジャンプします。(使用は非推奨 ⚠️) 通常は避けるべき
throw 例外を発生させます。 メソッド、try-catchブロック

// break の例
for (int i = 0; i < 10; i++)
{
    if (i == 5)
    {
        Console.WriteLine("5でループを抜けます。");
        break; // ループ終了
    }
    Console.WriteLine(i);
}

// continue の例
for (int i = 0; i < 5; i++)
{
    if (i % 2 == 0) // 偶数の場合
    {
        continue; // 以降の処理をスキップして次のiへ
    }
    Console.WriteLine($"奇数: {i}");
}
      

メソッドと関数 🔧

メソッドの定義、呼び出し、引数、戻り値、オーバーロード、ローカル関数などの基本。

基本的なメソッド定義と呼び出し


// 戻り値なし、引数なし
void SayHello()
{
    Console.WriteLine("こんにちは!");
}

// メソッドの呼び出し
SayHello();

// 戻り値あり、引数あり
int Add(int a, int b)
{
    return a + b;
}

// メソッドの呼び出しと戻り値の利用
int sum = Add(5, 3);
Console.WriteLine($"合計: {sum}"); // 出力: 合計: 8

// 式形式のメソッド (C# 6.0以降) - 単一の式で構成されるメソッド
int Multiply(int x, int y) => x * y;
void PrintMessage(string msg) => Console.WriteLine(msg);

int product = Multiply(4, 5);
PrintMessage($"積: {product}"); // 出力: 積: 20
      

引数の種類

種類 説明
値渡し (デフォルト) メソッドに渡されるのは引数のコピー。メソッド内での変更は呼び出し元の変数に影響しない(値型の場合)。 void MyMethod(int x)
参照渡し (ref) メソッドに引数の参照を渡す。メソッド内での変更が呼び出し元の変数に影響する。呼び出し元で初期化が必要。 void MyMethod(ref int x)
出力引数 (out) メソッドから複数の値を返すために使用。メソッド内で必ず値を代入する必要がある。呼び出し元での初期化は不要。 bool TryParse(string s, out int result)
入力参照渡し (in) 引数を参照で渡すが、メソッド内で変更できないことを保証する(パフォーマンス向上のため)。主に大きな構造体で使用。 void MyMethod(in MyStruct data)
パラメータ配列 (params) 可変長の引数を配列として受け取る。メソッドの引数リストの最後に1つだけ定義できる。 int Sum(params int[] numbers)
オプション引数 メソッド定義時にデフォルト値を指定できる引数。呼び出し時に省略可能。 void MyMethod(int required, int optional = 10)
名前付き引数 呼び出し時に引数名を指定して値を渡す。引数の順序を自由に変更できる。オプション引数と組み合わせることが多い。 MyMethod(optional: 5, required: 1);

// ref の例
void ModifyValue(ref int number)
{
    number = number * 2;
}
int val = 5;
ModifyValue(ref val);
Console.WriteLine($"ref 後の値: {val}"); // 出力: ref 後の値: 10

// out の例
bool TryDivide(int dividend, int divisor, out double result)
{
    result = 0; // out引数はメソッド内で初期化必須
    if (divisor == 0)
    {
        return false;
    }
    result = (double)dividend / divisor;
    return true;
}
if (TryDivide(10, 2, out double divResult))
{
    Console.WriteLine($"除算結果: {divResult}"); // 出力: 除算結果: 5
}
// out 変数宣言のインライン化 (C# 7.0以降)
if (TryDivide(10, 0, out double divResult2)) { /* ... */ }
else { Console.WriteLine("0では除算できません。"); }

// params の例
int CalculateSum(params int[] values)
{
    int total = 0;
    foreach (int value in values)
    {
        total += value;
    }
    return total;
}
int totalSum = CalculateSum(1, 2, 3, 4, 5); // 可変長の引数を渡す
Console.WriteLine($"Params 合計: {totalSum}"); // 出力: Params 合計: 15
int singleSum = CalculateSum(10); // 引数1つでもOK
Console.WriteLine($"Params 合計 (単一): {singleSum}"); // 出力: Params 合計 (単一): 10
int arraySum = CalculateSum(new int[] { 10, 20 }); // 配列を渡すことも可能
Console.WriteLine($"Params 合計 (配列): {arraySum}"); // 出力: Params 合計 (配列): 30


// オプション引数と名前付き引数の例
void PrintUserDetails(string name, int age = 30, string city = "未設定")
{
    Console.WriteLine($"名前: {name}, 年齢: {age}, 都市: {city}");
}
PrintUserDetails("山田"); // 出力: 名前: 山田, 年齢: 30, 都市: 未設定
PrintUserDetails("鈴木", 25); // 出力: 名前: 鈴木, 年齢: 25, 都市: 未設定
PrintUserDetails("佐藤", city: "大阪"); // 名前付き引数でcityのみ指定、ageはデフォルト値
// 出力: 名前: 佐藤, 年齢: 30, 都市: 大阪
PrintUserDetails(city: "福岡", name: "田中"); // 名前付き引数で順序変更
// 出力: 名前: 田中, 年齢: 30, 都市: 福岡
      

メソッドのオーバーロード

同じ名前で、引数の型、数、または種類 (ref/out/in) が異なるメソッドを複数定義できます。


void Display(int number)
{
    Console.WriteLine($"整数: {number}");
}

void Display(string text)
{
    Console.WriteLine($"文字列: {text}");
}

void Display(double number)
{
    Console.WriteLine($"倍精度浮動小数点数: {number}");
}

// 呼び出し時に引数によって適切なメソッドが選択される
Display(123);       // Display(int) が呼ばれる
Display("Hello");   // Display(string) が呼ばれる
Display(123.45);    // Display(double) が呼ばれる
      

ローカル関数 (C# 7.0以降)

メソッドの内部で定義され、そのメソッド内からのみ呼び出せる関数です。複雑なメソッドを内部で整理するのに役立ちます。


void ProcessData(IEnumerable<int> data)
{
    if (data == null) throw new ArgumentNullException(nameof(data));

    // ローカル関数の定義
    int SumSquare(int x, int y)
    {
        return (x * x) + (y * y);
    }

    foreach (var item in data.Where(d => d > 0)) // 正の数のみ処理
    {
        // フィルタリングなどの補助的な処理もローカル関数化できる
        bool ShouldProcess(int value) => value % 2 != 0; // 奇数のみ処理するローカル関数

        if (ShouldProcess(item))
        {
            int result = SumSquare(item, item + 1); // ローカル関数呼び出し
            Console.WriteLine($"データ {item} の処理結果: {result}");
        }
    }
    // Console.WriteLine(SumSquare(1, 2)); // ここからは呼び出せない (スコープ外)
}

List<int> myData = new List<int> { 1, 2, 3, 4, 5, -1, 6 };
ProcessData(myData);
       

クラスとオブジェクト指向 🏛️

クラスの定義、インスタンス化、コンストラクタ、プロパティ、継承、ポリモーフィズム、インターフェース、抽象クラスなど。

基本的なクラス定義とインスタンス化


// クラスの定義
public class Car
{
    // フィールド (通常はprivate)
    private string _color;
    private int _speed;

    // プロパティ (外部からのアクセスを提供)
    public string Color
    {
        get { return _color; }
        set { _color = value; } // valueは設定される値
    }

    // 自動実装プロパティ (C# 3.0以降) - フィールドを自動生成
    public string ModelName { get; set; }
    public int Year { get; private set; } // setをprivateにすることも可能

    // コンストラクタ (インスタンス化時に実行される)
    public Car(string modelName, int year, string color)
    {
        ModelName = modelName;
        Year = year;
        Color = color; // プロパティ経由で設定
        _speed = 0;    // フィールドを初期化
    }

    // メソッド
    public void Accelerate(int increment)
    {
        _speed += increment;
        Console.WriteLine($"{ModelName} が {increment} km/h 加速しました。現在の速度: {_speed} km/h");
    }

    public void Brake(int decrement)
    {
        _speed -= decrement;
        if (_speed < 0) _speed = 0;
        Console.WriteLine($"{ModelName} が {decrement} km/h 減速しました。現在の速度: {_speed} km/h");
    }

    public void DisplayInfo()
    {
        Console.WriteLine($"モデル: {ModelName}, 年式: {Year}, 色: {Color}, 速度: {_speed} km/h");
    }
}

// クラスのインスタンス化 (オブジェクトの生成)
Car myCar = new Car("トヨタ カローラ", 2023, "白");
Car anotherCar = new Car("ホンダ シビック", 2022, "黒");

// メソッドの呼び出し
myCar.Accelerate(50);
myCar.DisplayInfo();

anotherCar.Color = "赤"; // プロパティ経由で色を変更
anotherCar.Accelerate(30);
anotherCar.DisplayInfo();
            

コンストラクタ

オブジェクトの初期化を担当します。

  • クラス名と同じ名前で、戻り値がない。
  • 引数を持つコンストラクタを定義すると、デフォルトの引数なしコンストラクタは自動生成されない。
  • オーバーロード可能。
  • this(...) を使用して、同じクラスの他のコンストラクタを呼び出せる(コンストラクタ初期化子)。

public class Product
{
    public string Name { get; }
    public decimal Price { get; set; }
    public int Stock { get; private set; }

    // 基本のコンストラクタ
    public Product(string name, decimal price, int initialStock)
    {
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentException("製品名は必須です。", nameof(name));
        if (price <= 0)
            throw new ArgumentOutOfRangeException(nameof(price), "価格は正の値である必要があります。");
        if (initialStock < 0)
             throw new ArgumentOutOfRangeException(nameof(initialStock), "在庫数は0以上である必要があります。");

        Name = name;
        Price = price;
        Stock = initialStock;
        Console.WriteLine($"製品「{Name}」が作成されました。");
    }

    // 在庫0で初期化するコンストラクタ (thisを使用して基本コンストラクタを呼び出す)
    public Product(string name, decimal price) : this(name, price, 0)
    {
        Console.WriteLine($"在庫0で製品「{Name}」が作成されました。");
    }

    // 静的コンストラクタ (最初にクラスがアクセスされた時に一度だけ実行)
    static Product()
    {
        Console.WriteLine("Productクラスが初めてロードされました。");
        // 静的メンバーの初期化などに使用
    }
}

// 使用例
var item1 = new Product("ラップトップ", 150000m, 10);
var item2 = new Product("マウス", 3000m); // 在庫0のコンストラクタ呼び出し
// var item3 = new Product("", -100); // 例外が発生する
            

プロパティ

フィールドへのアクセスを制御します。getアクセサ(取得)とsetアクセサ(設定)を持ちます。

  • 自動実装プロパティ: public string Name { get; set; }
  • 読み取り専用プロパティ: public string Id { get; } (コンストラクタまたは初期化子でのみ設定可能)
  • 算出プロパティ: getアクセサ内で計算を行う。
  • アクセサの可視性変更: public string Code { get; private set; } (setのみprivate)
  • init アクセサ (C# 9.0以降): オブジェクト初期化時のみ設定可能。イミュータブルなオブジェクト作成に役立つ。

public class Rectangle
{
    public double Width { get; }
    public double Height { get; }

    // 算出プロパティ
    public double Area => Width * Height; // 式形式
    // public double Area { get { return Width * Height; } } // 通常形式

    // init アクセサ (オブジェクト初期化子でのみ設定可能)
    public string Label { get; init; } = "Default Label";

    public Rectangle(double width, double height)
    {
        Width = width;
        Height = height;
    }
}

// init プロパティの使用例
var rect = new Rectangle(10, 5)
{
    Label = "My Rectangle" // オブジェクト初期化子で設定可能
};
// rect.Label = "New Label"; // これはコンパイルエラー (initなので再代入不可)

Console.WriteLine($"幅: {rect.Width}, 高さ: {rect.Height}, 面積: {rect.Area}, ラベル: {rect.Label}");
            

継承

既存のクラス(基底クラス、親クラス)の機能を引き継いで新しいクラス(派生クラス、子クラス)を作成します。コードの再利用性を高めます。

  • C#では単一継承のみサポート(クラスは1つの基底クラスしか持てない)。
  • base キーワードで基底クラスのメンバー(コンストラクタ、メソッド)にアクセスできる。
  • メソッドのオーバーライド: virtual (基底クラス) と override (派生クラス) キーワードを使用。
  • メソッドの隠蔽: new キーワードを使用(非推奨)。
  • sealed キーワード: クラスを継承不可にする、またはメソッドをオーバーライド不可にする。

// 基底クラス
public class Animal
{
    public string Name { get; set; }

    public Animal(string name)
    {
        Name = name;
    }

    // virtual: 派生クラスでオーバーライド可能にする
    public virtual void Speak()
    {
        Console.WriteLine($"{Name} が鳴いています。");
    }
}

// 派生クラス
public class Dog : Animal
{
    public string Breed { get; set; }

    // 基底クラスのコンストラクタを呼び出す (baseを使用)
    public Dog(string name, string breed) : base(name)
    {
        Breed = breed;
    }

    // 基底クラスのメソッドをオーバーライドする
    public override void Speak()
    {
        Console.WriteLine($"{Name} ({Breed}) が「ワン!」と鳴きました。 🐶");
    }

    // 派生クラス独自のメソッド
    public void Fetch()
    {
        Console.WriteLine($"{Name} がボールを取ってきました。");
    }
}

// 継承の使用例
Dog myDog = new Dog("ポチ", "柴犬");
myDog.Speak(); // DogクラスのSpeakが呼ばれる
myDog.Fetch();

Animal genericAnimal = myDog; // 派生クラスのインスタンスを基底クラスの変数に代入可能
genericAnimal.Speak(); // DogクラスのSpeakが呼ばれる (ポリモーフィズム)
// genericAnimal.Fetch(); // これはエラー (AnimalクラスにはFetchメソッドがない)
            

ポリモーフィズム (多態性)

同じインターフェースや基底クラスを持つ異なるオブジェクトが、それぞれの実装に基づいて動作する能力。

  • 主に継承とメソッドのオーバーライドによって実現される。
  • インターフェースを通じても実現される。
  • コードの柔軟性と拡張性を高める。

// 上記の Animal と Dog の例もポリモーフィズムの一例

// 別の派生クラス
public class Cat : Animal
{
    public Cat(string name) : base(name) { }

    public override void Speak()
    {
        Console.WriteLine($"{Name} が「ニャー」と鳴きました。 🐱");
    }
}

// ポリモーフィズムのデモンストレーション
List<Animal> pets = new List<Animal>
{
    new Dog("タロウ", "秋田犬"),
    new Cat("ミケ"),
    new Dog("ハチ", "ゴールデンレトリバー")
};

Console.WriteLine("\n--- ペットたちの鳴き声 ---");
foreach (Animal pet in pets)
{
    pet.Speak(); // 同じ pet.Speak() 呼び出しでも、実際の型に応じて異なる動作をする
}
            

抽象クラス (abstract)

インスタンス化できないクラス。派生クラスで実装されるべき抽象メンバー(メソッド、プロパティなど)を持つことができる。

  • 少なくとも1つの抽象メンバーを持つか、abstract キーワードで修飾されている。
  • 抽象メンバーは実装を持たず、派生クラスで override が必須。
  • 通常のメンバー(実装を持つメソッドなど)も持つことができる。
  • 継承の基盤として、共通の構造や規約を定義するのに使う。

// 抽象クラス
public abstract class Shape
{
    public abstract string ShapeName { get; } // 抽象プロパティ

    // 抽象メソッド (実装を持たない)
    public abstract double CalculateArea();

    // 通常のメソッド (実装を持つ)
    public virtual void DisplayInfo()
    {
        Console.WriteLine($"図形の種類: {ShapeName}, 面積: {CalculateArea()}");
    }
}

// 抽象クラスを継承する具象クラス
public class Circle : Shape
{
    public double Radius { get; set; }

    public Circle(double radius)
    {
        Radius = radius;
    }

    public override string ShapeName => "円"; // 抽象プロパティの実装

    // 抽象メソッドの実装 (overrideが必須)
    public override double CalculateArea()
    {
        return Math.PI * Radius * Radius;
    }
}

public class Square : Shape
{
     public double Side { get; set; }

     public Square(double side)
     {
         Side = side;
     }

     public override string ShapeName => "正方形";

     public override double CalculateArea()
     {
         return Side * Side;
     }

     // 基底クラスのvirtualメソッドをoverrideすることも可能
     public override void DisplayInfo()
     {
         Console.WriteLine($"図形: {ShapeName}, 一辺: {Side}, 面積: {CalculateArea()}");
     }
}

// 使用例
// Shape shape = new Shape(); // エラー: 抽象クラスはインスタンス化できない

Shape circle = new Circle(5);
Shape square = new Square(4);

circle.DisplayInfo(); // 円のDisplayInfo (CalculateAreaはCircleの実装が呼ばれる)
square.DisplayInfo(); // SquareでオーバーライドされたDisplayInfoが呼ばれる
            

インターフェース (interface)

実装を持たないメンバー(メソッド、プロパティ、イベント、インデクサ)のシグネチャ(規約)のみを定義する。クラスが実装すべき機能を定義する。

  • クラスは複数のインターフェースを実装できる(多重継承の代替)。
  • インターフェースを実装するクラスは、定義されている全てのメンバーを実装する必要がある。
  • インターフェースのメンバーは暗黙的に public かつ abstract (C# 8.0未満)。
  • C# 8.0以降、デフォルト実装を持つことができる。

// インターフェースの定義
public interface ILogger
{
    void LogInfo(string message);
    void LogError(string message, Exception ex = null); // デフォルト引数も可能
}

public interface ISaveable
{
    bool Save();
    DateTime LastSaved { get; }
}

// インターフェースを実装するクラス
public class FileLogger : ILogger
{
    private string _logFilePath;

    public FileLogger(string filePath)
    {
        _logFilePath = filePath;
    }

    public void LogInfo(string message)
    {
        // ファイルに情報メッセージを書き込む処理 (省略)
        Console.WriteLine($"[File Info] {_logFilePath}: {message}");
    }

    public void LogError(string message, Exception ex = null)
    {
        // ファイルにエラーメッセージと例外情報を書き込む処理 (省略)
        Console.WriteLine($"[File Error] {_logFilePath}: {message} {(ex != null ? ex.Message : "")}");
    }
}

public class Document : ISaveable
{
    public string Content { get; set; }
    public DateTime LastSaved { get; private set; }

    public bool Save()
    {
        // ドキュメントを保存する処理 (省略)
        Console.WriteLine("ドキュメントを保存しました。");
        LastSaved = DateTime.Now;
        return true;
    }
}

// 複数のインターフェースを実装するクラス
public class ReportGenerator : ILogger, ISaveable
{
     public string ReportData { get; set; }
     public DateTime LastSaved { get; private set; }

     public void LogInfo(string message) { Console.WriteLine($"[Report Info] {message}"); }
     public void LogError(string message, Exception ex = null) { Console.WriteLine($"[Report Error] {message}"); }

     public bool Save()
     {
         Console.WriteLine("レポートを保存しました。");
         LastSaved = DateTime.Now;
         return true;
     }

     public void Generate() { Console.WriteLine("レポートを生成中..."); }
}


// 使用例
ILogger logger = new FileLogger("app.log");
logger.LogInfo("アプリケーションを開始しました。");

ISaveable doc = new Document { Content = "重要な内容..." };
doc.Save();
Console.WriteLine($"最終保存日時: {doc.LastSaved}");

ReportGenerator report = new ReportGenerator();
report.Generate();
report.LogInfo("レポート生成完了");
report.Save();

// インターフェース型の変数を使用する利点
List<ILogger> loggers = new List<ILogger> { new FileLogger("a.log"), report };
foreach(var log in loggers)
{
    log.LogInfo("共通ログメッセージ"); // FileLoggerとReportGeneratorの両方でLogInfoが呼ばれる
}
            

静的メンバー (static)

クラスのインスタンスではなく、クラス自体に関連付けられるメンバー。

  • 静的フィールド、静的プロパティ、静的メソッド、静的コンストラクタ、静的クラスがある。
  • インスタンスを作成せずに、クラス名を使ってアクセスする (例: Math.PI, Console.WriteLine())。
  • ユーティリティメソッドや定数、シングルトンパターンなどで使用される。
  • 静的クラスは静的メンバーのみを持つことができ、インスタンス化できない。

public static class StringUtils // 静的クラス
{
    // 静的メソッド
    public static bool IsNullOrEmpty(string s)
    {
        return string.IsNullOrEmpty(s);
    }

    // 静的プロパティ
    public static string DefaultPrefix { get; set; } = "[LOG]";

    // 静的フィールド (通常はprivate)
    private static int _counter = 0;

    // 静的コンストラクタ (一度だけ実行)
    static StringUtils()
    {
        Console.WriteLine("StringUtilsが初期化されました。");
    }

    public static string AddPrefix(string message)
    {
        _counter++; // 静的フィールドをインクリメント
        return $"{DefaultPrefix} ({_counter}): {message}";
    }
}

// 静的メンバーの使用 (インスタンス化不要)
Console.WriteLine(StringUtils.IsNullOrEmpty("")); // true
Console.WriteLine(StringUtils.IsNullOrEmpty("abc")); // false

Console.WriteLine(StringUtils.AddPrefix("処理開始"));
StringUtils.DefaultPrefix = "[DEBUG]";
Console.WriteLine(StringUtils.AddPrefix("デバッグ情報"));
            

コレクション 📚

データをまとめて扱うための主要なコレクションクラス。

配列 (Array)

固定サイズの同種要素のコレクション。最も基本的なコレクション。


// 配列の宣言と初期化
int[] numbers = new int[5]; // サイズ5のint配列、要素はデフォルト値(0)で初期化
string[] names = { "Alice", "Bob", "Charlie" }; // 初期値で初期化
var mixedArray = new object[] { 1, "two", 3.0, true }; // object配列

// 要素へのアクセス (0ベースのインデックス)
numbers[0] = 10;
numbers[1] = 20;
Console.WriteLine($"最初の要素: {numbers[0]}");
Console.WriteLine($"3番目の名前: {names[2]}");

// 配列の長さ (要素数)
int length = numbers.Length;
Console.WriteLine($"numbersの要素数: {length}");

// 配列のループ処理
for (int i = 0; i < names.Length; i++)
{
    Console.WriteLine($"名前 {i}: {names[i]}");
}
foreach (var name in names)
{
    Console.WriteLine($"名前 (foreach): {name}");
}

// 多次元配列
int[,] matrix = new int[2, 3]; // 2行3列の配列
matrix[0, 0] = 1; matrix[0, 1] = 2; matrix[0, 2] = 3;
matrix[1, 0] = 4; matrix[1, 1] = 5; matrix[1, 2] = 6;

int[,] matrixInitialized = { { 1, 2 }, { 3, 4 } }; // 2x2

// ジャグ配列 (配列の配列) - 各行の要素数が異なってもよい
int[][] jaggedArray = new int[3][];
jaggedArray[0] = new int[] { 1, 2 };
jaggedArray[1] = new int[] { 3, 4, 5 };
jaggedArray[2] = new int[] { 6 };
            

リスト (List<T>)

動的にサイズ変更可能な、ジェネリックな要素のコレクション。System.Collections.Generic 名前空間。


// Listの宣言と初期化
List<string> fruits = new List<string>(); // 空のリスト
List<int> scores = new List<int> { 90, 85, 77, 92 }; // 初期値で初期化
var cities = new List<string> { "東京", "大阪", "名古屋" };

// 要素の追加
fruits.Add("りんご");
fruits.Add("ばなな");
fruits.AddRange(new string[] { "みかん", "ぶどう" }); // 複数の要素を追加

// 要素へのアクセス (0ベースのインデックス)
Console.WriteLine($"最初の果物: {fruits[0]}");
fruits[1] = "キウイ"; // 要素の変更

// 要素の挿入
fruits.Insert(1, "もも"); // インデックス1に "もも" を挿入

// 要素の削除
fruits.Remove("みかん"); // 指定した要素を削除 (最初に見つかったもの)
fruits.RemoveAt(0); // 指定したインデックスの要素を削除
// fruits.Clear(); // 全ての要素を削除

// 要素の検索
bool containsKiwi = fruits.Contains("キウイ"); // 要素が含まれているか
int indexOfGrape = fruits.IndexOf("ぶどう"); // 要素のインデックスを検索 (-1なら存在しない)

// リストのサイズ (要素数)
int count = fruits.Count;
Console.WriteLine($"現在の果物の数: {count}");

// リストのループ処理
foreach (var fruit in fruits)
{
    Console.WriteLine(fruit);
}

// Listの容量 (Capacity) - 内部配列のサイズ
int capacity = fruits.Capacity;
Console.WriteLine($"リストの容量: {capacity}");
// fruits.TrimExcess(); // 不要な容量を解放
            

辞書 (Dictionary<TKey, TValue>)

キーと値のペアを格納するコレクション。キーは一意である必要がある。System.Collections.Generic 名前空間。


// Dictionaryの宣言と初期化
Dictionary<string, string> capitals = new Dictionary<string, string>();
var userAges = new Dictionary<int, int> // { UserId, Age }
{
    { 101, 30 },
    { 102, 25 },
    { 103, 42 }
};
// C# 6.0以降のインデクサを使った初期化
var productPrices = new Dictionary<string, decimal>
{
    ["Laptop"] = 120000m,
    ["Mouse"] = 3500m,
    ["Keyboard"] = 8000m
};

// 要素の追加
capitals.Add("日本", "東京");
capitals.Add("フランス", "パリ");
// capitals.Add("日本", "京都"); // エラー: キーは一意である必要がある (ArgumentException)

// キーを使って値にアクセス (キーが存在しないと例外 KeyNotFoundException)
Console.WriteLine($"日本の首都: {capitals["日本"]}");
productPrices["Mouse"] = 3800m; // 値の更新

// 安全なアクセス方法 (キーの存在確認)
if (capitals.ContainsKey("アメリカ"))
{
    Console.WriteLine($"アメリカの首都: {capitals["アメリカ"]}");
}
else
{
    Console.WriteLine("アメリカの首都は登録されていません。");
}

// 安全なアクセス方法 (TryGetValue) - パフォーマンスが良い
if (userAges.TryGetValue(102, out int age))
{
    Console.WriteLine($"ユーザーID 102 の年齢: {age}");
}
else
{
    Console.WriteLine("ユーザーID 102 は存在しません。");
}

// 要素の削除
bool removed = productPrices.Remove("Keyboard"); // キーを指定して削除
// userAges.Clear(); // 全要素削除

// キーと値のコレクションを取得
var countries = capitals.Keys; // キーのみのコレクション (ICollection)
var citiesOnly = capitals.Values; // 値のみのコレクション (ICollection)

// Dictionaryのループ処理 (KeyValuePairを使用)
foreach (KeyValuePair<string, string> pair in capitals)
{
    Console.WriteLine($"国: {pair.Key}, 首都: {pair.Value}");
}
// var を使うと簡潔に
foreach (var pair in userAges)
{
    Console.WriteLine($"ユーザーID: {pair.Key}, 年齢: {pair.Value}");
}

// 要素数
int dictCount = capitals.Count;
            

セット (HashSet<T>)

一意な要素のみを格納するコレクション。順序は保証されない。要素の追加、削除、検索が高速。System.Collections.Generic 名前空間。


// HashSetの宣言と初期化
HashSet<string> uniqueNames = new HashSet<string>();
var tags = new HashSet<string> { "C#", "Programming", "Web", "API" };

// 要素の追加 (重複は無視される)
uniqueNames.Add("Alice");
uniqueNames.Add("Bob");
bool addedAliceAgain = uniqueNames.Add("Alice"); // false (既に追加されているため)
Console.WriteLine($"Aliceの再追加結果: {addedAliceAgain}");

// 要素の存在確認 (高速)
bool hasBob = uniqueNames.Contains("Bob"); // true
bool hasCharlie = uniqueNames.Contains("Charlie"); // false

// 要素の削除
uniqueNames.Remove("Bob");

// 要素数
int uniqueCount = uniqueNames.Count;

// ループ処理 (順序は保証されない)
Console.WriteLine("\nユニークな名前リスト:");
foreach (var name in uniqueNames)
{
    Console.WriteLine(name);
}

// セット演算
HashSet<int> setA = new HashSet<int> { 1, 2, 3, 4, 5 };
HashSet<int> setB = new HashSet<int> { 4, 5, 6, 7, 8 };

// 和集合 (Union)
setA.UnionWith(setB); // setA が {1, 2, 3, 4, 5, 6, 7, 8} になる
Console.WriteLine("和集合:");
foreach(var item in setA) Console.Write($"{item} "); // 1 2 3 4 5 6 7 8
Console.WriteLine();

// 積集合 (Intersection)
setA = new HashSet<int> { 1, 2, 3, 4, 5 }; // setA を元に戻す
setA.IntersectWith(setB); // setA が {4, 5} になる
Console.WriteLine("積集合:");
foreach(var item in setA) Console.Write($"{item} "); // 4 5
Console.WriteLine();

// 差集合 (Except)
setA = new HashSet<int> { 1, 2, 3, 4, 5 }; // setA を元に戻す
setA.ExceptWith(setB); // setA が {1, 2, 3} になる (AにあってBにないもの)
Console.WriteLine("差集合 (A - B):");
foreach(var item in setA) Console.Write($"{item} "); // 1 2 3
Console.WriteLine();

// 対称差集合 (Symmetric Except)
setA = new HashSet<int> { 1, 2, 3, 4, 5 }; // setA を元に戻す
setA.SymmetricExceptWith(setB); // setA が {1, 2, 3, 6, 7, 8} になる (どちらか一方にのみ含まれるもの)
Console.WriteLine("対称差集合:");
foreach(var item in setA) Console.Write($"{item} "); // 1 2 3 6 7 8
Console.WriteLine();

// サブセット、スーパーセットの判定
bool isSubset = setA.IsSubsetOf(setB);
bool isSuperset = setA.IsSupersetOf(new HashSet<int> { 1, 2 });
            

その他のコレクション

コレクション 説明 主な用途
Queue<T> FIFO (First-In, First-Out) のキュー。Enqueueで追加、Dequeueで取り出す。 タスク処理、メッセージキュー、幅優先探索など。
Stack<T> LIFO (Last-In, First-Out) のスタック。Pushで追加、Popで取り出す。 戻る/進む機能、式の評価、深さ優先探索など。
LinkedList<T> 双方向リンクリスト。要素の挿入・削除が高速(特にリストの中間)。インデックスアクセスは低速。 頻繁な挿入・削除が必要な場合。
SortedList<TKey, TValue> キーでソートされたキー/値ペアのコレクション。メモリ使用量はDictionaryより少ないが、追加/削除は遅い。 ソートされた状態でのアクセスが必要な場合。
SortedDictionary<TKey, TValue> キーでソートされたキー/値ペアのコレクション。赤黒木で実装。追加/削除はSortedListより高速。 ソートされた状態でのアクセスと、効率的な追加/削除が必要な場合。
SortedSet<T> ソートされた一意な要素のコレクション。HashSetのソート版。 一意かつソートされた要素の集合が必要な場合。
Concurrent Collections (例: ConcurrentDictionary, ConcurrentQueue, ConcurrentBag) System.Collections.Concurrent名前空間。複数のスレッドから安全にアクセスできるコレクション。 マルチスレッド環境でのデータ共有。
ReadOnlyCollection<T> 読み取り専用のラッパーコレクション。元のコレクションへの変更は反映される場合がある。 コレクションを外部に公開するが、変更はさせたくない場合。
Immutable Collections (例: ImmutableList, ImmutableDictionary) System.Collections.Immutable名前空間 (NuGetパッケージ)。変更操作を行うと新しいインスタンスが返される不変コレクション。 スレッドセーフティの確保、状態変更の追跡が容易な場合。関数型プログラミングスタイル。

LINQ (Language Integrated Query) 🔍

コレクションやデータソースに対する問い合わせ (クエリ) を簡潔に記述するための機能。System.Linq 名前空間。

LINQの基本 (メソッド構文とクエリ構文)

LINQ にはメソッドチェーンで記述するメソッド構文と、SQLに似たクエリ構文があります。


List<int> numbers = new List<int> { 1, 5, 3, 8, 2, 9, 4, 7, 6, 0 };

// --- メソッド構文 (Method Syntax) ---
Console.WriteLine("--- メソッド構文 ---");
// 5より大きい偶数を抽出して、降順にソート
var methodResult = numbers
                      .Where(n => n > 5)     // 抽出 (フィルタリング)
                      .Where(n => n % 2 == 0) // さらに抽出
                      .OrderByDescending(n => n) // 降順ソート
                      .Select(n => $"数値: {n}"); // 射影 (データ変換)

foreach (var result in methodResult)
{
    Console.WriteLine(result);
}
// 出力:
// 数値: 8
// 数値: 6

// --- クエリ構文 (Query Syntax) ---
Console.WriteLine("\n--- クエリ構文 ---");
// 5より大きい偶数を抽出して、降順にソート
var queryResult = from n in numbers
                  where n > 5 && n % 2 == 0 // where句でフィルタリング
                  orderby n descending     // orderby句でソート
                  select $"数値: {n}";      // select句で射影

foreach (var result in queryResult)
{
    Console.WriteLine(result);
}
// 出力 (メソッド構文と同じ):
// 数値: 8
// 数値: 6

// クエリ構文は内部的にメソッド構文に変換されます。どちらを使うかは好みや可読性によります。
// クエリ構文で書けない(書きにくい)メソッドもあります (例: Count, FirstOrDefault など)。
      

主要なLINQメソッド

カテゴリ メソッド 説明
フィルタリング Where() 条件に一致する要素を抽出します。
OfType<TResult>() 指定した型にキャストできる要素のみを抽出します。
Distinct() コレクションから重複する要素を除去します。
Skip(), Take(), SkipWhile(), TakeWhile() 要素をスキップしたり、指定した数の要素を取得したりします。条件に基づくスキップ/取得も可能です。
射影 (Projection) Select() 各要素を指定した形式に変換します。
SelectMany() 各要素から生成されるコレクションをフラット化して1つのシーケンスにします。
Zip() 2つのシーケンスを結合し、対応する要素のペアから新しいシーケンスを作成します。
ソート OrderBy(), OrderByDescending() 要素を指定したキーで昇順または降順にソートします。(最初のソート)
ThenBy(), ThenByDescending() OrderByThenBy の後に追加のソートキーを指定します。(2番目以降のソート)
Reverse() シーケンスの要素の順序を反転します。
AsEnumerable() 入力を IEnumerable<T> としてキャストします。IQueryableから切り替える際などに使います。
グループ化 GroupBy() 指定したキーに基づいて要素をグループ化します。
ToLookup() GroupBy に似ていますが、即時実行され、キーが存在しない場合に空のシーケンスを返すLookupオブジェクトを作成します。
GroupJoin() 2つのシーケンスをキーに基づいてグループ化し、結合します(左外部結合に近い)。
結合 Join() 2つのシーケンスを共通のキーに基づいて結合します(内部結合)。
Concat() 2つのシーケンスを連結します。
Union() 2つのシーケンスの和集合(重複なし)を返します。
Intersect() 2つのシーケンスの積集合(共通要素)を返します。
Except() 最初のシーケンスにあり、2番目のシーケンスにない要素(差集合)を返します。
集計 Count(), LongCount() 要素の数を返します。条件を指定することも可能です。
Sum() 数値シーケンスの合計値を計算します。
Average() 数値シーケンスの平均値を計算します。
Min(), Max() シーケンスの最小値または最大値を返します。
Aggregate() シーケンスに対してアキュムレータ関数を適用し、単一の結果を生成します。
Any() シーケンスに要素が含まれているか、または条件を満たす要素が存在するかどうかを判定します。
All() シーケンスのすべての要素が指定した条件を満たすかどうかを判定します。
要素取得 First(), FirstOrDefault() シーケンスの最初の要素を取得します。FirstOrDefault は要素がない場合にデフォルト値を返します(nullまたは0など)。条件指定も可能。
Last(), LastOrDefault() シーケンスの最後の要素を取得します。LastOrDefault は要素がない場合にデフォルト値を返します。条件指定も可能。
Single(), SingleOrDefault() 条件を満たす唯一の要素を取得します。要素がないか複数ある場合に例外が発生します。SingleOrDefault は要素がない場合はデフォルト値、複数ある場合は例外を返します。
ElementAt(), ElementAtOrDefault() 指定したインデックスの要素を取得します。ElementAtOrDefault はインデックスが範囲外の場合にデフォルト値を返します。
DefaultIfEmpty() シーケンスが空の場合に、指定したデフォルト値を含む単一要素のシーケンスを返します。
変換 ToArray() シーケンスを配列に変換します。
ToList() シーケンスを List<T> に変換します。
ToDictionary(), ToHashSet() シーケンスを Dictionary<TKey, TValue>HashSet<T> に変換します。キーや要素を選択する関数を指定します。

LINQの遅延実行

多くのLINQ演算子(Where, Select, OrderBy など)は遅延実行 (Deferred Execution) されます。これは、クエリが定義された時点では実行されず、結果が実際に必要になったとき(例: foreach で列挙されたり、ToList(), Count() などが呼ばれたりしたとき)に初めて実行される仕組みです。


List<int> data = new List<int> { 1, 2, 3 };

// クエリの定義 (この時点では実行されない)
var query = data.Select(x =>
{
    Console.WriteLine($"Select実行: {x}");
    return x * 2;
});

Console.WriteLine("クエリ定義完了");

// クエリの実行 (ここで Select 内の Console.WriteLine が実行される)
var results = query.ToList(); // ToList() で即時実行される

Console.WriteLine("\nクエリ実行後");

// 再度データを追加
data.Add(4);

Console.WriteLine("\nデータを追加後、再度クエリを実行 (ToList)");
// 再度実行すると、追加されたデータも含めて処理される
var results2 = query.ToList();

// Whereなども同様に遅延実行される
var filteredQuery = data.Where(x =>
{
    Console.WriteLine($"Where実行: {x}");
    return x % 2 == 0; // 偶数のみ
});

Console.WriteLine("\nフィルタリングクエリ定義完了");

// foreachで列挙すると、その時点でWhereが評価される
Console.WriteLine("フィルタリングクエリ実行 (foreach):");
foreach (var item in filteredQuery)
{
    Console.WriteLine($"結果: {item}");
}

// 注意: Count(), ToList(), ToArray(), First(), Max() などは即時実行されます。
      

LINQ to Objects, LINQ to XML, LINQ to SQL/Entities

  • LINQ to Objects: メモリ内のコレクション (IEnumerable<T>) に対するクエリ。これまで見てきた例。
  • LINQ to XML: XMLデータ (XDocument, XElement など System.Xml.Linq 名前空間) に対するクエリ。
  • LINQ to SQL (非推奨): SQL Server データベースに対するクエリ(現在は Entity Framework Core が主流)。
  • LINQ to Entities (Entity Framework / EF Core): ORM (Object-Relational Mapper) を介してリレーショナルデータベースに対するクエリ。LINQクエリがSQLに変換されて実行される (IQueryable<T>)。

// LINQ to XML の簡単な例
string xmlString = @"
<People>
  <Person Name=""Alice"" Age=""30"" />
  <Person Name=""Bob"" Age=""25"" />
  <Person Name=""Charlie"" Age=""35"" />
</People>";

XDocument doc = XDocument.Parse(xmlString);

// 30歳以上の人の名前を取得
var namesOver30 = from person in doc.Root.Elements("Person")
                  where (int)person.Attribute("Age") >= 30
                  select (string)person.Attribute("Name");

Console.WriteLine("\n--- LINQ to XML ---");
foreach (var name in namesOver30)
{
    Console.WriteLine(name); // Alice, Charlie
}

// LINQ to Entities (EF Core) の概念的な例 (DbContextが必要)
/*
// DbContextのインスタンス (データベース接続を表す)
// using var db = new MyDbContext();

// 20歳以上のユーザーを名前順で取得
var adultUsers = from user in db.Users // db.Users は DbSet (IQueryable)
                 where user.Age >= 20
                 orderby user.Name
                 select user;

// この時点ではSQLは実行されない (遅延実行)

// ToList() などで実際にデータベースにクエリが発行される
// List users = await adultUsers.ToListAsync();

// foreach (var user in users)
// {
//     Console.WriteLine($"ID: {user.Id}, Name: {user.Name}, Age: {user.Age}");
// }
*/
      

非同期処理 (async/await) ⚡️

時間のかかる操作(I/Oバウンド: ネットワーク通信、ファイルアクセスなど、CPUバウンド: 重い計算)を、UIスレッドや他の処理をブロックせずに行うための機能。

基本的な async/await の使い方

  • async キーワード: メソッドが非同期であることを示し、内部で await を使用可能にする。
  • await キーワード: 非同期操作 (Task または Task<TResult> を返すメソッド呼び出しなど) の完了を待機する。待機中、呼び出し元のスレッドはブロックされず、他の処理を実行できる。
  • 非同期メソッドの戻り値:
    • Task: 操作が完了したことを示す (戻り値なし)。
    • Task<TResult>: 操作が完了し、TResult 型の結果を返すことを示す。
    • void: イベントハンドラなど、特殊な場合にのみ使用。通常は避けるべき。
    • ValueTask, ValueTask<TResult> (C# 7.0以降): 同期的に完了する可能性が高い場合や、頻繁に呼び出される非同期メソッドでのパフォーマンス向上のための軽量版Task。

using System.Threading.Tasks; // Taskクラスなどを使用するために必要

// --- 非同期メソッドの定義 ---

// 時間のかかる処理をシミュレートするメソッド (Taskを返す)
async Task SimulateLongOperationAsync(int durationMilliseconds, string operationName)
{
    Console.WriteLine($"{operationName} 開始...");
    await Task.Delay(durationMilliseconds); // 指定時間、非同期に待機
    Console.WriteLine($"{operationName} 完了.");
}

// 結果を返す非同期メソッド (Task<TResult>を返す)
async Task<string> FetchDataAsync(string url)
{
    Console.WriteLine($"データ取得開始: {url}");
    // HttpClientを使った実際の非同期ネットワーク通信 (ここではシミュレーション)
    await Task.Delay(1500); // 1.5秒待機
    Console.WriteLine($"データ取得完了: {url}");
    return $"\"{url}\" から取得したデータ";
}

// --- 非同期メソッドの呼び出し ---

// Mainメソッドなどを async にする (C# 7.1以降、async Mainが可能)
// async static Task Main(string[] args) // トップレベルステートメントがない場合
// トップレベルステートメントでは await を直接使用可能

// async void のイベントハンドラの例 (注意して使用)
// private async void Button_Click(object sender, RoutedEventArgs e)
// {
//     try
//     {
//         button.IsEnabled = false; // UI操作
//         await SimulateLongOperationAsync(2000, "ボタンクリック処理");
//         MessageBox.Show("処理が完了しました!");
//     }
//     catch (Exception ex)
//     {
//         MessageBox.Show($"エラーが発生しました: {ex.Message}");
//     }
//     finally
//     {
//         button.IsEnabled = true; // UI操作
//     }
// }


// --- async/awaitを使った呼び出し例 (トップレベルステートメント内と仮定) ---
Console.WriteLine("非同期処理を開始します...");

// 複数の非同期処理を順次実行 (await で待機)
await SimulateLongOperationAsync(1000, "操作A");
string data = await FetchDataAsync("http://example.com/api/data");
Console.WriteLine($"取得結果: {data}");
await SimulateLongOperationAsync(500, "操作B");

Console.WriteLine("\n--- 複数の非同期処理を並行実行 ---");
// Taskを開始するが、完了は待たない
Task taskA = SimulateLongOperationAsync(1200, "並行操作A");
Task<string> taskB = FetchDataAsync("http://example.com/api/more_data");
Task taskC = SimulateLongOperationAsync(800, "並行操作C");

// すべてのタスクが完了するのを待つ
await Task.WhenAll(taskA, taskB, taskC);
Console.WriteLine("すべての並行操作が完了しました。");

// taskBの結果を取得 (既に完了しているので待機時間はほぼゼロ)
string dataB = await taskB; // または taskB.Result (ただし、Resultは完了していない場合にブロックする可能性あり)
Console.WriteLine($"並行操作Bの結果: {dataB}");

// いずれか一つのタスクが完了するのを待つ
Console.WriteLine("\n--- いずれかの非同期処理が完了するのを待つ ---");
Task taskX = Task.Delay(1000);
Task taskY = Task.Delay(500);

Task completedTask = await Task.WhenAny(taskX, taskY);
if (completedTask == taskX)
{
    Console.WriteLine("TaskX が先に完了しました。");
}
else if (completedTask == taskY)
{
    Console.WriteLine("TaskY が先に完了しました。"); // こちらが出力されるはず
}

Console.WriteLine("\nすべての非同期処理が終了しました。");

            

非同期処理の注意点とベストプラクティス

  • async void は避ける: async void メソッド内で発生した例外は、呼び出し元で try-catch しても捕捉できない場合が多く、アプリケーションをクラッシュさせる可能性がある。イベントハンドラなど、やむを得ない場合を除き、async Task または async Task<TResult> を使用する。
  • 非同期メソッドの命名規則: 非同期メソッドの名前の末尾には Async を付けることが推奨される (例: DownloadFileAsync)。
  • ConfigureAwait(false): ライブラリコードなど、特定の同期コンテキスト(UIスレッドなど)に戻る必要がない場合、await task.ConfigureAwait(false); を使用すると、デッドロックのリスクを減らし、パフォーマンスが向上することがある。アプリケーションレベルのコード(UIイベントハンドラなど)では通常不要。
  • ブロックしない: 非同期メソッド内で Task.ResultTask.Wait() を使うと、非同期の利点が失われ、スレッドがブロックされ、デッドロックの原因になることがある。可能な限り await を使用する。
  • CPUバウンドな処理の非同期化: 重い計算処理などは Task.Run(() => { /* 重い処理 */ }) を使ってバックグラウンドスレッドで実行し、await でその完了を待つ。
  • キャンセル処理 (CancellationToken): 長時間かかる可能性のある非同期操作には、キャンセル機能を提供することが重要。CancellationTokenSourceCancellationToken を使用する。

using System.Threading; // CancellationTokenSource などを使用するために必要

// CPUバウンド処理の非同期化とキャンセル処理の例
async Task ProcessLargeDataAsync(byte[] data, CancellationToken cancellationToken)
{
    Console.WriteLine("重いデータ処理を開始します...");

    // Task.Run を使って別スレッドで実行
    await Task.Run(() =>
    {
        for (int i = 0; i < 100; i++)
        {
            // キャンセル要求をチェック
            cancellationToken.ThrowIfCancellationRequested(); // 要求があれば OperationCanceledException をスロー

            // 重い計算処理をシミュレート
            Console.Write(".");
            Thread.Sleep(50); // Thread.Sleep はデモ用。実際はCPUを使う処理
        }
    }, cancellationToken); // Task.RunにもCancellationTokenを渡せる

    Console.WriteLine("\nデータ処理が完了しました。");
}

// --- 呼び出し側の例 ---
var cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

Console.WriteLine("\nCPUバウンド処理を開始します。5秒後にキャンセルします。");
Task processingTask = ProcessLargeDataAsync(new byte[1024], token);

// 5秒後にキャンセルを要求
await Task.Delay(2500); // 2.5秒待機してからキャンセル
Console.WriteLine("\nキャンセルを要求します...");
cts.Cancel();

try
{
    await processingTask; // タスクの完了 (またはキャンセルによる例外) を待つ
}
catch (OperationCanceledException)
{
    Console.WriteLine("\n処理がキャンセルされました。");
}
catch (Exception ex)
{
    Console.WriteLine($"\n予期せぬエラーが発生しました: {ex.Message}");
}
finally
{
    cts.Dispose(); // CancellationTokenSource を破棄
}
            

I/O バウンド vs CPU バウンド

種類 説明 非同期化の方法
I/O バウンド 処理の完了を待つ時間が主。CPUリソースはあまり消費しない(例: ネットワークからの応答待ち、ファイル読み書きの完了待ち)。 ネットワークリクエスト (HTTP, DBアクセス)、ファイル読み書き、Task.Delay async/await をネイティブにサポートするメソッド (例: HttpClient.GetStringAsync, Stream.ReadAsync, Task.Delay) をそのまま await する。スレッドをほとんど占有しない。
CPU バウンド CPUが計算に集中している時間が主。 複雑な計算、画像処理、データ圧縮/展開、暗号化/復号 Task.Run(() => { /* CPU処理 */ }) を使って、処理をスレッドプール上の別スレッドにオフロードし、それを await する。これにより、呼び出し元スレッド (特にUIスレッド) がブロックされるのを防ぐ。

例外処理 ⚠️

プログラム実行中に発生する予期しない状況(エラー)に対処するための仕組み。

基本的な try-catch-finally

  • try ブロック: 例外が発生する可能性のあるコードを囲む。
  • catch ブロック: 特定の型の例外を捕捉し、処理するコードを記述する。複数の catch ブロックを記述できる(より具体的な型から順に記述)。
  • finally ブロック: 例外の発生有無にかかわらず、最後に必ず実行されるコードを記述する(リソースの解放など)。省略可能。

StreamReader reader = null;
try
{
    Console.WriteLine("ファイルを開こうとしています...");
    reader = new StreamReader("nonexistentfile.txt"); // FileNotFoundExceptionが発生する可能性
    string content = reader.ReadToEnd();
    Console.WriteLine("ファイルの内容:");
    Console.WriteLine(content);

    // その他の例外が発生する可能性のある処理
    int zero = 0;
    int result = 10 / zero; // DivideByZeroExceptionが発生
}
catch (FileNotFoundException ex) // 特定の例外を捕捉
{
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine($"エラー: ファイルが見つかりませんでした。({ex.FileName})");
    Console.WriteLine($"詳細: {ex.Message}");
    Console.ResetColor();
}
catch (DivideByZeroException ex) // 別の特定の例外を捕捉
{
     Console.ForegroundColor = ConsoleColor.Red;
     Console.WriteLine("エラー: 0で除算しようとしました。");
     Console.WriteLine($"詳細: {ex.Message}");
     Console.ResetColor();
}
catch (IOException ex) // 上記以外のIO関連の例外を捕捉 (FileNotFoundExceptionより基底クラス)
{
    Console.ForegroundColor = ConsoleColor.Yellow;
    Console.WriteLine($"I/Oエラーが発生しました: {ex.Message}");
    Console.ResetColor();
}
catch (Exception ex) // その他のすべての例外を捕捉 (最も基底のExceptionクラス)
{
    Console.ForegroundColor = ConsoleColor.Magenta;
    Console.WriteLine($"予期せぬエラーが発生しました: {ex.GetType().Name}");
    Console.WriteLine($"詳細: {ex.Message}");
    // スタックトレースなどをログに出力することが多い
    // Console.WriteLine(ex.StackTrace);
    Console.ResetColor();
}
finally
{
    Console.WriteLine("finally ブロックを実行しています...");
    // リソースの解放 (readerがnullでないことを確認)
    if (reader != null)
    {
        Console.WriteLine("StreamReaderをクローズします。");
        reader.Close(); // または reader.Dispose();
    }
    // このブロックは例外が発生しても、しなくても、tryブロックを抜けるときに必ず実行される
}

Console.WriteLine("try-catch-finally ブロックを抜けました。");
      

例外フィルター (C# 6.0以降)

catch ブロックに when 句を追加して、例外オブジェクトのプロパティに基づいて、その catch ブロックで処理するかどうかを判断できます。スタックトレースを壊さずに条件分岐できる利点があります。


try
{
    // 例外を引き起こす可能性のある操作
    throw new HttpRequestException("ネットワーク接続エラー", null, System.Net.HttpStatusCode.ServiceUnavailable);
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
{
    // HTTP 404 Not Found の場合のみここで処理
    Console.WriteLine($"リソースが見つかりませんでした (404): {ex.Message}");
}
catch (HttpRequestException ex) when (ex.StatusCode >= System.Net.HttpStatusCode.InternalServerError)
{
    // サーバーエラー (5xx) の場合のみここで処理
    Console.WriteLine($"サーバーエラー ({ex.StatusCode}): {ex.Message}");
    // リトライ処理などを検討
}
catch (HttpRequestException ex)
{
    // その他の HttpRequestException (whenでフィルタされなかったもの)
    Console.WriteLine($"HTTPリクエストエラー: {ex.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"一般的なエラー: {ex.Message}");
}
      

例外のスロー (throw)

意図的に例外を発生させます。

  • throw new ExceptionType("message");: 新しい例外インスタンスを作成してスローする。
  • throw; (catch ブロック内): 捕捉した例外をそのまま再スローする。スタックトレース情報が保持されるため、推奨される方法。
  • throw ex; (catch ブロック内): 捕捉した例外インスタンスを再スローするが、スタックトレースがリセットされ、例外発生箇所がこの throw ex; の場所になるため、通常は避けるべき。

void ProcessValue(int value)
{
    if (value < 0)
    {
        // 不正な値の場合、ArgumentOutOfRangeExceptionをスロー
        throw new ArgumentOutOfRangeException(nameof(value), "値は0以上である必要があります。");
    }
    Console.WriteLine($"値を処理しました: {value}");
}

void PerformOperation()
{
    try
    {
        Console.WriteLine("操作を実行します...");
        // 何らかの処理...
        throw new InvalidOperationException("操作中に問題が発生しました。");
    }
    catch (InvalidOperationException ex)
    {
        Console.WriteLine($"操作エラーを捕捉しました: {ex.Message}");
        // ここでログ記録などを行い、例外を呼び出し元に通知する
        Console.WriteLine("例外を再スローします...");
        throw; // スタックトレースを保持して再スロー
    }
    // catch (Exception ex)
    // {
    //     Console.WriteLine("別のエラーを捕捉。スタックトレースがリセットされる投げ方:");
    //     throw ex; // 非推奨: スタックトレースがここから始まる
    // }
}

try
{
    ProcessValue(10);
    ProcessValue(-5); // ここで例外が発生
}
catch (ArgumentOutOfRangeException ex)
{
    Console.WriteLine($"引数エラー: {ex.ParamName} - {ex.Message}");
}

try
{
    PerformOperation();
}
catch (InvalidOperationException ex)
{
    Console.WriteLine($"呼び出し元で再スローされた例外を捕捉: {ex.Message}");
    // スタックトレースを見ると、元の発生箇所と再スロー箇所がわかる
    // Console.WriteLine(ex.StackTrace);
}
      

一般的な例外クラス

例外クラス 説明
System.Exception 全ての例外の基底クラス。通常、これを直接スローすることは避ける。
System.SystemException CLRによってスローされる全ての例外の基底クラス。
System.ApplicationException ユーザーアプリケーションによってスローされる例外の基底クラスとして意図されていたが、現在は直接Exceptionを継承することが推奨される。
System.ArgumentException メソッドに渡された引数が無効な場合にスローされる。
System.ArgumentNullException メソッドにnullが許容されない引数として渡された場合にスローされる (ArgumentExceptionの派生)。
System.ArgumentOutOfRangeException 引数の値が許容範囲外の場合にスローされる (ArgumentExceptionの派生)。
System.InvalidOperationException オブジェクトの現在の状態に対してメソッド呼び出しが無効な場合にスローされる。
System.NotSupportedException 呼び出されたメソッドや操作がサポートされていない場合にスローされる。
System.NullReferenceException nullであるオブジェクト参照のメンバーにアクセスしようとした場合にスローされる。
System.IndexOutOfRangeException 配列やコレクションのインデックスが範囲外の場合にスローされる。
System.IO.IOException I/Oエラーが発生した場合の基底クラス。
System.IO.FileNotFoundException アクセスしようとしたファイルが存在しない場合にスローされる (IOExceptionの派生)。
System.IO.DirectoryNotFoundException アクセスしようとしたディレクトリが存在しない場合にスローされる (IOExceptionの派生)。
System.FormatException 引数の形式が無効な場合にスローされる (例: int.Parse("abc"))。
System.DivideByZeroException 整数またはDecimalをゼロで除算しようとした場合にスローされる。
System.OutOfMemoryException プログラムの実行に必要なメモリを確保できない場合にスローされる。
System.StackOverflowException 実行スタックがオーバーフローした場合にスローされる(通常、無限再帰などが原因)。通常、catchできない。
System.ObjectDisposedException 既に破棄 (Dispose) されたオブジェクトに対して操作を実行しようとした場合にスローされる。

リソース管理と using ステートメント

ファイルハンドル、データベース接続、ネットワークソケットなどのアンマネージドリソースや、IDisposable インターフェースを実装するオブジェクトは、使用後に適切に解放(破棄)する必要があります。finally ブロックで解放処理を行うこともできますが、using ステートメントを使うとより簡潔かつ安全に記述できます。

  • using ステートメントは、IDisposable を実装したオブジェクトのスコープを定義します。
  • ブロックを抜ける際(正常終了時も例外発生時も)、オブジェクトの Dispose() メソッドが自動的に呼び出されます。
  • finally ブロックで明示的に Dispose()Close() を呼ぶコードが不要になります。
  • C# 8.0以降、using 宣言 (using var ...) が導入され、波括弧 ({}) が不要になり、変数のスコープの終わりで自動的に破棄されるようになりました。

using System.IO;
using System.Data.SqlClient; // SqlConnectionなどを使用する場合

// --- using ステートメント (ブロック形式) ---
void WriteToFile(string filePath, string content)
{
    // StreamWriterはIDisposableを実装している
    using (StreamWriter writer = new StreamWriter(filePath))
    {
        writer.WriteLine(content);
        // このブロックを抜けるときに writer.Dispose() が自動的に呼ばれる
        // (Dispose() は内部的に Close() を呼ぶことが多い)
        Console.WriteLine("ファイルに書き込みました (usingブロック内)。");

        // 例外が発生した場合でも Dispose() は呼ばれる
        // if (true) throw new Exception("テスト例外");

    } // ここで writer.Dispose() が呼ばれる
    Console.WriteLine("using ブロックを抜けました。");
}

// --- using 宣言 (C# 8.0以降) ---
void ReadFromFile(string filePath)
{
    // using宣言: 波括弧が不要。reader変数のスコープの終わりでDisposeされる
    using var reader = new StreamReader(filePath);

    Console.WriteLine("ファイルから読み取ります (using宣言)...");
    string line;
    while ((line = reader.ReadLine()) != null)
    {
        Console.WriteLine($"> {line}");
    }
    // ReadFromFile メソッドの終わり (readerのスコープの終わり) で reader.Dispose() が呼ばれる
}


// --- 複数のリソースを扱う場合 ---
void CopyFileContent(string sourcePath, string destinationPath)
{
    // 複数の using ステートメントをネスト
    using (StreamReader reader = new StreamReader(sourcePath))
    {
        using (StreamWriter writer = new StreamWriter(destinationPath))
        {
            string line;
            while ((line = reader.ReadLine()) != null)
            {
                writer.WriteLine(line);
            }
        } // writer.Dispose()
    } // reader.Dispose()

    // C# 8.0以降の using 宣言ならもっとシンプルに
    using var reader2 = new StreamReader(sourcePath + ".v2"); // 仮のファイル名
    using var writer2 = new StreamWriter(destinationPath + ".v2"); // 仮のファイル名
    string line2;
    while ((line2 = reader2.ReadLine()) != null)
    {
         writer2.WriteLine(line2);
    }
    // メソッドの終わりで writer2 と reader2 が(宣言と逆順で)Dispose される
}

// 実行例 (ファイルが存在する前提)
string tempFile = "temp.txt";
try
{
    WriteToFile(tempFile, "Hello, using statement!");
    ReadFromFile(tempFile);
}
catch (Exception ex)
{
    Console.WriteLine($"エラー発生: {ex.Message}");
}
finally
{
    if (File.Exists(tempFile))
    {
        File.Delete(tempFile);
        Console.WriteLine($"{tempFile} を削除しました。");
    }
}
      

ファイル・IO操作 💾

ファイルやディレクトリの操作、テキストファイルやバイナリファイルの読み書きなど、System.IO 名前空間の主要な機能。

ファイルとディレクトリの基本情報 (File, Directory, Path)

静的クラス File, Directory, Path を使って、ファイルやディレクトリの存在確認、作成、削除、移動、パスの操作などを行います。


using System.IO;

string testFilePath = "testFile.txt";
string testDirPath = "testDir";
string subDirPath = Path.Combine(testDirPath, "subDir"); // OS依存のパス区切り文字で結合
string movedFilePath = Path.Combine(testDirPath, "movedFile.txt");

// --- Fileクラス ---
Console.WriteLine("--- File クラス ---");
// ファイルの作成 (空ファイル) と書き込み (簡易)
File.WriteAllText(testFilePath, "これはテストファイルです。\n2行目です。");
Console.WriteLine($"{testFilePath} を作成し、書き込みました。");

// ファイルの存在確認
if (File.Exists(testFilePath))
{
    Console.WriteLine($"{testFilePath} は存在します。");

    // ファイルの読み込み (簡易)
    string content = File.ReadAllText(testFilePath);
    Console.WriteLine($"内容:\n{content}");

    string[] lines = File.ReadAllLines(testFilePath);
    Console.WriteLine($"行単位の内容:");
    foreach (var line in lines) Console.WriteLine($"- {line}");

    // ファイル情報の取得 (FileInfoクラスを使用することも可能)
    DateTime lastWriteTime = File.GetLastWriteTime(testFilePath);
    Console.WriteLine($"最終更新日時: {lastWriteTime}");

    // ファイルのコピー
    string copyPath = "testFileCopy.txt";
    File.Copy(testFilePath, copyPath, true); // trueで上書き許可
    Console.WriteLine($"{testFilePath} を {copyPath} にコピーしました。");
    if (File.Exists(copyPath)) File.Delete(copyPath); // コピーしたファイルを削除

    // ファイルの移動 (名前変更にも使える)
    // File.Move(testFilePath, movedFilePath); // 移動先ディレクトリがないとエラー
}
else
{
    Console.WriteLine($"{testFilePath} は存在しません。");
}

// ファイルの削除
if (File.Exists(testFilePath))
{
    File.Delete(testFilePath);
    Console.WriteLine($"{testFilePath} を削除しました。");
}

// --- Directoryクラス ---
Console.WriteLine("\n--- Directory クラス ---");
// ディレクトリの作成
if (!Directory.Exists(testDirPath))
{
    Directory.CreateDirectory(testDirPath);
    Console.WriteLine($"{testDirPath} を作成しました。");
}
// サブディレクトリも再帰的に作成
Directory.CreateDirectory(subDirPath);
Console.WriteLine($"{subDirPath} を作成しました(または既に存在)。");

// ディレクトリ内のファイル一覧取得
Directory.CreateDirectory(testDirPath); // 存在確認してからの方が良いが、CreateDirectoryは存在してもエラーにならない
File.Create(Path.Combine(testDirPath, "file1.txt")).Close(); // ダミーファイル作成
File.Create(Path.Combine(testDirPath, "file2.log")).Close();
Console.WriteLine($"\n{testDirPath} 内のファイル:");
string[] files = Directory.GetFiles(testDirPath); // パターン指定も可能 (e.g., "*.txt")
foreach (var file in files) Console.WriteLine(Path.GetFileName(file)); // Pathクラスでファイル名のみ取得

// ディレクトリ内のサブディレクトリ一覧取得
Console.WriteLine($"\n{testDirPath} 内のディレクトリ:");
string[] dirs = Directory.GetDirectories(testDirPath);
foreach (var dir in dirs) Console.WriteLine(Path.GetFileName(dir));

// ディレクトリの移動
string movedDirPath = "movedTestDir";
if (Directory.Exists(testDirPath) && !Directory.Exists(movedDirPath))
{
    Directory.Move(testDirPath, movedDirPath);
    Console.WriteLine($"{testDirPath} を {movedDirPath} に移動しました。");
    testDirPath = movedDirPath; // 以降の処理のためパスを更新
}

// ディレクトリの削除 (空である必要がある)
// Directory.Delete(subDirPath); // サブディレクトリを先に削除
// Directory.Delete(testDirPath); // その後親ディレクトリを削除
// 再帰的に削除する場合 (注意して使用!)
if (Directory.Exists(testDirPath))
{
     Directory.Delete(testDirPath, true); // trueで中身ごと削除
     Console.WriteLine($"{testDirPath} を再帰的に削除しました。");
}


// --- Pathクラス (パス文字列の操作) ---
Console.WriteLine("\n--- Path クラス ---");
string fullPath = @"C:\Users\Public\Documents\report.docx";
Console.WriteLine($"フルパス: {fullPath}");
Console.WriteLine($"ディレクトリ名: {Path.GetDirectoryName(fullPath)}");
Console.WriteLine($"ファイル名: {Path.GetFileName(fullPath)}");
Console.WriteLine($"拡張子なしファイル名: {Path.GetFileNameWithoutExtension(fullPath)}");
Console.WriteLine($"拡張子: {Path.GetExtension(fullPath)}");
Console.WriteLine($"ルートディレクトリ: {Path.GetPathRoot(fullPath)}");
Console.WriteLine($"パス結合: {Path.Combine("C:\\Data", "Images", "photo.jpg")}");
Console.WriteLine($"一時ファイル名取得: {Path.GetTempFileName()}"); // 実際にファイルが作成される
Console.WriteLine($"一時ディレクトリパス: {Path.GetTempPath()}");
            

テキストファイルの読み書き (StreamReader / StreamWriter)

テキストファイルを効率的に読み書きするためのクラス。エンコーディングを指定できます。using ステートメントと組み合わせて使うのが一般的です。


using System.IO;
using System.Text; // Encodingクラスのため

string textFilePath = "myTextFile.txt";

// --- StreamWriterによる書き込み ---
try
{
    // usingステートメントで確実にDisposeを呼ぶ
    // 第二引数で追記モード (true) or 上書きモード (false, デフォルト) を指定
    // 第三引数でエンコーディングを指定 (デフォルトはUTF8 BOM付き)
    using (StreamWriter writer = new StreamWriter(textFilePath, false, Encoding.UTF8))
    {
        writer.WriteLine("これは StreamWriter で書き込んだ1行目です。");
        writer.WriteLine("こんにちは、世界!🌍");
        writer.Write("Writeメソッドは改行しません。");
        writer.Write("続けて書きます。\n"); // \nで改行も可能
        writer.WriteLine($"現在時刻: {DateTime.Now}");
        // writer.Flush(); // バッファの内容を強制的に書き込む (通常はDispose時に自動で行われる)
    }
    Console.WriteLine($"{textFilePath} に書き込みました。");

    // 追記モードで書き込み
    using (StreamWriter writer = new StreamWriter(textFilePath, true, Encoding.UTF8))
    {
        writer.WriteLine("--- ここから追記 ---");
        writer.WriteLine("追記テスト。");
    }
     Console.WriteLine($"{textFilePath} に追記しました。");

}
catch (IOException ex)
{
    Console.WriteLine($"書き込みエラー: {ex.Message}");
}


// --- StreamReaderによる読み込み ---
if (File.Exists(textFilePath))
{
    try
    {
        using (StreamReader reader = new StreamReader(textFilePath, Encoding.UTF8)) // 書き込み時と同じエンコーディングを指定
        {
            Console.WriteLine($"\n--- {textFilePath} の内容 ---");

            // 1. 一行ずつ読み込む
            Console.WriteLine("--- 一行ずつ読み込み ---");
            string line;
            while ((line = reader.ReadLine()) != null) // ファイルの終わりまで読み込む
            {
                Console.WriteLine(line);
            }

            // StreamReaderは一度最後まで読むと、再度読むためには位置を戻すか再生成が必要
            // reader.BaseStream.Position = 0; // ストリームの位置を先頭に戻す
            // reader.DiscardBufferedData(); // バッファをクリア

            // 2. 全体を一度に読み込む
            // 注意: 巨大なファイルの場合はメモリを大量に消費する可能性あり
            // reader.BaseStream.Position = 0;
            // reader.DiscardBufferedData();
            // string allContent = reader.ReadToEnd();
            // Console.WriteLine("\n--- 全体読み込み ---");
            // Console.WriteLine(allContent);

        } // usingを抜ける際にreader.Dispose() (内部的にClose()) が呼ばれる
    }
    catch (IOException ex)
    {
        Console.WriteLine($"読み込みエラー: {ex.Message}");
    }
    finally
    {
         // 作成したテストファイルを削除
         // File.Delete(textFilePath);
    }
}
            

バイナリファイルの読み書き (FileStream, BinaryReader / BinaryWriter)

テキスト以外のデータ(画像、音声、実行ファイル、独自のデータ構造など)をバイト単位で読み書きします。


using System.IO;
using System.Text;

string binaryFilePath = "myBinaryFile.dat";

// --- BinaryWriterによる書き込み ---
try
{
    // FileStreamでファイルを開く (または作成)
    using (FileStream fs = new FileStream(binaryFilePath, FileMode.Create, FileAccess.Write))
    // using (FileStream fs = File.Create(binaryFilePath)) // こちらでも可
    {
        // FileStreamをラップしてBinaryWriterを作成
        using (BinaryWriter writer = new BinaryWriter(fs, Encoding.UTF8, false)) // leaveOpen=falseでFileStreamも一緒にDispose
        {
            // 様々なデータ型をバイナリ形式で書き込む
            writer.Write(123);                  // int (4バイト)
            writer.Write(3.14159);              // double (8バイト)
            writer.Write(true);                 // bool (1バイト)
            writer.Write("Hello Binary!");      // string (Length-prefixed UTF8)
            byte[] byteArray = { 0x01, 0x02, 0x03, 0x04, 0x05 };
            writer.Write(byteArray.Length);     // バイト配列の長さを先に書き込む (読み込み時に必要)
            writer.Write(byteArray);            // バイト配列本体

            Console.WriteLine($"{binaryFilePath} にバイナリデータを書き込みました。");
        } // BinaryWriter.Dispose() -> FileStream.Dispose()
    }
}
catch (IOException ex)
{
    Console.WriteLine($"バイナリ書き込みエラー: {ex.Message}");
}

// --- BinaryReaderによる読み込み ---
if (File.Exists(binaryFilePath))
{
    try
    {
        using (FileStream fs = new FileStream(binaryFilePath, FileMode.Open, FileAccess.Read))
        {
            using (BinaryReader reader = new BinaryReader(fs, Encoding.UTF8, false))
            {
                Console.WriteLine($"\n--- {binaryFilePath} からバイナリデータを読み込み ---");

                // 書き込んだ順序と型に合わせて読み込む必要がある
                int intValue = reader.ReadInt32();
                double doubleValue = reader.ReadDouble();
                bool boolValue = reader.ReadBoolean();
                string stringValue = reader.ReadString(); // Length-prefixedで読み込み
                int arrayLength = reader.ReadInt32();      // 配列の長さを読み込む
                byte[] readByteArray = reader.ReadBytes(arrayLength); // 指定した長さのバイトを読む

                Console.WriteLine($"Int: {intValue}");
                Console.WriteLine($"Double: {doubleValue}");
                Console.WriteLine($"Bool: {boolValue}");
                Console.WriteLine($"String: {stringValue}");
                Console.Write("Byte Array: ");
                foreach (byte b in readByteArray)
                {
                    Console.Write($"{b:X2} "); // 16進数で表示
                }
                Console.WriteLine();

                // ファイルの終端チェック
                // if (reader.PeekChar() == -1) // 次の読み取りがないかチェック
                // {
                //     Console.WriteLine("ファイルの終端に達しました。");
                // }
                 // または BaseStream の Position と Length を比較
                 if (reader.BaseStream.Position == reader.BaseStream.Length)
                 {
                     Console.WriteLine("ファイルの終端に達しました。");
                 }
            }
        }
    }
    catch (EndOfStreamException) // ファイルの終端を超えて読み込もうとした場合
    {
        Console.WriteLine("エラー: ファイルの終端を超えて読み込もうとしました。");
    }
    catch (IOException ex)
    {
        Console.WriteLine($"バイナリ読み込みエラー: {ex.Message}");
    }
    finally
    {
        // 作成したテストファイルを削除
        // File.Delete(binaryFilePath);
    }
}
            

ストリーム (Stream)

Stream は、バイトシーケンスの抽象的な表現です。ファイル、メモリ、ネットワークなど、様々なデータソース/デスティネーションに対する統一的なアクセス方法を提供します。

  • FileStream: ファイルに対するストリーム。
  • MemoryStream: メモリ内のバイト配列に対するストリーム。
  • NetworkStream: ネットワーク接続に対するストリーム。
  • BufferedStream: 別のストリームにバッファリング機能を追加するラッパーストリーム。
  • GZipStream / DeflateStream: データを圧縮/展開するためのストリーム。
  • StreamReader / StreamWriter / BinaryReader / BinaryWriter は、これらの基底となるストリームを扱いやすくするためのクラスです。

using System.IO;
using System.IO.Compression; // GZipStreamのため
using System.Text;

// MemoryStream の例: メモリ内でデータの読み書き
byte[] buffer;
using (MemoryStream ms = new MemoryStream())
{
    using (StreamWriter writer = new StreamWriter(ms, Encoding.UTF8, -1, true)) // leaveOpen=true で MemoryStream は閉じない
    {
        writer.WriteLine("MemoryStream Test");
        writer.WriteLine("インメモリデータ");
    } // StreamWriter のみ Dispose (バッファはフラッシュされる)

    // MemoryStream からバイト配列を取得
    buffer = ms.ToArray();
    Console.WriteLine($"MemoryStream から取得したバイト数: {buffer.Length}");

    // バイト配列を文字列として表示 (確認用)
    Console.WriteLine($"メモリ内容: {Encoding.UTF8.GetString(buffer)}");

    // MemoryStream の位置を先頭に戻して読み取ることも可能
    ms.Position = 0;
    using (StreamReader reader = new StreamReader(ms, Encoding.UTF8))
    {
        Console.WriteLine("MemoryStream から再度読み込み:");
        Console.WriteLine(reader.ReadToEnd());
    }
} // MemoryStream が Dispose される


// GZipStream の例: データの圧縮と展開
string originalText = "これは圧縮される予定の長い長いテキストです。繰り返し繰り返し。";
byte[] originalBytes = Encoding.UTF8.GetBytes(originalText);
byte[] compressedBytes;
byte[] decompressedBytes;

Console.WriteLine($"\n--- GZipStream ---");
Console.WriteLine($"元データサイズ: {originalBytes.Length} バイト");

// 圧縮
using (MemoryStream compressedStream = new MemoryStream())
{
    // MemoryStream を GZipStream でラップ (圧縮モード)
    using (GZipStream gzipStream = new GZipStream(compressedStream, CompressionMode.Compress, true)) // leaveOpen=true
    {
        // 圧縮ストリームに元のデータを書き込む
        gzipStream.Write(originalBytes, 0, originalBytes.Length);
    } // GZipStream を Dispose すると圧縮が完了し、基になる Stream に書き込まれる
    compressedBytes = compressedStream.ToArray();
    Console.WriteLine($"圧縮後データサイズ: {compressedBytes.Length} バイト");
}

// 展開
using (MemoryStream compressedStream = new MemoryStream(compressedBytes)) // 圧縮データで MemoryStream を初期化
{
    using (MemoryStream decompressedStream = new MemoryStream())
    {
        // MemoryStream を GZipStream でラップ (展開モード)
        using (GZipStream gzipStream = new GZipStream(compressedStream, CompressionMode.Decompress))
        {
            // 展開ストリームから読み取ったデータを別の MemoryStream にコピー
            gzipStream.CopyTo(decompressedStream);
        } // GZipStream を Dispose
        decompressedBytes = decompressedStream.ToArray();
    }
}

Console.WriteLine($"展開後データサイズ: {decompressedBytes.Length} バイト");
string decompressedText = Encoding.UTF8.GetString(decompressedBytes);
Console.WriteLine($"展開後テキスト: {decompressedText}");
Console.WriteLine($"展開成功: {originalText == decompressedText}");

            

文字列操作 ✍️

System.String クラスと System.Text.StringBuilder クラスを使用した文字列の生成、比較、検索、置換、書式設定など。

注意: string不変 (immutable) です。文字列を変更する操作 (連結、置換など) は、実際には新しい文字列オブジェクトを生成します。多数の変更を行う場合は StringBuilder の使用を検討してください。

文字列の生成と連結


// 文字列リテラル
string s1 = "Hello";
string s2 = "World";
string verbatimString = @"C:\Program Files\MyApp\data.txt
改行もそのまま含まれる。\エスケープシーケンスは無効 ( "" は "" と書く)";
string emptyString = "";
string nullString = null; // null参照

// 連結 (+)
string s3 = s1 + " " + s2 + "!"; // "Hello World!" (内部で新しいstringが複数生成される可能性)
Console.WriteLine(s3);

// 複合代入 (+=)
string message = "Current value: ";
int value = 10;
message += value; // message = message + value.ToString(); とほぼ同じ
Console.WriteLine(message); // "Current value: 10"

// string.Concat()
string s4 = string.Concat(s1, " ", s2, " ", 123); // 複数の引数を連結
Console.WriteLine(s4); // "Hello World 123"

// string.Join() - 配列やコレクションの要素を指定した区切り文字で連結
string[] names = { "Alice", "Bob", "Charlie" };
string joinedNames = string.Join(", ", names); // ", " で連結
Console.WriteLine(joinedNames); // "Alice, Bob, Charlie"

// 文字列補間 (String Interpolation) (C# 6.0以降) - 推奨される方法 ✨
string firstName = "太郎";
string lastName = "山田";
int age = 30;
string interpolated = $"名前: {lastName} {firstName}, 年齢: {age}歳";
Console.WriteLine(interpolated); // "名前: 山田 太郎, 年齢: 30歳"
// 式も埋め込める
Console.WriteLine($"来年の年齢: {age + 1}歳");
// 書式指定も可能
double price = 1980.5;
Console.WriteLine($"価格: {price:C}"); // 通貨形式 (現在のカルチャ設定に依存)
Console.WriteLine($"価格 (小数点以下2桁): {price:F2}");
DateTime now = DateTime.Now;
Console.WriteLine($"現在日時: {now:yyyy/MM/dd HH:mm:ss}");
// 条件演算子も使える
Console.WriteLine($"ステータス: {(age >= 20 ? "成人" : "未成年")}");
// @と$を組み合わせる (逐語的補間文字列)
string filePath = @"C:\data";
Console.WriteLine($@"ログファイルのパス: {filePath}\log_{now:yyyyMMdd}.log");
      

文字列の比較


string strA = "apple";
string strB = "Apple";
string strC = "apple";

// 等価演算子 (==, !=) - 値の比較 (大文字/小文字を区別)
Console.WriteLine($"strA == strB: {strA == strB}"); // false
Console.WriteLine($"strA == strC: {strA == strC}"); // true
Console.WriteLine($"strA != strB: {strA != strB}"); // true

// string.Equals() - より詳細な比較が可能
Console.WriteLine($"strA.Equals(strB): {strA.Equals(strB)}"); // false (デフォルトは大文字/小文字区別)
Console.WriteLine($"strA.Equals(strC): {strA.Equals(strC)}"); // true

// 大文字/小文字を区別しない比較
Console.WriteLine($"strA.Equals(strB, StringComparison.OrdinalIgnoreCase): {strA.Equals(strB, StringComparison.OrdinalIgnoreCase)}"); // true
Console.WriteLine($"strA.Equals(strB, StringComparison.CurrentCultureIgnoreCase): {strA.Equals(strB, StringComparison.CurrentCultureIgnoreCase)}"); // true (カルチャ依存の比較)

// string.Compare() - 辞書順比較 (戻り値: 0=等しい, <0=str1が小さい, >0=str1が大きい)
Console.WriteLine($"string.Compare(strA, strB): {string.Compare(strA, strB)}"); // >0 ('a' > 'A')
Console.WriteLine($"string.Compare(strA, strC): {string.Compare(strA, strC)}"); // 0
Console.WriteLine($"string.Compare(strA, strB, StringComparison.OrdinalIgnoreCase): {string.Compare(strA, strB, StringComparison.OrdinalIgnoreCase)}"); // 0

// 比較オプション (StringComparison)
// - Ordinal: バイトレベルの比較 (高速、非言語的)。ファイルパス、プロトコルキーなど、言語に依存しない場合に推奨。
// - OrdinalIgnoreCase: Ordinalの大文字/小文字を区別しない版。
// - CurrentCulture: 現在のスレッドのカルチャに基づいて比較 (言語的)。ユーザーに表示する文字列の比較など。
// - CurrentCultureIgnoreCase: CurrentCultureの大文字/小文字を区別しない版。
// - InvariantCulture: カルチャに依存しない言語的な比較 (英語ベース)。
// - InvariantCultureIgnoreCase: InvariantCultureの大文字/小文字を区別しない版。
      

文字列の検索と検査


string text = "The quick brown fox jumps over the lazy dog.";

// IndexOf(), LastIndexOf() - 文字または部分文字列が最初/最後に現れるインデックスを検索
Console.WriteLine($"'x' の最初のインデックス: {text.IndexOf('x')}"); // 16
Console.WriteLine($"'o' の最初のインデックス: {text.IndexOf('o')}"); // 12
Console.WriteLine($"'o' の最後のインデックス: {text.LastIndexOf('o')}"); // 40
Console.WriteLine($"部分文字列 \"the\" の最初のインデックス: {text.IndexOf("the")}"); // 31 (大文字小文字区別)
Console.WriteLine($"部分文字列 \"The\" の最初のインデックス: {text.IndexOf("The")}"); // 0
Console.WriteLine($"部分文字列 \"the\" の最初のインデックス (IgnoreCase): {text.IndexOf("the", StringComparison.OrdinalIgnoreCase)}"); // 0
Console.WriteLine($"部分文字列 \"cat\" のインデックス: {text.IndexOf("cat")}"); // -1 (見つからない場合)

// Contains() - 特定の文字または部分文字列が含まれているか
Console.WriteLine($"'q' が含まれるか: {text.Contains('q')}"); // true
Console.WriteLine($"\"jumps\" が含まれるか: {text.Contains("jumps")}"); // true
Console.WriteLine($"\"CAT\" が含まれるか (IgnoreCase): {text.Contains("CAT", StringComparison.OrdinalIgnoreCase)}"); // false (ContainsはC#バージョンによりIgnoreCaseオーバーロード有無が異なる。ない場合はIndexOfを使う)
// C# 7.2 以前などでIgnoreCaseが必要な場合:
bool containsCatIgnoreCase = text.IndexOf("CAT", StringComparison.OrdinalIgnoreCase) >= 0;
Console.WriteLine($"\"CAT\" が含まれるか (IgnoreCase, IndexOf使用): {containsCatIgnoreCase}"); // true

// StartsWith(), EndsWith() - 特定の文字列で始まるか/終わるか
Console.WriteLine($"\"The\" で始まるか: {text.StartsWith("The")}"); // true
Console.WriteLine($"\"the\" で始まるか: {text.StartsWith("the")}"); // false
Console.WriteLine($"\"the\" で始まるか (IgnoreCase): {text.StartsWith("the", StringComparison.OrdinalIgnoreCase)}"); // true
Console.WriteLine($"\"dog.\" で終わるか: {text.EndsWith("dog.")}"); // true
Console.WriteLine($"\".\" で終わるか: {text.EndsWith('.')}"); // true (charも可)
Console.WriteLine($"\"DOG.\" で終わるか (IgnoreCase): {text.EndsWith("DOG.", StringComparison.OrdinalIgnoreCase)}"); // true

// IsNullOrEmpty() - 文字列が null または空文字列 ("") かどうか
string s_empty = "";
string s_null = null;
string s_valid = "abc";
Console.WriteLine($"IsNullOrEmpty(s_empty): {string.IsNullOrEmpty(s_empty)}"); // true
Console.WriteLine($"IsNullOrEmpty(s_null): {string.IsNullOrEmpty(s_null)}"); // true
Console.WriteLine($"IsNullOrEmpty(s_valid): {string.IsNullOrEmpty(s_valid)}"); // false

// IsNullOrWhiteSpace() - 文字列が null, 空文字列, または空白文字のみで構成されているか
string s_space = "   ";
string s_tab = "\t";
Console.WriteLine($"IsNullOrWhiteSpace(s_empty): {string.IsNullOrWhiteSpace(s_empty)}"); // true
Console.WriteLine($"IsNullOrWhiteSpace(s_null): {string.IsNullOrWhiteSpace(s_null)}"); // true
Console.WriteLine($"IsNullOrWhiteSpace(s_space): {string.IsNullOrWhiteSpace(s_space)}"); // true
Console.WriteLine($"IsNullOrWhiteSpace(s_tab): {string.IsNullOrWhiteSpace(s_tab)}"); // true
Console.WriteLine($"IsNullOrWhiteSpace(s_valid): {string.IsNullOrWhiteSpace(s_valid)}"); // false
      

部分文字列の抽出と置換


string text = "The quick brown fox jumps over the lazy dog.";

// Substring() - 指定したインデックスから部分文字列を抽出
string sub1 = text.Substring(10); // インデックス10から最後まで
Console.WriteLine($"Substring(10): \"{sub1}\""); // "brown fox jumps over the lazy dog."
string sub2 = text.Substring(10, 5); // インデックス10から5文字
Console.WriteLine($"Substring(10, 5): \"{sub2}\""); // "brown"

// Replace() - 文字または部分文字列を置換
string replaced1 = text.Replace('o', '*'); // 文字'o'を'*'に置換
Console.WriteLine($"Replace('o', '*'): \"{replaced1}\"");
string replaced2 = text.Replace("fox", "cat"); // 部分文字列 "fox" を "cat" に置換
Console.WriteLine($"Replace(\"fox\", \"cat\"): \"{replaced2}\"");
// 大文字/小文字を区別しない置換 (標準にはない。.NET Core以降 StringComparison 指定可能)
// .NET Framework の場合、Regex.Replaceを使うか、自前で実装する必要がある
#if NETCOREAPP
string replacedIgnoreCase = text.Replace("the", "A", StringComparison.OrdinalIgnoreCase);
Console.WriteLine($"Replace(\"the\", \"A\", IgnoreCase): \"{replacedIgnoreCase}\"");
#else
// .NET Framework での代替例 (Regex使用)
// using System.Text.RegularExpressions;
// string replacedIgnoreCase = Regex.Replace(text, "the", "A", RegexOptions.IgnoreCase);
// Console.WriteLine($"Replace(\"the\", \"A\", IgnoreCase, Regex): \"{replacedIgnoreCase}\"");
#endif

// Remove() - 指定したインデックスから文字を削除
string removed1 = text.Remove(10); // インデックス10以降を削除
Console.WriteLine($"Remove(10): \"{removed1}\""); // "The quick "
string removed2 = text.Remove(10, 6); // インデックス10から6文字 ("brown ")を削除
Console.WriteLine($"Remove(10, 6): \"{removed2}\""); // "The quick fox jumps over the lazy dog."

// Insert() - 指定したインデックスに文字列を挿入
string inserted = text.Insert(10, "very ");
Console.WriteLine($"Insert(10, \"very \"): \"{inserted}\""); // "The quick very brown fox..."
      

大文字/小文字変換、空白除去


string mixedCase = "  Hello World!  ";

// ToUpper(), ToLower() - 大文字/小文字に変換 (カルチャ依存)
Console.WriteLine($"ToUpper(): \"{mixedCase.ToUpper()}\""); // "  HELLO WORLD!  "
Console.WriteLine($"ToLower(): \"{mixedCase.ToLower()}\""); // "  hello world!  "

// ToUpperInvariant(), ToLowerInvariant() - インバリアントカルチャで変換 (非言語的)
Console.WriteLine($"ToUpperInvariant(): \"{mixedCase.ToUpperInvariant()}\"");
Console.WriteLine($"ToLowerInvariant(): \"{mixedCase.ToLowerInvariant()}\"");

// Trim(), TrimStart(), TrimEnd() - 先頭/末尾/両方の空白文字を除去
Console.WriteLine($"Trim(): \"{mixedCase.Trim()}\""); // "Hello World!"
Console.WriteLine($"TrimStart(): \"{mixedCase.TrimStart()}\""); // "Hello World!  "
Console.WriteLine($"TrimEnd(): \"{mixedCase.TrimEnd()}\""); // "  Hello World!"
// 特定の文字を除去することも可能
string messy = "***Hello***";
Console.WriteLine($"Trim('*'): \"{messy.Trim('*')}\""); // "Hello"
      

分割 (Split)

文字列を指定した区切り文字で分割し、文字列配列を生成します。


string csvLine = "apple,banana,orange,grape";
string sentence = "The quick brown fox";

// 単一の文字で分割
string[] fruits = csvLine.Split(',');
Console.WriteLine("フルーツ:");
foreach (var fruit in fruits) Console.WriteLine($"- {fruit}");

// 複数の文字で分割
string data = "one;two,three four";
char[] separators = { ';', ',', ' ' };
string[] items = data.Split(separators); // 空の要素が含まれる可能性あり
Console.WriteLine("\n複数の区切り文字:");
foreach (var item in items) Console.WriteLine($"- \"{item}\"");
// 出力: "one", "two", "three", "four"

// 空の要素を除去するオプション
string dataWithEmpty = "A,,B, C , D";
string[] itemsNoEmpty = dataWithEmpty.Split(new char[]{','}, StringSplitOptions.RemoveEmptyEntries);
Console.WriteLine("\n空要素除去:");
foreach (var item in itemsNoEmpty) Console.WriteLine($"- \"{item}\""); // "A", "B", " C ", " D" (前後の空白は残る)

// 空要素除去 + 各要素の空白除去 (LINQと組み合わせる)
string[] itemsCleaned = dataWithEmpty
                         .Split(new char[]{','}, StringSplitOptions.RemoveEmptyEntries)
                         .Select(s => s.Trim()) // 各要素をTrim
                         .ToArray();
Console.WriteLine("\n空要素除去 + Trim:");
foreach (var item in itemsCleaned) Console.WriteLine($"- \"{item}\""); // "A", "B", "C", "D"

// 文字列で分割 (.NET Core / .NET 5+ で StringSplitOptions が使える)
string textToSplit = "apple--banana==orange";
#if NETCOREAPP || NET5_0_OR_GREATER
string[] parts = textToSplit.Split(new string[] { "--", "==" }, StringSplitOptions.None);
Console.WriteLine("\n文字列で分割:");
foreach (var part in parts) Console.WriteLine($"- \"{part}\""); // "apple", "banana", "orange"
#endif
      

書式設定 (string.Format)

複合書式指定を使用して文字列を整形します。文字列補間 (`$””`) が使える場合はそちらの方が推奨されますが、書式文字列を外部から読み込む場合などに使われます。


string name = "田中";
int score = 85;
DateTime date = new DateTime(2023, 10, 26);

// {index[,alignment][:formatString]} の形式
// index: 引数の0ベースインデックス
// alignment: 最小幅 (正の数=右揃え, 負の数=左揃え)
// formatString: 標準またはカスタムの書式指定文字列

string formatted1 = string.Format("名前: {0}, スコア: {1}", name, score);
Console.WriteLine(formatted1); // "名前: 田中, スコア: 85"

// アラインメント
string formatted2 = string.Format("名前: |{0,-10}| スコア: |{1,5}|", name, score);
Console.WriteLine(formatted2); // "名前: |田中        | スコア: |   85|"

// 書式指定文字列
string formatted3 = string.Format("スコア(3桁): {0:D3}, 日付: {1:yyyy/MM/dd}, 金額: {2:C}",
                                  score, date, 1234.56);
Console.WriteLine(formatted3); // "スコア(3桁): 085, 日付: 2023/10/26, 金額: ¥1,235" (通貨記号はカルチャ依存)

// IFormatProvider を指定してカルチャを指定することも可能
// using System.Globalization;
// string formattedUS = string.Format(new CultureInfo("en-US"), "金額: {0:C}", 1234.56);
// Console.WriteLine(formattedUS); // "金額: $1,234.56"
      

StringBuilder

多数の文字列変更(特にループ内での連結など)を行う場合に、string の代わりに StringBuilder (System.Text 名前空間) を使用するとパフォーマンスが向上します。StringBuilder は可変 (mutable) です。


using System.Text;

// StringBuilderの初期化
StringBuilder sb = new StringBuilder(); // デフォルト容量
// StringBuilder sb = new StringBuilder("初期文字列");
// StringBuilder sb = new StringBuilder(1024); // 初期容量を指定

Console.WriteLine("StringBuilder を使用した連結:");
// Append() で文字列や様々な型を追加
sb.Append("これは ");
sb.Append("StringBuilder ");
sb.Append("のテストです。");
sb.AppendLine(); // 文字列を追加して改行
sb.AppendFormat("数値: {0}, 日付: {1:d}", 123, DateTime.Now); // 書式指定して追加
sb.AppendLine();

// Insert() で指定位置に挿入
sb.Insert(0, "[開始] ");

// Replace() で置換
sb.Replace("テスト", "デモンストレーション");

// Remove() で削除
// sb.Remove(5, 10); // インデックス5から10文字削除

// 最終的な文字列を取得するには ToString() を呼ぶ
string finalString = sb.ToString();
Console.WriteLine(finalString);

// 容量と長さ
Console.WriteLine($"長さ: {sb.Length}");
Console.WriteLine($"容量: {sb.Capacity}");
// sb.Clear(); // 内容をクリア (長さは0になるが、容量は変わらない)
// sb.EnsureCapacity(2048); // 必要なら容量を増やす
      

日付と時刻 🗓️

System.DateTime, System.DateTimeOffset, System.TimeSpan 構造体を使用した日付と時刻の表現、操作、書式設定。

DateTime: 日付と時刻の表現

特定の日付と時刻を表します。Kind プロパティ (Local, Utc, Unspecified) が重要です。


// 現在の日付と時刻
DateTime now = DateTime.Now; // ローカル時刻 (OSのタイムゾーン設定に基づく)
DateTime utcNow = DateTime.UtcNow; // UTC (協定世界時)
DateTime today = DateTime.Today; // 今日の日付の0時0分 (ローカル)

Console.WriteLine($"Now (Local): {now} (Kind: {now.Kind})");
Console.WriteLine($"UtcNow:      {utcNow} (Kind: {utcNow.Kind})");
Console.WriteLine($"Today (Local): {today} (Kind: {today.Kind})");

// 特定の日付と時刻を指定して作成
DateTime specificDate = new DateTime(2023, 10, 26, 15, 30, 0); // KindはUnspecified
Console.WriteLine($"Specific Date: {specificDate} (Kind: {specificDate.Kind})");

// Kindを指定して作成
DateTime specificUtc = new DateTime(2023, 10, 26, 6, 30, 0, DateTimeKind.Utc);
DateTime specificLocal = new DateTime(2023, 10, 26, 15, 30, 0, DateTimeKind.Local);
Console.WriteLine($"Specific UTC:  {specificUtc} (Kind: {specificUtc.Kind})");
Console.WriteLine($"Specific Local:{specificLocal} (Kind: {specificLocal.Kind})");

// 文字列からのパース (カルチャ依存)
string dateString = "2023/10/27 10:00:00";
try
{
    DateTime parsedDate = DateTime.Parse(dateString); // 現在のカルチャでパース
    Console.WriteLine($"Parsed Date: {parsedDate} (Kind: {parsedDate.Kind})"); // KindはUnspecifiedになることが多い

    // 書式を厳密に指定してパース (推奨)
    DateTime parsedExact = DateTime.ParseExact(dateString, "yyyy/MM/dd HH:mm:ss", System.Globalization.CultureInfo.InvariantCulture);
    Console.WriteLine($"Parsed Exact: {parsedExact} (Kind: {parsedExact.Kind})");

    // TryParse / TryParseExact (パース失敗時に例外を投げない)
    if (DateTime.TryParse("invalid date", out DateTime failedParse))
    {
        // パース成功時の処理
    }
    else
    {
        Console.WriteLine("\"invalid date\" のパースに失敗しました。");
    }
}
catch (FormatException ex)
{
    Console.WriteLine($"日付文字列のパースエラー: {ex.Message}");
}

// DateTimeの各要素へのアクセス
Console.WriteLine($"\n--- DateTimeの要素 ({now:yyyy/MM/dd HH:mm:ss}) ---");
Console.WriteLine($"Year:   {now.Year}");
Console.WriteLine($"Month:  {now.Month}");
Console.WriteLine($"Day:    {now.Day}");
Console.WriteLine($"Hour:   {now.Hour}");
Console.WriteLine($"Minute: {now.Minute}");
Console.WriteLine($"Second: {now.Second}");
Console.WriteLine($"Millisecond: {now.Millisecond}");
Console.WriteLine($"DayOfWeek: {now.DayOfWeek}"); // Sunday, Monday, ...
Console.WriteLine($"DayOfYear: {now.DayOfYear}");
Console.WriteLine($"TimeOfDay: {now.TimeOfDay}"); // TimeSpan (0時からの経過時間)
Console.WriteLine($"Ticks:   {now.Ticks}"); // 100ナノ秒単位のティック数
            

TimeSpan: 時間間隔の表現

時間の間隔を表します。日、時、分、秒、ミリ秒で表現できます。


// TimeSpanの作成
TimeSpan oneHour = TimeSpan.FromHours(1);
TimeSpan ninetyMinutes = TimeSpan.FromMinutes(90);
TimeSpan twoDays = TimeSpan.FromDays(2.5); // 2日と12時間
TimeSpan specificTime = new TimeSpan(3, 30, 0); // 3時間30分0秒
TimeSpan fromTicks = TimeSpan.FromTicks(10000000); // 1秒 (1 Tick = 100ナノ秒)

Console.WriteLine($"\n--- TimeSpan ---");
Console.WriteLine($"1時間: {oneHour}");
Console.WriteLine($"90分: {ninetyMinutes}");
Console.WriteLine($"2.5日: {twoDays}");
Console.WriteLine($"3時間30分: {specificTime}");
Console.WriteLine($"1秒 (Ticks): {fromTicks}");

// TimeSpanのプロパティ
Console.WriteLine($"\n--- TimeSpanの要素 ({ninetyMinutes}) ---");
Console.WriteLine($"TotalDays:    {ninetyMinutes.TotalDays}"); // 合計日数 (1.5)
Console.WriteLine($"TotalHours:   {ninetyMinutes.TotalHours}"); // 合計時間 (1.5)
Console.WriteLine($"TotalMinutes: {ninetyMinutes.TotalMinutes}"); // 合計分 (90)
Console.WriteLine($"TotalSeconds: {ninetyMinutes.TotalSeconds}"); // 合計秒 (5400)
Console.WriteLine($"TotalMilliseconds: {ninetyMinutes.TotalMilliseconds}"); // 合計ミリ秒 (5400000)
Console.WriteLine($"Days:         {ninetyMinutes.Days}"); // 日部分 (0)
Console.WriteLine($"Hours:        {ninetyMinutes.Hours}"); // 時間部分 (1)
Console.WriteLine($"Minutes:      {ninetyMinutes.Minutes}"); // 分部分 (30)
Console.WriteLine($"Seconds:      {ninetyMinutes.Seconds}"); // 秒部分 (0)
Console.WriteLine($"Milliseconds: {ninetyMinutes.Milliseconds}"); // ミリ秒部分 (0)
Console.WriteLine($"Ticks:        {ninetyMinutes.Ticks}");

// TimeSpanの演算
TimeSpan t1 = TimeSpan.FromHours(2);
TimeSpan t2 = TimeSpan.FromMinutes(45);
TimeSpan sum = t1 + t2; // 加算
TimeSpan diff = t1 - t2; // 減算
TimeSpan neg = -t1;    // 符号反転
TimeSpan abs = diff.Duration(); // 絶対値 (常に正)

Console.WriteLine($"\n--- TimeSpanの演算 ---");
Console.WriteLine($"{t1} + {t2} = {sum}"); // 02:45:00
Console.WriteLine($"{t1} - {t2} = {diff}"); // 01:15:00
Console.WriteLine($"-{t1} = {neg}"); // -02:00:00
Console.WriteLine($"Duration({diff}) = {abs}"); // 01:15:00
            

DateTime と TimeSpan の演算

DateTimeTimeSpan を加算・減算して、未来や過去の日時を計算できます。DateTime 同士の減算で TimeSpan を得られます。


DateTime start = new DateTime(2023, 10, 26, 12, 0, 0);
TimeSpan duration = TimeSpan.FromHours(3.5);

// 加算・減算
DateTime end = start + duration; // 3.5時間後
DateTime past = start - TimeSpan.FromDays(1); // 1日前

Console.WriteLine($"\n--- DateTime と TimeSpan の演算 ---");
Console.WriteLine($"開始時刻: {start:g}");
Console.WriteLine($"期間:     {duration}");
Console.WriteLine($"終了時刻: {end:g}");
Console.WriteLine($"1日前:    {past:d}");

// DateTime間の差
DateTime eventStart = new DateTime(2023, 11, 1, 9, 0, 0);
DateTime eventEnd = new DateTime(2023, 11, 1, 17, 30, 0);
TimeSpan eventDuration = eventEnd - eventStart;
Console.WriteLine($"イベント期間: {eventDuration}"); // 08:30:00

// Add系のメソッド (可読性が高い場合がある)
DateTime nextWeek = start.AddDays(7);
DateTime prevMonth = start.AddMonths(-1);
DateTime nextHour = start.AddHours(1);

Console.WriteLine($"来週: {nextWeek:d}");
Console.WriteLine($"先月: {prevMonth:d}");
Console.WriteLine($"1時間後: {nextHour:t}");
            

DateTimeOffset: タイムゾーンオフセット付き日時

UTCからのオフセット(時差)を含む日付と時刻を表します。異なるタイムゾーンの日時を正確に扱う場合に DateTime より適しています。


// 現在の日時とオフセット
DateTimeOffset dtoNow = DateTimeOffset.Now; // ローカル時刻とOSのオフセット
DateTimeOffset dtoUtcNow = DateTimeOffset.UtcNow; // UTC (オフセットは+00:00)

Console.WriteLine($"\n--- DateTimeOffset ---");
Console.WriteLine($"Now (Local Offset): {dtoNow}"); // 例: 2023/10/26 15:30:00 +09:00
Console.WriteLine($"UtcNow:             {dtoUtcNow}"); // 例: 2023/10/26 06:30:00 +00:00

// 特定の日時とオフセットを指定して作成
TimeSpan jstOffset = TimeSpan.FromHours(9); // 日本標準時 (+09:00)
DateTimeOffset specificDto = new DateTimeOffset(2023, 11, 15, 10, 0, 0, jstOffset);
Console.WriteLine($"Specific DTO (JST): {specificDto}");

// DateTimeOffsetの要素
Console.WriteLine($"\n--- DateTimeOffsetの要素 ({dtoNow}) ---");
Console.WriteLine($"DateTime:      {dtoNow.DateTime}"); // DateTime部分 (KindはUnspecified)
Console.WriteLine($"UtcDateTime:   {dtoNow.UtcDateTime}"); // UTCに変換されたDateTime (KindはUtc)
Console.WriteLine($"LocalDateTime: {dtoNow.LocalDateTime}"); // ローカルに変換されたDateTime (KindはLocal)
Console.WriteLine($"Offset:        {dtoNow.Offset}"); // UTCからのオフセット
Console.WriteLine($"Ticks:         {dtoNow.Ticks}"); // UTCエポックからのティック数
Console.WriteLine($"UtcTicks:      {dtoNow.UtcTicks}"); // 上と同じ

// タイムゾーン変換
DateTimeOffset timeInTokyo = specificDto;
// TimeZoneInfo pacificZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time"); // Windows
TimeZoneInfo pacificZone;
try
{
    // OSによってIDが異なる場合がある (Windows vs Linux/macOS)
    if (System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows))
    {
        pacificZone = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
    }
    else
    {
         pacificZone = TimeZoneInfo.FindSystemTimeZoneById("America/Los_Angeles"); // IANA ID
    }
     DateTimeOffset timeInPacific = TimeZoneInfo.ConvertTime(timeInTokyo, pacificZone);
     Console.WriteLine($"東京時刻 ({timeInTokyo.Offset}): {timeInTokyo}");
     Console.WriteLine($"太平洋時刻 ({timeInPacific.Offset}): {timeInPacific}");
}
catch (TimeZoneNotFoundException)
{
    Console.WriteLine("指定されたタイムゾーンが見つかりませんでした。");
}
catch (InvalidTimeZoneException)
{
     Console.WriteLine("タイムゾーン情報が無効です。");
}


// DateTimeOffsetの演算もDateTimeと同様に可能
DateTimeOffset futureDto = dtoNow.AddHours(5);
TimeSpan dtoDiff = futureDto - dtoNow;
Console.WriteLine($"5時間後: {futureDto}");
Console.WriteLine($"差: {dtoDiff}");
            

書式設定 (ToString)

ToString() メソッドに書式指定文字列を渡して、日付や時刻を特定の形式の文字列に変換します。

書式指定子 説明 例 (2023/10/26 15:30:45)
d短い日付パターン2023/10/26
D長い日付パターン2023年10月26日
t短い時刻パターン15:30
T長い時刻パターン15:30:45
f長い日付と短い時刻2023年10月26日 15:30
F長い日付と長い時刻 (フル)2023年10月26日 15:30:45
g短い日付と短い時刻2023/10/26 15:30
G短い日付と長い時刻2023/10/26 15:30:45
M, m月日パターン10月26日
Y, y年月パターン2023年10月
O, oISO 8601 ラウンドトリップ形式 (DateTimeOffset推奨)2023-10-26T15:30:45.1234567+09:00
R, rRFC1123 パターン (GMT)Thu, 26 Oct 2023 15:30:45 GMT
sソート可能形式 (ISO 8601ベース)2023-10-26T15:30:45
u汎用ソート可能形式 (UTC)2023-10-26 06:30:45Z
U汎用フル形式 (UTC)2023年10月26日 6:30:45
カスタム書式指定子 (一部)
yyyy年 (4桁)2023
yy年 (2桁)23
MM月 (2桁、0埋め)10
M月 (1桁または2桁)10
dd日 (2桁、0埋め)26
d日 (1桁または2桁)26
ddd曜日 (省略形)
dddd曜日 (完全形)木曜日
HH時 (24時間形式、0埋め)15
H時 (24時間形式)15
hh時 (12時間形式、0埋め)03
h時 (12時間形式)3
mm分 (0埋め)30
m30
ss秒 (0埋め)45
s45
fffミリ秒 (3桁)123 (例)
tt午前/午後 (AM/PM)午後
Kタイムゾーン情報 (DateTimeOffset)+09:00
zzzUTCからのオフセット (時:分)+09:00

DateTime dt = new DateTime(2023, 5, 7, 8, 9, 10, 123);
DateTimeOffset dto = new DateTimeOffset(dt, TimeSpan.FromHours(9));

// 標準書式
Console.WriteLine($"\n--- 書式設定 (ToString) ---");
Console.WriteLine($"d: {dt.ToString("d")}");   // 2023/05/07
Console.WriteLine($"D: {dt.ToString("D")}");   // 2023年5月7日
Console.WriteLine($"T: {dt.ToString("T")}");   // 8:09:10
Console.WriteLine($"G: {dt.ToString("G")}");   // 2023/05/07 8:09:10
Console.WriteLine($"O (DTO): {dto.ToString("O")}"); // 2023-05-07T08:09:10.1230000+09:00
Console.WriteLine($"s: {dt.ToString("s")}");   // 2023-05-07T08:09:10

// カスタム書式
Console.WriteLine($"カスタム1: {dt.ToString("yyyy年MM月dd日 (ddd) HH:mm")}"); // 2023年05月07日 (日) 08:09
Console.WriteLine($"カスタム2: {dt:yyyyMMdd_HHmmss_fff}"); // 文字列補間でも書式指定可能: 20230507_080910_123
Console.WriteLine($"カスタム3 (DTO): {dto:yyyy/MM/dd HH:mm:ss zzz}"); // 2023/05/07 08:09:10 +09:00

// カルチャ指定
// using System.Globalization;
// CultureInfo usCulture = new CultureInfo("en-US");
// Console.WriteLine($"US (d): {dt.ToString("d", usCulture)}"); // 5/7/2023
// Console.WriteLine($"US (D): {dt.ToString("D", usCulture)}"); // Sunday, May 7, 2023

// TimeSpanの書式設定
TimeSpan ts = new TimeSpan(2, 14, 30, 5, 100); // 2日 14時間 30分 5秒 100ミリ秒
Console.WriteLine($"\n--- TimeSpan 書式設定 ---");
Console.WriteLine($"Default: {ts}"); // 2.14:30:05.1000000
Console.WriteLine($"c (定数形式): {ts:c}"); // 2.14:30:05.1000000
Console.WriteLine($"g (短い形式): {ts:g}"); // 2:14:30:05.1000000
Console.WriteLine($"G (長い形式): {ts:G}"); // 2:14:30:05.100000000
Console.WriteLine($"カスタム: {ts:d'日と'hh'時間'mm'分'}"); // 2日と14時間30分 (エスケープ文字 ' や "")
Console.WriteLine($"カスタム(Total): {(int)ts.TotalHours}時間 {ts.Minutes}分"); // 62時間 30分
            

型変換 🔄

異なるデータ型の間で値を変換する方法。明示的なキャスト、Convert クラス、Parse/TryParse メソッド、as 演算子など。

暗黙的な変換 (Implicit Conversion)

データ損失の危険がない場合に、コンパイラが自動的に行う変換。


int i = 100;
long l = i; // int から long へは暗黙的に変換可能
float f = i; // int から float へ
double d = f; // float から double へ
// double d2 = i; // int から double へも可能

Console.WriteLine($"--- 暗黙的な変換 ---");
Console.WriteLine($"int {i} -> long {l}");
Console.WriteLine($"int {i} -> float {f}");
Console.WriteLine($"float {f} -> double {d}");

// objectへの変換 (ボックス化)
object o = i;
object o2 = "hello";
Console.WriteLine($"int {i} -> object {o}");
      

明示的な変換 (Explicit Conversion / キャスト)

データ損失の可能性がある場合や、互換性のない型の間で、開発者が意図的に行う変換。キャスト演算子 (目標の型) を使用。


double d_val = 123.45;
int i_val;
long l_val = 5000000000L; // intの範囲を超える可能性のあるlong

// double から int へ (小数点以下が切り捨てられる - データ損失)
i_val = (int)d_val;
Console.WriteLine($"--- 明示的な変換 (キャスト) ---");
Console.WriteLine($"double {d_val} -> int {i_val}"); // 123

// long から int へ (値がintの範囲外だとオーバーフローまたは予期しない値になる可能性)
// チェックなし (デフォルト)
// int i_overflow = (int)l_val; // 環境や設定により結果が異なる or 例外
// Console.WriteLine($"long {l_val} -> int (unchecked) {i_overflow}");

// checked ブロック/式 でオーバーフローをチェックし、OverflowException をスローさせる
try
{
    // checked
    // {
    //     int i_checked = (int)l_val;
    //     Console.WriteLine($"long {l_val} -> int (checked) {i_checked}");
    // }
    int i_checked_expr = checked((int)l_val); // checked 式
     Console.WriteLine($"long {l_val} -> int (checked) {i_checked_expr}");
}
catch (OverflowException ex)
{
    Console.WriteLine($"オーバーフロー例外: {ex.Message}");
}

// object からの変換 (ボックス化解除) - 元の型と一致する必要がある
object obj_int = 100;
int unboxed_int = (int)obj_int;
Console.WriteLine($"object {obj_int} -> int {unboxed_int}");

object obj_str = "world";
try
{
    // int unboxed_fail = (int)obj_str; // InvalidCastException
    string unboxed_str = (string)obj_str;
    Console.WriteLine($"object \"{obj_str}\" -> string \"{unboxed_str}\"");
}
catch (InvalidCastException ex)
{
    Console.WriteLine($"不正なキャスト例外: {ex.Message}");
}
      

Convert クラス

System.Convert クラスは、様々な基本データ型の間で変換を行う静的メソッドを提供します。null の扱いや丸め処理がキャストと異なる場合があります。


string s_num = "123";
string s_bool = "true";
string s_null = null;
double d_conv = 456.78;
DateTime dt_conv = DateTime.Now;

Console.WriteLine($"\n--- Convert クラス ---");

// 文字列から数値へ
int i_conv = Convert.ToInt32(s_num);
Console.WriteLine($"string \"{s_num}\" -> int {i_conv}");
// int i_fail = Convert.ToInt32("abc"); // FormatException

// 文字列からboolへ
bool b_conv = Convert.ToBoolean(s_bool);
Console.WriteLine($"string \"{s_bool}\" -> bool {b_conv}");
// bool b_fail = Convert.ToBoolean("yes"); // FormatException ("true" or "false" のみ)

// null の変換
int? i_from_null = Convert.ToInt32(s_null); // 数値型への変換では 0 が返る
object o_from_null = Convert.ChangeType(s_null, typeof(int?)); // ChangeTypeではnullが返る (Nullableの場合)
Console.WriteLine($"Convert.ToInt32(null) -> {i_from_null}"); // 0
Console.WriteLine($"Convert.ChangeType(null, typeof(int?)) -> {(o_from_null == null ? "null" : o_from_null)}"); // null

// double から int へ (四捨五入に近い丸め - 最近接偶数丸め / Banker's rounding)
int i_from_double = Convert.ToInt32(d_conv);
int i_from_double_2 = Convert.ToInt32(123.5); // -> 124
int i_from_double_3 = Convert.ToInt32(122.5); // -> 122
Console.WriteLine($"Convert.ToInt32({d_conv}) -> {i_from_double}"); // 457 (キャストの(int)456.78 -> 456 と異なる)
Console.WriteLine($"Convert.ToInt32(123.5) -> {i_from_double_2}");
Console.WriteLine($"Convert.ToInt32(122.5) -> {i_from_double_3}");


// 他の型への変換
string s_from_dt = Convert.ToString(dt_conv); // 現在のカルチャで文字列化
Console.WriteLine($"DateTime {dt_conv} -> string \"{s_from_dt}\"");
string s_from_int = Convert.ToString(i_conv);
Console.WriteLine($"int {i_conv} -> string \"{s_from_int}\"");
// Base64エンコード/デコード
byte[] bytes = Encoding.UTF8.GetBytes("Hello Base64");
string base64String = Convert.ToBase64String(bytes);
Console.WriteLine($"\"Hello Base64\" -> Base64 \"{base64String}\"");
byte[] decodedBytes = Convert.FromBase64String(base64String);
Console.WriteLine($"Base64 \"{base64String}\" -> \"{Encoding.UTF8.GetString(decodedBytes)}\"");

// Convert.ChangeType - 実行時に型を指定して変換
object obj_val = "999";
Type targetType = typeof(int);
try
{
    object converted_obj = Convert.ChangeType(obj_val, targetType);
    Console.WriteLine($"Convert.ChangeType(\"{obj_val}\", typeof(int)) -> {converted_obj} (Type: {converted_obj.GetType()})");
}
catch (Exception ex) when (ex is InvalidCastException || ex is FormatException || ex is OverflowException)
{
     Console.WriteLine($"ChangeType エラー: {ex.Message}");
}
      

Parse / TryParse メソッド

主に文字列を数値や日付などの他の型に変換するために使用されます。各型に静的メソッドとして定義されています。

  • Parse(): 変換に失敗すると FormatException (または ArgumentNullException など) をスローします。
  • TryParse(): 変換を試み、成功したかどうかを bool で返します。変換結果は out パラメータで受け取ります。例外を発生させないため、ユーザー入力の処理などに適しています。

string str_int = "456";
string str_double = "78.90";
string str_date = "2023-10-26";
string str_invalid = "xyz";

Console.WriteLine($"\n--- Parse / TryParse ---");

// --- Parse ---
try
{
    int parsed_i = int.Parse(str_int);
    Console.WriteLine($"int.Parse(\"{str_int}\") -> {parsed_i}");

    double parsed_d = double.Parse(str_double); // カルチャ依存 ("." or ",")
    Console.WriteLine($"double.Parse(\"{str_double}\") -> {parsed_d}");

    DateTime parsed_dt = DateTime.Parse(str_date); // カルチャ依存
    Console.WriteLine($"DateTime.Parse(\"{str_date}\") -> {parsed_dt:d}");

    // int error = int.Parse(str_invalid); // FormatException
}
catch (FormatException ex)
{
    Console.WriteLine($"Parseエラー: {ex.Message}");
}
catch (ArgumentNullException ex)
{
     Console.WriteLine($"Parseエラー (null): {ex.Message}");
}

// --- TryParse ---
int result_i;
if (int.TryParse(str_int, out result_i))
{
    Console.WriteLine($"int.TryParse(\"{str_int}\") -> Success: {result_i}");
}
else
{
    Console.WriteLine($"int.TryParse(\"{str_int}\") -> Failed");
}

// 不正な文字列でTryParse
if (int.TryParse(str_invalid, out result_i))
{
     Console.WriteLine($"int.TryParse(\"{str_invalid}\") -> Success: {result_i}");
}
else
{
     Console.WriteLine($"int.TryParse(\"{str_invalid}\") -> Failed (result_i is {result_i})"); // 失敗した場合、result_iはデフォルト値(0)になる
}

// doubleのTryParse (カルチャを指定可能)
double result_d;
// using System.Globalization;
if (double.TryParse("1,234.56", NumberStyles.Any, CultureInfo.GetCultureInfo("en-US"), out result_d))
{
    Console.WriteLine($"double.TryParse(\"1,234.56\", en-US) -> Success: {result_d}");
}
if (double.TryParse("1.234,56", NumberStyles.Any, CultureInfo.GetCultureInfo("de-DE"), out result_d))
{
     Console.WriteLine($"double.TryParse(\"1.234,56\", de-DE) -> Success: {result_d}");
}

// DateTimeのTryParseExact
DateTime result_dt;
if (DateTime.TryParseExact("26/10/2023", "dd/MM/yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out result_dt))
{
    Console.WriteLine($"DateTime.TryParseExact(\"26/10/2023\") -> Success: {result_dt:yyyy-MM-dd}");
}
else
{
    Console.WriteLine($"DateTime.TryParseExact(\"26/10/2023\") -> Failed");
}
      

as 演算子

参照型または Nullable 値型の間で、互換性がある場合に安全に変換を試みます。変換できない場合は InvalidCastException をスローする代わりに null を返します。


object obj_s = "I am a string";
object obj_i = 123;
string s_as_string;
string s_as_int_string;
int? i_nullable;

Console.WriteLine($"\n--- as 演算子 ---");

// stringへの変換 (成功)
s_as_string = obj_s as string;
if (s_as_string != null)
{
    Console.WriteLine($"obj_s as string -> Success: \"{s_as_string}\"");
}
else
{
     Console.WriteLine($"obj_s as string -> Failed (Result is null)");
}

// stringへの変換 (失敗) - obj_i は int なので string には変換できない
s_as_int_string = obj_i as string;
if (s_as_int_string != null)
{
    Console.WriteLine($"obj_i as string -> Success: \"{s_as_int_string}\"");
}
else
{
     Console.WriteLine($"obj_i as string -> Failed (Result is null)"); // こちらが出力される
}

// Nullable int への変換 (成功) - obj_i は int なので int? に変換可能
i_nullable = obj_i as int?;
if (i_nullable.HasValue) // Nullable は HasValue で null チェック
{
     Console.WriteLine($"obj_i as int? -> Success: {i_nullable.Value}");
}
else
{
    Console.WriteLine($"obj_i as int? -> Failed (Result is null)");
}

// Nullable int への変換 (失敗) - obj_s は string なので int? には変換できない
i_nullable = obj_s as int?;
if (i_nullable.HasValue)
{
     Console.WriteLine($"obj_s as int? -> Success: {i_nullable.Value}");
}
else
{
    Console.WriteLine($"obj_s as int? -> Failed (Result is null)"); // こちらが出力される
}

// キャストとの比較
try
{
    // string s_cast_fail = (string)obj_i; // InvalidCastException
}
catch (InvalidCastException ex)
{
    Console.WriteLine($"(string)obj_i -> Throws InvalidCastException");
}

// 用途: if文と組み合わせて型チェックとキャストを同時に行う
if (obj_s is string str_val) // is演算子でのパターンマッチング (C# 7.0以降) もよく使われる
{
    Console.WriteLine($"obj_s is string: \"{str_val}\"");
}
// または as を使う
string str_val_as = obj_s as string;
if (str_val_as != null)
{
     Console.WriteLine($"obj_s as string (in if): \"{str_val_as}\"");
}
      

ユーザー定義の変換 (Implicit/Explicit Operators)

クラスや構造体で、特定の型への暗黙的または明示的な変換演算子を定義できます。


// 例: センチメートルを表す構造体
public readonly struct Centimeter
{
    public double Value { get; }

    public Centimeter(double value)
    {
        if (value < 0) throw new ArgumentOutOfRangeException(nameof(value));
        Value = value;
    }

    // double への明示的な変換演算子
    public static explicit operator double(Centimeter cm)
    {
        return cm.Value;
    }

    // double からの暗黙的な変換演算子
    public static implicit operator Centimeter(double value)
    {
        return new Centimeter(value);
    }

    // メートルへの明示的な変換
    public static explicit operator Meter(Centimeter cm)
    {
        return new Meter(cm.Value / 100.0);
    }

    public override string ToString() => $"{Value} cm";
}

// 例: メートルを表す構造体
public readonly struct Meter
{
     public double Value { get; }
     public Meter(double value) { Value = value; }
     public static implicit operator Meter(double value) => new Meter(value);
     public static explicit operator double(Meter m) => m.Value;
     public override string ToString() => $"{Value} m";
}


// --- ユーザー定義変換の使用 ---
Console.WriteLine($"\n--- ユーザー定義の変換 ---");

// double から Centimeter への暗黙的変換
Centimeter heightCm = 175.5; // implicit operator Centimeter(double value) が呼ばれる
Console.WriteLine($"Height: {heightCm}");

// Centimeter から double への明示的変換
double heightDouble = (double)heightCm; // explicit operator double(Centimeter cm) が呼ばれる
Console.WriteLine($"Height as double: {heightDouble}");

// Centimeter から Meter への明示的変換
Meter heightM = (Meter)heightCm; // explicit operator Meter(Centimeter cm) が呼ばれる
Console.WriteLine($"Height in Meters: {heightM}");

// double heightM_implicit = heightM; // エラー: Meterからdoubleへは明示的(explicit)のみ
double heightM_explicit = (double)heightM;
Console.WriteLine($"Height Meters as double: {heightM_explicit}");
      

デリゲートとイベント 📢

デリゲートはメソッドへの参照を保持する型であり、イベントはデリゲートを利用してクラスが通知を発行する仕組みです。

デリゲート (Delegate)

メソッドシグネチャ(戻り値の型と引数の型リスト)を定義し、そのシグネチャに一致するメソッドへの参照を保持・呼び出しできます。タイプセーフな関数ポインタのようなものです。


// デリゲート型の宣言 (メソッドシグネチャを定義)
delegate int CalculateDelegate(int x, int y);
delegate void PrintDelegate(string message);

class DelegateExample
{
    // デリゲートのシグネチャに一致するメソッド
    public int Add(int a, int b)
    {
        Console.WriteLine($"Addメソッド実行: {a} + {b}");
        return a + b;
    }

    public static int Subtract(int a, int b) // 静的メソッドも可能
    {
         Console.WriteLine($"Subtractメソッド実行: {a} - {b}");
        return a - b;
    }

    public void PrintMessage(string msg)
    {
        Console.WriteLine($"メッセージ: {msg}");
    }

    public void Run()
    {
        Console.WriteLine("--- デリゲートの基本 ---");
        // デリゲートのインスタンス化とメソッドの代入
        CalculateDelegate calc1 = new CalculateDelegate(Add); // new は省略可能
        CalculateDelegate calc2 = Subtract; // 静的メソッドも直接代入可能
        PrintDelegate print = PrintMessage;

        // デリゲート経由でのメソッド呼び出し
        int result1 = calc1(10, 5); // Add(10, 5) が呼ばれる
        Console.WriteLine($"結果1: {result1}"); // 15

        int result2 = calc2(10, 5); // Subtract(10, 5) が呼ばれる
        Console.WriteLine($"結果2: {result2}"); // 5

        print("こんにちは、デリゲート!"); // PrintMessage("...") が呼ばれる

        Console.WriteLine("\n--- マルチキャストデリゲート ---");
        // デリゲートに複数のメソッドを登録 (マルチキャスト)
        CalculateDelegate multiCalc = calc1;
        multiCalc += Subtract; // Subtractメソッドを追加 (+= 演算子)
        multiCalc += Add;      // Addメソッドをさらに追加

        // マルチキャストデリゲートを呼び出すと、登録されたメソッドが順番に実行される
        // 戻り値がある場合、最後に実行されたメソッドの戻り値が返される
        int multiResult = multiCalc(20, 7);
        Console.WriteLine($"マルチキャスト結果 (最後の戻り値): {multiResult}"); // 27 (最後のAddの結果)

        // メソッドの登録解除 (-= 演算子)
        multiCalc -= Add; // 最初に登録したAddを解除 (最後に追加したものも解除される可能性あり)
        Console.WriteLine("\nAddを一つ解除後:");
        multiResult = multiCalc(8, 3); // Subtract(8, 3) -> Add(8, 3) が呼ばれるはず (実装による)
        Console.WriteLine($"解除後の結果: {multiResult}"); // 11 または 5 (実装による)

        // nullチェック
        CalculateDelegate nullDelegate = null;
        // nullDelegate(1, 1); // NullReferenceException
        nullDelegate?.Invoke(1, 1); // null条件演算子で安全に呼び出し
    }
}

// 実行
var de = new DelegateExample();
de.Run();
            

Func, Action, Predicate デリゲート

System 名前空間には、よく使われるシグネチャのジェネリックデリゲートが事前定義されています。これらを使うことで、独自のデリゲート型を宣言する手間を省けます。

  • Func<..., TResult>: 結果 (TResult) を返すメソッドを参照します。最大16個の入力引数を取れます (Func<T1, ..., T16, TResult>)。
  • Action<...>: 結果を返さない (void) メソッドを参照します。最大16個の入力引数を取れます (Action<T1, ..., T16>)。
  • Predicate<T>: 1つの引数を受け取り、bool 値を返すメソッドを参照します。主に条件判定に使用されます (Func<T, bool> と同等)。

// Func の例: int, int を受け取り int を返す (CalculateDelegateの代わり)
Func<int, int, int> addFunc = (a, b) => a + b; // ラムダ式でメソッドを直接定義
Func<string, int> getLengthFunc = s => s.Length;

int sumFunc = addFunc(5, 8);
int lenFunc = getLengthFunc("Hello Func");
Console.WriteLine($"\n--- Func, Action, Predicate ---");
Console.WriteLine($"Func 結果 (sum): {sumFunc}"); // 13
Console.WriteLine($"Func 結果 (length): {lenFunc}"); // 10

// Action の例: string を受け取り void を返す (PrintDelegateの代わり)
Action<string> printAction = msg => Console.WriteLine($"Action Message: {msg}");
Action<int, int> printSumAction = (x, y) => Console.WriteLine($"Action Sum: {x + y}");

printAction("こんにちは、Action!");
printSumAction(10, 20);

// Action (引数なし)
Action greetAction = () => Console.WriteLine("Action: Hello!");
greetAction();

// Predicate の例: int を受け取り bool を返す
Predicate<int> isEvenPredicate = n => n % 2 == 0;
// Func isEvenFunc = n => n % 2 == 0; // Funcでも同じことができる

bool check1 = isEvenPredicate(10); // true
bool check2 = isEvenPredicate(7);  // false
Console.WriteLine($"Predicate (10 is even?): {check1}");
Console.WriteLine($"Predicate (7 is even?): {check2}");

// LINQ でよく使われる
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
List<int> evenNumbers = numbers.FindAll(isEvenPredicate); // FindAllはPredicateを引数に取る
Console.WriteLine("偶数:");
evenNumbers.ForEach(n => Console.Write($"{n} ")); // ForEachはActionを引数に取る
Console.WriteLine();
            

ラムダ式 (Lambda Expressions)

デリゲートや式ツリーを作成するための簡潔な構文。匿名メソッドの代替としてよく使われます。

(入力パラメータ) => 式またはステートメントブロック


Console.WriteLine("\n--- ラムダ式 ---");

// パラメータなし、式形式
Func<string> getGreeting = () => "Hello Lambda!";
Console.WriteLine(getGreeting());

// パラメータ1つ、式形式 (括弧は省略可能)
Func<int, int> square = x => x * x;
// Func square = (x) => x * x; // 括弧ありでもOK
Console.WriteLine($"Square(5): {square(5)}"); // 25

// パラメータ2つ、式形式
Func<int, int, int> multiply = (x, y) => x * y;
Console.WriteLine($"Multiply(4, 6): {multiply(4, 6)}"); // 24

// パラメータあり、ステートメントブロック形式 (複数の文を含む場合)
Action<string> processString = name =>
{
    string upperName = name.ToUpper();
    Console.WriteLine($"Original: {name}, Upper: {upperName}");
};
processString("Alice");

// LINQ でのラムダ式の活用
List<string> names = new List<string> { "Bob", "Charlie", "Anna", "David" };
var namesStartingWithA = names.Where(n => n.StartsWith("A")); // Whereメソッドにラムダ式を渡す
Console.WriteLine("Aで始まる名前:");
foreach(var name in namesStartingWithA) Console.WriteLine(name); // Anna

var nameLengths = names.Select(n => new { Name = n, Length = n.Length }); // Selectで匿名型に変換
Console.WriteLine("名前と長さ:");
foreach(var item in nameLengths) Console.WriteLine($"- {item.Name} ({item.Length})");

// ラムダ式を変数に代入せず直接使う
names.ForEach(n => Console.WriteLine($"Name: {n}"));
            

イベント (Event)

クラスの状態変化や特定のアクションが発生したことを、他のオブジェクト(購読者、サブスクライバ)に通知するための仕組み。オブザーバーパターンの実装。event キーワードを使用し、デリゲート型に基づいて宣言されます。

  • イベントを発行するクラス (パブリッシャ):
    • event キーワードを使ってイベントを宣言 (通常は public)。デリゲート型を指定。
    • イベントを発生させるメソッド (通常は protected virtual) を持つ。
    • イベントハンドラが登録されているか確認 (!= null) してからイベントを発生させる。
  • イベントを購読するクラス (サブスクライバ):
    • イベント発行元のインスタンスを取得する。
    • イベントに対応するメソッド(イベントハンドラ)を定義する。シグネチャはイベントのデリゲート型と一致する必要がある (通常 (object sender, EventArgs e) の形式)。
    • += 演算子を使ってイベントハンドラをイベントに登録する。
    • 不要になったら -= 演算子を使って登録を解除する(メモリリーク防止)。
  • EventArgs: イベントに関する追加情報がない場合に使用。カスタムデータが必要な場合は EventArgs を継承したクラスを作成する。

// --- イベント発行クラス (Publisher) ---
// イベントデータを渡すためのクラス (EventArgsを継承)
public class ThresholdReachedEventArgs : EventArgs
{
    public int Threshold { get; }
    public DateTime TimeReached { get; }

    public ThresholdReachedEventArgs(int threshold, DateTime timeReached)
    {
        Threshold = threshold;
        TimeReached = timeReached;
    }
}

public class Counter
{
    private int _count;
    private int _threshold;

    // 1. イベントの宣言 (デリゲート型を指定)
    // EventHandler は .NET で推奨される汎用イベントハンドラデリゲート
    // public delegate void ThresholdReachedEventHandler(object sender, ThresholdReachedEventArgs e);
    // public event ThresholdReachedEventHandler ThresholdReached;
    // 上記の代わりに EventHandler を使うのが一般的
    public event EventHandler<ThresholdReachedEventArgs> ThresholdReached;

    public Counter(int threshold)
    {
        _threshold = threshold;
    }

    public void Increment()
    {
        _count++;
        Console.WriteLine($"Count incremented to: {_count}");

        // 閾値に達したらイベントを発生させる
        if (_count >= _threshold)
        {
            // 3. イベント発生用メソッド (protected virtualが一般的)
            OnThresholdReached(new ThresholdReachedEventArgs(_threshold, DateTime.Now));
        }
    }

    // 4. イベント発生メソッドの実装
    protected virtual void OnThresholdReached(ThresholdReachedEventArgs e)
    {
        // 5. イベントハンドラが登録されているか確認し、イベントを発生させる
        // EventHandler handler = ThresholdReached;
        // handler?.Invoke(this, e);
        // または直接
        ThresholdReached?.Invoke(this, e);
    }
}

// --- イベント購読クラス (Subscriber) ---
public class EventListener
{
    // 6. イベントハンドラメソッド (デリゲートのシグネチャに合わせる)
    public void OnCounterThresholdReached(object sender, ThresholdReachedEventArgs e)
    {
        Console.ForegroundColor = ConsoleColor.Green;
        Console.WriteLine("イベント受信!");
        Console.WriteLine($"送信元: {sender?.GetType().Name ?? "null"}");
        Console.WriteLine($"閾値 ({e.Threshold}) に達しました。時刻: {e.TimeReached:HH:mm:ss}");
        Console.ResetColor();

        // イベント送信元オブジェクトにアクセスも可能
        if (sender is Counter c)
        {
           // c.Reset(); // 例: カウンターをリセットするなど
        }
    }
}

// --- イベントの使用 ---
Console.WriteLine("\n--- イベント ---");
Counter counter = new Counter(5); // 閾値5のカウンター
EventListener listener = new EventListener();

// 7. イベントハンドラの登録 (+=)
counter.ThresholdReached += listener.OnCounterThresholdReached;
// ラムダ式や匿名メソッドも登録可能
counter.ThresholdReached += (sender, e) => {
    Console.ForegroundColor = ConsoleColor.Yellow;
    Console.WriteLine("ラムダ式ハンドラ: 閾値到達!");
    Console.ResetColor();
};


// カウンターをインクリメントしてイベント発生をトリガー
counter.Increment(); // 1
counter.Increment(); // 2
counter.Increment(); // 3
counter.Increment(); // 4
counter.Increment(); // 5 (ここでイベントが発生し、登録されたハンドラが呼ばれる)
counter.Increment(); // 6 (閾値以上なので再度イベント発生)

// 8. イベントハンドラの登録解除 (-=) (不要になったら行う)
counter.ThresholdReached -= listener.OnCounterThresholdReached;
Console.WriteLine("\nListenerのハンドラを解除後:");
counter.Increment(); // 7 (ラムダ式ハンドラのみ呼ばれる)

            

ジェネリクス (Generics) 🧬

型をパラメータ化することで、特定の型に依存しない、再利用可能でタイプセーフなクラス、構造体、インターフェース、メソッド、デリゲートを作成するための機能。

ジェネリッククラス・構造体・インターフェース

型パラメータ <T> を使って定義します。T はプレースホルダであり、使用時に具体的な型を指定します。


// ジェネリッククラスの例: 任意の型のデータを保持するコンテナ
public class DataContainer<T> // T は型パラメータ
{
    private T _data;

    public DataContainer(T initialData)
    {
        _data = initialData;
    }

    public T GetData()
    {
        return _data;
    }

    public void SetData(T newData)
    {
        _data = newData;
    }

    public void DisplayData()
    {
        Console.WriteLine($"データ ({typeof(T).Name}): {_data}");
    }
}

// ジェネリック構造体の例 (クラスと同様)
public struct Point<T>
{
    public T X { get; set; }
    public T Y { get; set; }
}

// ジェネリックインターフェースの例
public interface IRepository<TEntity> where TEntity : class // 型制約の例
{
    TEntity GetById(int id);
    IEnumerable<TEntity> GetAll();
    void Add(TEntity entity);
    void Update(TEntity entity);
    void Delete(int id);
}


// --- ジェネリック型の使用 ---
Console.WriteLine("--- ジェネリクス ---");

// 具体的な型を指定してインスタンス化
DataContainer<int> intContainer = new DataContainer<int>(123);
DataContainer<string> stringContainer = new DataContainer<string>("Hello Generics");
DataContainer<DateTime> dateContainer = new DataContainer<DateTime>(DateTime.Now);
// varキーワードでも推論可能 (new の後で型指定が必要)
var boolContainer = new DataContainer<bool>(true);

// メソッド呼び出し (タイプセーフ)
int intData = intContainer.GetData();
string stringData = stringContainer.GetData();
// int stringToInt = stringContainer.GetData(); // コンパイルエラー (型安全)

intContainer.DisplayData();    // データ (Int32): 123
stringContainer.DisplayData(); // データ (String): Hello Generics
dateContainer.DisplayData();   // データ (DateTime): ...
boolContainer.DisplayData();   // データ (Boolean): True

// ジェネリック構造体の使用
Point<int> p1 = new Point<int> { X = 10, Y = 20 };
Point<double> p2 = new Point<double> { X = 1.5, Y = 3.7 };
Console.WriteLine($"Point: ({p1.X}, {p1.Y})");
Console.WriteLine($"Point: ({p2.X}, {p2.Y})");

// ジェネリックインターフェースの実装例 (仮)
public class Product { public int Id { get; set; } public string Name { get; set; } }
public class ProductRepository : IRepository<Product> // Product型で実装
{
    // ... IRepository のメソッドを実装 ...
    public Product GetById(int id) { return new Product { Id = id, Name = "Sample" }; }
    public IEnumerable<Product> GetAll() { return new List<Product> { GetById(1) }; }
    public void Add(Product entity) { /* ... */ }
    public void Update(Product entity) { /* ... */ }
    public void Delete(int id) { /* ... */ }
}

IRepository<Product> repo = new ProductRepository();
Product p = repo.GetById(1);
Console.WriteLine($"Repository GetById(1): {p.Name}");
            

ジェネリックメソッド

メソッド自身のシグネチャに型パラメータを持ちます。クラスがジェネリックでなくても、メソッドだけジェネリックにできます。


class GenericMethodExample
{
    // ジェネリックメソッドの定義
    public void Swap<T>(ref T a, ref T b) // メソッド名の後に  を記述
    {
        Console.WriteLine($"Swap<{typeof(T).Name}>: 元の値 a={a}, b={b}");
        T temp = a;
        a = b;
        b = temp;
         Console.WriteLine($"Swap<{typeof(T).Name}>: 交換後 a={a}, b={b}");
    }

    // 戻り値を持つジェネリックメソッド
    public T GetFirstElement<T>(IEnumerable<T> collection)
    {
        if (collection == null || !collection.Any())
        {
            // throw new ArgumentException("Collection is null or empty.");
            return default(T); // 型Tのデフォルト値を返す (参照型ならnull, intなら0, boolならfalseなど)
        }
        return collection.First();
    }
}

// --- ジェネリックメソッドの使用 ---
Console.WriteLine("\n--- ジェネリックメソッド ---");
var gm = new GenericMethodExample();

// int 型で Swap を呼び出し
int x = 5, y = 10;
gm.Swap<int>(ref x, ref y); // 型引数を明示的に指定
// 型引数は多くの場合、引数から推論されるため省略可能
// gm.Swap(ref x, ref y); // これでもOK

// string 型で Swap を呼び出し
string s1 = "abc", s2 = "xyz";
gm.Swap(ref s1, ref s2); // 型推論で Swap が呼ばれる

// GetFirstElement の呼び出し
List<double> doubles = new List<double> { 3.14, 2.71, 1.61 };
double firstDouble = gm.GetFirstElement(doubles); // 型推論で GetFirstElement
Console.WriteLine($"最初の double 要素: {firstDouble}");

string[] strings = { "one", "two", "three" };
string firstString = gm.GetFirstElement(strings);
Console.WriteLine($"最初の string 要素: {firstString}");

// 空のコレクションで default(T) が返る例
List<int> emptyInts = new List<int>();
int firstIntOrDefault = gm.GetFirstElement(emptyInts);
Console.WriteLine($"空リストの最初の int 要素 (default): {firstIntOrDefault}"); // 0

List<string> emptyStrings = null;
string firstStringOrDefault = gm.GetFirstElement(emptyStrings);
Console.WriteLine($"nullリストの最初の string 要素 (default): {(firstStringOrDefault == null ? "null" : firstStringOrDefault)}"); // null
            

型制約 (Type Constraints)

ジェネリック型パラメータ T が満たすべき要件を指定します。これにより、ジェネリックコード内で T の特定のメンバー(メソッド、プロパティ)にアクセスしたり、特定の特性(クラスである、コンストラクタを持つなど)を保証したりできます。where T : 制約 の形式で記述します。

制約 説明
where T : struct T は Nullable でない値型である必要があります。
where T : class T は参照型 (クラス, インターフェース, デリゲート, 配列) である必要があります。
where T : new() T は public な引数なしコンストラクタを持つ必要があります。他の制約と組み合わせる場合は最後に記述します。
where T : <基本クラス名> T は指定された基本クラス、またはその派生クラスである必要があります。
where T : <インターフェース名> T は指定されたインターフェースを実装している必要があります。
where T : U T は別の型パラメータ U から派生しているか、U と同じ型である必要があります。
where T : unmanaged (C# 7.3以降) T はポインタ型を含まないアンマネージド型である必要があります (struct制約も暗黙的に含む)。
where T : notnull (C# 8.0以降) T は Nullable でない型 (値型または Nullable でない参照型) である必要があります。
where T : default (C# 11以降) 型パラメータがstruct制約を満たさないことを許可します。主にnew()制約の緩和に使われます。

// 型制約の例

// T は IDisposable を実装し、引数なしコンストラクタを持つクラスである必要がある
public class ResourceManager<T> where T : class, IDisposable, new()
{
    public T GetResource()
    {
        T resource = new T(); // new() 制約があるのでインスタンス化できる
        Console.WriteLine($"{typeof(T).Name} のリソースを取得しました。");
        return resource;
    }

    public void ReleaseResource(T resource)
    {
        Console.WriteLine($"{typeof(T).Name} のリソースを解放します。");
        resource.Dispose(); // IDisposable 制約があるので Dispose() を呼べる
    }
}

// T は IComparable を実装している必要がある
public static T Max<T>(T a, T b) where T : IComparable<T>
{
    // IComparable 制約があるので CompareTo メソッドを呼べる
    return a.CompareTo(b) > 0 ? a : b;
}

// T は Animal またはその派生クラスである必要がある
public class Zoo<T> where T : Animal // Animalは前のセクションで定義したクラス
{
    private List<T> animals = new List<T>();
    public void AddAnimal(T animal)
    {
        animals.Add(animal);
        Console.WriteLine($"{animal.Name} ({typeof(T).Name}) を追加しました。");
        animal.Speak(); // Animalクラスのメンバーにアクセス可能
    }
}

// --- 型制約の使用 ---
Console.WriteLine("\n--- 型制約 ---");

// ResourceManager の使用 (StreamWriterは条件を満たす)
// var rm = new ResourceManager(); // StreamWriterには引数なしコンストラクタがないためエラー
public class MyResource : IDisposable { public MyResource() {} public void Dispose() { Console.WriteLine("MyResource Disposed"); } }
var rm = new ResourceManager<MyResource>();
MyResource res = rm.GetResource();
rm.ReleaseResource(res);

// Max メソッドの使用
int maxInt = Max(10, 5); // int は IComparable を実装
string maxString = Max("apple", "orange"); // string は IComparable を実装
Console.WriteLine($"Max(10, 5): {maxInt}");
Console.WriteLine($"Max(\"apple\", \"orange\"): {maxString}");
// DateTime maxDate = Max(DateTime.Now, DateTime.UtcNow); // DateTimeもOK

// Zoo の使用
var zoo = new Zoo<Dog>(); // Dog は Animal の派生クラスなのでOK
zoo.AddAnimal(new Dog("ポチ", "柴犬"));
// var zoo2 = new Zoo(); // エラー: stringはAnimalではない

            

ジェネリクスと継承

  • ジェネリッククラスも継承できます。
  • 派生クラスは、基底クラスの型パラメータを具体的に指定するか、自身の型パラメータとして引き継ぐことができます。

// 基底ジェネリッククラス
public class BaseCollection<T>
{
    protected List<T> items = new List<T>();
    public void Add(T item) => items.Add(item);
    public int Count => items.Count;
}

// 1. 型パラメータを具体的に指定して継承
public class StringCollection : BaseCollection<string> // T を string に固定
{
    public void PrintAll()
    {
        Console.WriteLine("--- StringCollection ---");
        foreach (var item in items) // 基底クラスのprotectedメンバーにアクセス可能
        {
            Console.WriteLine(item.ToUpper()); // string固有のメソッドも使える
        }
    }
}

// 2. 型パラメータを引き継いで継承
public class SortedCollection<T> : BaseCollection<T> where T : IComparable<T>
{
    public new void Add(T item) // 基底クラスのAddを隠蔽 (new キーワード)
    {
        base.Add(item);
        items.Sort(); // IComparable 制約があるので Sort 可能
    }
    public IEnumerable<T> GetItems() => items;
}

// --- 継承したジェネリッククラスの使用 ---
Console.WriteLine("\n--- ジェネリクスと継承 ---");

StringCollection sc = new StringCollection();
sc.Add("apple");
sc.Add("Banana");
sc.PrintAll();
Console.WriteLine($"Count: {sc.Count}");

SortedCollection<int> sortedInts = new SortedCollection<int>();
sortedInts.Add(5);
sortedInts.Add(1);
sortedInts.Add(3);
Console.WriteLine("--- SortedCollection ---");
foreach (var item in sortedInts.GetItems()) Console.Write($"{item} "); // 1 3 5
Console.WriteLine();
            

ジェネリクスの利点

  • タイプセーフティ: コンパイル時に型チェックが行われ、実行時エラー(InvalidCastExceptionなど)のリスクを減らします。
  • パフォーマンス: 値型を扱う際にボックス化・ボックス化解除が発生しないため、ArrayList などの非ジェネリックコレクションよりもパフォーマンスが良い場合があります。
  • コードの再利用性: 同じロジックを異なる型に対して再利用できるため、コードの重複を減らせます。
  • コードの明確化: コードが扱う型が明確になり、可読性が向上します。

コメント

タイトルとURLをコピーしました