Skip to content

C# チートシート

.NET、Web、ゲーム向けのモダンなオブジェクト指向言語。

01

はじめに

Hello World

C# プログラムは Main() から開始されます。using System; は System 名前空間(Console、Math など)をインポートします。C# 9 以降はトップレベルステートメントをサポートしており、Console.WriteLine("Hello"); だけのファイルでも有効なプログラムになります。コンパイラがクラスと Main を自動的に生成します。

csharp
using System;

class Program {
    static void Main() {
        Console.WriteLine("Hello, World!");
    }
}

変数と型

C# は静的型付けです。var を使うとコンパイラが型を推論します(コンパイル時の型安全性は維持されます)。decimal(m サフィックス)は正確な精度が必要な金融計算用です。const はコンパイル時定数、readonly はコンストラクタで設定される実行時定数です。

csharp
int age = 30;
double pi = 3.14159;
decimal price = 19.99m;
char grade = 'A';
bool isDev = true;
string name = "Alice";
var x = 42;  // implicitly typed (int)
const double TAX = 0.08;

文字列補間

文字列補間($"...")は中括弧内に式を埋め込みます。: の後の書式指定子が出力を制御します(F2 は小数点以下2桁、yyyy-MM-dd は日付)。逐語的文字列(@"")はバックスラッシュをそのまま扱います—ファイルパスや正規表現に便利です。両方を組み合わせて $@"..." も可能です。

csharp
string name = "Alice";
int age = 30;
Console.WriteLine($"Name: {name}, Age: {age}");

// Formatting
double pi = 3.14159;
Console.WriteLine($"Pi: {pi:F2}");  // Pi: 3.14
Console.WriteLine($"Date: {DateTime.Now:yyyy-MM-dd}");

// Verbatim strings (@"") preserve backslashes
string path = @"C:\Users\name";

入出力

Console.ReadLine() は1行全体を文字列として読み取ります。int.Parse は変換しますが無効な入力で例外をスローします。int.TryParse はより安全で、bool を返し out パラメータを使用します。ユーザー入力には常に TryParse を使用し、不正なデータによる例外を避けてください。

csharp
Console.Write("Enter name: ");
string name = Console.ReadLine();
Console.Write("Enter age: ");
int age = int.Parse(Console.ReadLine());

Console.WriteLine($"Hi {name}, you are {age}");

// Safe parsing with TryParse
if (int.TryParse(Console.ReadLine(), out int num)) {
    Console.WriteLine($"Valid: {num}");
} else {
    Console.WriteLine("Invalid number");
}

Null許容型

int? は null 許容値型(Nullable<int>)です。?. 演算子(null 条件)はメンバーに安全にアクセスし—スローせずに null を返します。?? 演算子(null 合体)は null のデフォルト値を提供します。C# 8 以降の null 許容参照型は、コンパイル時に潜在的な null を警告します。

csharp
int? maybeAge = null;  // nullable int
if (maybeAge.HasValue) {
    Console.WriteLine(maybeAge.Value);
}

string? maybeName = null;  // nullable reference (C# 8+)
int length = maybeName?.Length ?? 0;  // null-conditional + null-coalescing

// ValueOrDefault
int age = maybeAge ?? 0;  // 0 if null
02

文字列

文字列メソッド

C# の文字列は不変です—メソッドは元の文字列を変更せず新しい文字列を返します。一般的なメソッド:Length、ToUpper/ToLower、Substring、IndexOf、Replace、Contains、Split。頻繁な文字列操作には StringBuilder を使用し、多数の中間文字列の生成を避けてください。

csharp
string s = "Hello, World!";
Console.WriteLine(s.Length);        // 13
Console.WriteLine(s.ToUpper());     // HELLO, WORLD!
Console.WriteLine(s.Substring(0, 5)); // Hello
Console.WriteLine(s.IndexOf("World")); // 7
Console.WriteLine(s.Replace("World", "C#"));
Console.WriteLine(s.Contains("Hello")); // True
Console.WriteLine(s.Split(", "));   // ["Hello", "World!"]

StringBuilder

StringBuilder は可変で、ループ内での文字列構築に効率的です。文字列連結(+)は毎回新しい文字列を作成するため、繰り返し連結は O(n^2) になります。StringBuilder は償却で O(n) です。数回以上の連結がある場合は StringBuilder を使用してください。

csharp
using System.Text;
var sb = new StringBuilder();
for (int i = 0; i < 100; i++) {
    sb.Append("Line ").Append(i).Append("\n");
}
string result = sb.ToString();

// vs string concatenation (creates many temporary strings)
// string result = "";
// for (int i = 0; i < 100; i++) result += "Line " + i;

書式設定と解析

string.Format は {0}、{1} プレースホルダーを使用します。書式指定子:F(固定小数点)、N(桁区切り付き数値)、P(パーセント)、X(16進数)。解析はカルチャに依存します—小数点区切り記号はロケールによって異なります。明示的に制御するには CultureInfo を、安全に解析するには TryParse を使用してください。

csharp
// String formatting
string s1 = string.Format("{0} is {1} years old", "Alice", 30);
string s2 = $"{3.14159:F2}";  // "3.14"
string s3 = $"{1234567:N0}";  // "1,234,567" (thousands separator)

// Parsing
int n = int.Parse("42");
double d = double.Parse("3.14");
bool b = bool.Parse("true");

// Culture-aware parsing
double euro = double.Parse("3,14",
    System.Globalization.CultureInfo.GetCultureInfo("de-DE"));

文字列比較

大文字小文字を区別しない比較には StringComparison.OrdinalIgnoreCase を使用してください—ToLower() してから == するより明確で高速です。カルチャに依存する比較に == は避け、CultureInfo を指定した string.Compare を使用してください。ディクショナリのキーには StringComparer.OrdinalIgnoreCase を使用してください。

csharp
string a = "Hello", b = "hello";

// Case-sensitive
bool equal = a == b;  // false
bool equal2 = a.Equals(b);  // false

// Case-insensitive
bool equalCI = a.Equals(b, StringComparison.OrdinalIgnoreCase);  // true
int cmp = string.Compare(a, b, StringComparison.OrdinalIgnoreCase);

// Best practice for comparison
bool same = string.Equals(a, b,
    StringComparison.OrdinalIgnoreCase);

正規表現

Regex(System.Text.RegularExpressions)はパターンマッチングを提供します。@"" 逐語的文字列を使用するとバックスラッシュの二重エスケープが不要になります。繰り返し使用する場合は、Regex インスタンスを一度コンパイルして再利用(RegexOptions.Compiled)するとパフォーマンスが向上します。

csharp
using System.Text.RegularExpressions;

string text = "Phone: 123-456-7890";
var match = Regex.Match(text, @"\d{3}-\d{3}-\d{4}");
if (match.Success) Console.WriteLine(match.Value);  // 123-456-7890

// Replace
string clean = Regex.Replace(text, @"\d", "X");
// "Phone: XXX-XXX-XXXX"

// Extract all matches
var emails = Regex.Matches("[email protected], [email protected]", @"\S+@\S+");
03

数値と数学

数値型

C# には固定サイズの型があります:int(32ビット)、long(64ビット)、double(64ビット浮動小数点)、decimal(128ビット、金融用)。アンダースコア(9_000_000)で可読性が向上します。金額には decimal を使用してください—浮動小数点の丸め誤差を回避できます。サフィックス:L(long)、f(float)、m(decimal)。

csharp
int i = 42;
long big = 9_000_000_000L;  // underscores for readability
double d = 3.14159265;
float f = 3.14f;
decimal price = 19.99m;     // exact decimal for money
byte b = 255;

Console.WriteLine(sizeof(int));   // 4
Console.WriteLine(int.MaxValue);  // 2147483647

Math クラス

Math は一般的な操作の静的メソッドを提供します。Math.Round はデフォルトで銀行家丸め(偶数に丸める)を使用します—通常の丸めには MidpointRounding.AwayFromZero を使用してください。Math.BigMul はオーバーフローを避けるため long の乗算を処理します。

csharp
double x = 2.5;
Math.Pow(x, 3);     // 15.625
Math.Sqrt(x);       // 1.581
Math.Abs(-5);       // 5
Math.Floor(3.7);    // 3
Math.Ceiling(3.2);  // 4
Math.Round(3.5);    // 4 (banker's rounding)
Math.Max(3, 7);     // 7
Math.Min(3, 7);     // 3
Math.PI;            // 3.14159...

乱数

Random は疑似乱数を生成します。1つのインスタンスを作成して再利用してください(ループ内で多数作成すると時間ベースのシードにより重複が発生する可能性があります)。暗号論的な乱数には System.Security.Cryptography.RandomNumberGenerator を使用してください。C# 8 以降はスレッドセーフな Random.Shared が提供されています。

csharp
var random = new Random();
int r = random.Next(1, 101);     // 1-100
double d = random.NextDouble();   // 0.0-1.0
byte[] bytes = new byte[4];
random.NextBytes(bytes);

// C# 8+: thread-safe
// var rng = Random.Shared;
// int n = rng.Next(1, 101);

// Array of random items
int[] nums = Enumerable.Range(0, 5)
    .Select(_ => random.Next(1, 100)).ToArray();

型変換

データ損失がない場合、暗黙的な変換が自動的に行われます(int から double)。精度が失われる可能性がある場合は明示的キャスト(型)が必要です。Convert.ToInt32 は多くの型を処理し丸めを行います(切り捨てるキャストとは異なります)。例外を避けるため、文字列から数値への変換には常に TryParse を優先してください。

csharp
// Implicit (safe, no data loss)
int i = 42;
double d = i;  // int -> double

// Explicit cast (may lose data)
double pi = 3.14;
int truncated = (int)pi;  // 3

// Convert class
string s = "42";
int n = Convert.ToInt32(s);
string str = Convert.ToString(42);

// Parse / TryParse
int parsed = int.Parse("100");
bool ok = int.TryParse("abc", out int result);  // false

整数オーバーフローと checked

デフォルトでは整数オーバーフローは暗黙にラップアラウンドします(unchecked)。checked ブロックはオーバーフロー時に OverflowException をスローします—安全が重要なコードに使用してください。非常に大きな数値には BigInteger(System.Numerics)がオーバーフローなしで任意精度の整数を処理します。

csharp
int a = int.MaxValue;  // 2147483647
// a++;  // overflow: wraps to -2147483648 (unchecked by default)

checked {
    a++;  // throws OverflowException!
}

// BigInteger for arbitrary precision
using System.Numerics;
var big = BigInteger.Pow(2, 100);
Console.WriteLine(big);  // 1267650...376 (31 digits)
04

制御フロー

If / Else

if/else if/else は標準的です。三項演算子(condition ? a : b)は簡潔な if/else 式です。C# はブール条件を要求します—C とは異なり整数は暗黙的に bool に変換されません。保守バグを防ぐため、単一文でも中括弧を使用してください。

csharp
int score = 85;
if (score >= 90) {
    Console.WriteLine("A");
} else if (score >= 80) {
    Console.WriteLine("B");
} else {
    Console.WriteLine("C");
}

// Ternary operator
string grade = score >= 60 ? "Pass" : "Fail";

Switch とパターンマッチング

C# 8 以降の switch 式(=>)は簡潔で値を返します。_ パターンはデフォルトケースです。パターンマッチング(is、switch)は型パターン、プロパティパターン、関係パターン(> 18)をサポートします。これは従来の switch より強力でボイラープレートを削減します。

csharp
// Classic switch
int day = 3;
string name = day switch {
    1 => "Mon", 2 => "Tue", 3 => "Wed",
    6 or 7 => "Weekend",
    _ => "Invalid"  // default
};

// Pattern matching with types
object obj = "hello";
if (obj is string s && s.Length > 3) {
    Console.WriteLine(s);  // "hello"
}

// Property patterns
if (person is { Age: > 18, Name: "Alice" }) {
    // matches if Age > 18 and Name == "Alice"
}

ループ

for はカウント付き反復用、foreach はコレクション(配列、リスト、IEnumerable)を反復、while は条件が false になるまでループします。foreach は読み取り専用です—反復中にコレクションを変更できません。インデックスが必要な場合や要素を変更する必要がある場合は for を使用してください。

csharp
// for loop
for (int i = 0; i < 5; i++) {
    Console.WriteLine(i);
}

// foreach
string[] fruits = { "apple", "banana", "cherry" };
foreach (string fruit in fruits) {
    Console.WriteLine(fruit);
}

// while
int n = 5;
while (n > 0) Console.WriteLine(n--);

Break、Continue、goto

break は最も内側のループまたは switch を終了し、continue は次の反復にスキップします。C# にはネストされたループ用のラベル付き break はありません—フラグを使用するか、メソッドに抽出して return するか、LINQ を使用してください。goto は switch のフォールスルーで有効ですが、それ以外では推奨されません。

csharp
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    Console.WriteLine(i);  // 0 1 2 4 5 6
}

// goto in switch (rare but valid)
switch (code) {
    case 1:
        DoSomething();
        goto case 2;  // fall through
    case 2:
        DoMore();
        break;
}

イテレータと yield

yield return はメソッドをイテレータに変換し、値を遅延生成します—要求に応じて一度に1つずつ生成します。これは大きなシーケンスや無限シーケンスにメモリ効率が良いです。コンパイラが状態機械を生成します。yield break は反復を早期終了します。LINQ はイテレータを広範に使用します。

csharp
// yield return creates an iterator (lazy evaluation)
IEnumerable<int> GetNumbers() {
    for (int i = 0; i < 5; i++) {
        yield return i;
    }
}

foreach (int n in GetNumbers()) {
    Console.WriteLine(n);  // 0 1 2 3 4
}

// Infinite sequence (consumed lazily)
IEnumerable<int> Naturals() {
    int n = 0;
    while (true) yield return n++;
}
05

メソッドとデリゲート

メソッド定義

メソッドは式形式(=>)で1行で記述できます。デフォルトパラメータで引数をオプションにできます。名前付き引数(name:)はパラメータが多い呼び出しの可読性を向上させ、オプション引数をスキップできます。C# は戻り値型のみが異なるメソッドのオーバーロードをサポートしません。

csharp
static int Add(int a, int b) {
    return a + b;
}

// Expression-bodied (C# 6+)
static int Multiply(int a, int b) => a * b;

// Default parameters
static void Greet(string name = "Guest", int times = 1) {
    for (int i = 0; i < times; i++)
        Console.WriteLine($"Hi {name}!");
}

Greet();              // Hi Guest!
Greet("Alice", 3);    // Hi Alice! x3
Greet(times: 2);      // named argument

out、ref、params

out パラメータはメソッドで割り当てる必要があります(呼び出し側は初期化不要)。ref パラメータは呼び出し側が初期化し、変更可能です。params は可変数の引数を許可します。C# 7 以降はインラインで out var 宣言が可能です:TryParse(s, out var result)。

csharp
// out: must be assigned in method (caller doesn't init)
bool TryParse(string s, out int result) {
    result = 0;
    return int.TryParse(s, out result);
}

// ref: caller must initialize
void Increment(ref int n) { n++; }
int x = 5;
Increment(ref x);  // x is now 6

// params: variable arguments
int Sum(params int[] nums) => nums.Sum();
Sum(1, 2, 3);  // 6
Sum();         // 0

デリゲートとイベント

デリゲートはタイプセーフな関数ポインタです。Func<T,TResult> は入力を取り値を返し、Action<T> は void を返し、Predicate<T> は bool を返します。イベントは +=/-= でサブスクライブし、?.Invoke で安全に発生(null チェック)するデリゲートです。オブザーバーパターン(pub/sub)にイベントを使用してください。

csharp
// Delegate type
delegate int MathOp(int a, int b);

MathOp add = (a, b) => a + b;
MathOp mul = (a, b) => a * b;
Console.WriteLine(add(3, 4));  // 7

// Built-in delegate types
Func<int, int, int> divide = (a, b) => a / b;
Action<string> log = msg => Console.WriteLine(msg);
Predicate<int> isEven = n => n % 2 == 0;

// Events
event Action<string> OnMessage;
OnMessage += msg => Console.WriteLine($"Got: {msg}");
OnMessage?.Invoke("Hello");

ラムダ式

ラムダは => を使用する匿名メソッドです。外側のスコープから変数をキャプチャします(クロージャ)。キャプチャされた変数はラムダ作成時ではなく実行時に評価されます—ループ内ではこれに注意してください。LINQ、イベント、コールバックでラムダを広範に使用してください。

csharp
// Lambda with explicit types
Func<int, int> square = (int x) => x * x;

// Type inference
Func<int, int, int> add = (a, b) => a + b;

// Statement lambda
Action<string> log = msg => {
    Console.WriteLine($"[{DateTime.Now}] {msg}");
};

// Capturing variables (closures)
int factor = 3;
Func<int, int> multiply = x => x * factor;
Console.WriteLine(multiply(5));  // 15

拡張メソッド

拡張メソッドは既存の型を変更せずにメソッドを追加します。最初のパラメータの前の this キーワードが拡張メソッドであることを示します。静的クラスに配置する必要があります。LINQ は IEnumerable 上の拡張メソッドとして完全に実装されています。これはユーティリティメソッドを追加する強力な方法です。

csharp
static class StringExtensions {
    public static int WordCount(this string s) {
        return s.Split(new[] { ' ' },
            StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

string text = "Hello World C#";
Console.WriteLine(text.WordCount());  // 3

// LINQ is built on extension methods
var evens = nums.Where(n => n % 2 == 0);
06

コレクション

List<T>

List<T> は動的配列です(C++ の vector や Java の ArrayList のようなもの)。Add/Remove は償却 O(1) / O(n) です。Count は要素数を返します(容量ではありません)。Find/FindAll は述語を使用します。中央での頻繁な挿入/削除には LinkedList<T> が適している場合があります。

csharp
using System.Collections.Generic;
var nums = new List<int> { 1, 2, 3 };
nums.Add(4);
nums.AddRange(new[] { 5, 6 });
nums.Remove(3);
nums.RemoveAt(0);
Console.WriteLine(nums.Count);  // 4
Console.WriteLine(nums.Contains(4));  // true

// Find
int first = nums.Find(n => n > 3);
List<int> all = nums.FindAll(n => n > 3);

Dictionary<TKey, TValue>

Dictionary はハッシュテーブルで、平均 O(1) でルックアップできます。TryGetValue はインデクサ(KeyNotFoundException をスロー)より安全です。大文字小文字を区別しない文字列キーには、コンストラクタに StringComparer.OrdinalIgnoreCase を渡してください。Dictionary は挿入順序を保持しません(必要な場合は OrderedDictionary を使用)。

csharp
var ages = new Dictionary<string, int> {
    ["Alice"] = 30,
    ["Bob"] = 25
};
ages["Charlie"] = 35;

// Safe access
if (ages.TryGetValue("Alice", out int age)) {
    Console.WriteLine($"Alice: {age}");
}

// Iterate
foreach (var kv in ages) {
    Console.WriteLine($"{kv.Key}: {kv.Value}");
}

// Case-insensitive keys
var dict = new Dictionary<string, int>(
    StringComparer.OrdinalIgnoreCase);

HashSet<T> と SortedSet<T>

HashSet<T> は O(1) でルックアップできるユニーク要素を格納します—重複排除とメンバーシップテストに使用します。セット操作(Union、Intersect、Except)をサポートします。SortedSet<T>(赤黒木)は要素をソートし O(log n) で操作します。順序が不要な場合は HashSet、必要な場合は SortedSet を使用してください。

csharp
var set = new HashSet<int> { 1, 2, 3 };
set.Add(2);  // duplicate, ignored
set.Add(4);
Console.WriteLine(set.Count);  // 4
Console.WriteLine(set.Contains(3));  // true

// Set operations
var other = new HashSet<int> { 3, 4, 5 };
set.IntersectWith(other);  // set: {3, 4}
set.UnionWith(other);      // set: {3, 4, 5}

// SortedSet keeps elements sorted
var sorted = new SortedSet<int> { 3, 1, 2 };  // {1, 2, 3}

Queue<T> と Stack<T>

Queue<T> は FIFO(Enqueue/Dequeue)—タスクスケジューリング、BFS に使用します。Stack<T> は LIFO(Push/Pop)—アンドゥ/リドゥ、DFS、式評価に使用します。どちらもコア操作は O(1) です。並行シナリオには System.Collections.Concurrent の ConcurrentQueue と ConcurrentStack を使用してください。

csharp
// Queue: FIFO (first in, first out)
var queue = new Queue<string>();
queue.Enqueue("first");
queue.Enqueue("second");
string next = queue.Dequeue();  // "first"
string peek = queue.Peek();     // "second"

// Stack: LIFO (last in, first out)
var stack = new Stack<int>();
stack.Push(1);
stack.Push(2);
int top = stack.Pop();   // 2
int peek = stack.Peek(); // 1

配列

配列は固定サイズです。Array.Sort はインプレースでソートします。多次元配列([,])は長方形(均一な列)です。ジャグ配列([][])は配列の配列です(行ごとに異なる長さを持てます)。動的サイズには List<T> を、固定サイズのパフォーマンス重視データには配列を使用してください。

csharp
int[] nums = { 1, 2, 3, 4, 5 };
Console.WriteLine(nums.Length);  // 5
Array.Sort(nums);
Array.Reverse(nums);
int found = Array.IndexOf(nums, 3);
int[] copy = new int[5];
Array.Copy(nums, copy, 5);

// Multidimensional
int[,] grid = { { 1, 2 }, { 3, 4 } };
Console.WriteLine(grid[1, 0]);  // 3

// Jagged array (array of arrays)
int[][] jagged = { new[] { 1, 2 }, new[] { 3, 4, 5 } };
07

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

クラスとプロパティ

自動プロパティ({ get; set; })はバッキングフィールドを自動生成します。式形式プロパティ(=>)はアクセス時に計算します。C# 9 以降の init 専用プロパティ({ get; init; })は構築中のみ設定可能で、不変性を実現します。オブジェクト初期化子:new Person { Name = "X" }。

csharp
class Person {
    // Auto-properties (C# 3+)
    public string Name { get; set; }
    public int Age { get; set; }

    // Read-only property
    public bool IsAdult => Age >= 18;

    // Constructor
    public Person(string name, int age) {
        Name = name;
        Age = age;
    }
}

var p = new Person("Alice", 30);
Console.WriteLine(p.IsAdult);  // true

継承と Virtual

virtual はオーバーライド可能なメソッドを示し、サブクラスで override が新しい実装を提供します。C# は明示的な override が必要です(Java とは異なります)。sealed はクラスの継承やメソッドのオーバーライドを防ぎます。すべてのクラスは暗黙的に object(System.Object)を継承します。

csharp
class Animal {
    public virtual void Speak() {
        Console.WriteLine("...");
    }
}

class Dog : Animal {
    public override void Speak() {
        Console.WriteLine("Woof");
    }
}

Animal a = new Dog();
a.Speak();  // Woof (polymorphism)

// sealed prevents further inheritance
sealed class Puppy : Dog { }

インターフェース

インターフェースは実装なしでコントラクトを定義します。クラスは複数のインターフェースを実装できます(単一クラス継承とは異なります)。C# 8 以降はデフォルトインターフェースメソッドを許可します。ポリモーフィズムと依存性注入にインターフェースを使用してください。命名規約:I プレフィックス(IShape、ILogger)。

csharp
interface IShape {
    double Area();  // interface member
    double Perimeter { get; }
}

class Circle : IShape {
    public double Radius { get; set; }
    public double Area() => Math.PI * Radius * Radius;
    public double Perimeter => 2 * Math.PI * Radius;
}

// Default interface methods (C# 8+)
interface ILogger {
    void Log(string msg);
    void Error(string msg) => Log($"ERROR: {msg}");  // default
}

抽象クラス

抽象クラスはインスタンス化できず、抽象(オーバーライド必須)メンバーと具象(virtual)メンバーの両方を持てます。共有実装がある場合は抽象クラスを、純粋なコントラクトにはインターフェースを使用してください。クラスは1つの抽象クラスのみ継承できますが、多くのインターフェースを実装できます。

csharp
abstract class Shape {
    public abstract double Area();  // must be overridden
    public virtual void Describe() {
        Console.WriteLine($"Area: {Area()}");
    }
}

class Rectangle : Shape {
    public double Width { get; set; }
    public double Height { get; set; }
    public override double Area() => Width * Height;
}

// Cannot instantiate abstract class
// var s = new Shape();  // ERROR
var r = new Rectangle { Width = 3, Height = 4 };

レコードと構造体

レコード(C# 9 以降)は値ベースの等値性、不変性、簡潔な構文を提供します—DTOやデータモデルに最適です。with 式で変更したコピーを作成できます。構造体は値型(代入時にコピー、スタック割り当て)です—小さく軽量なデータに使用します。クラスは参照型(ヒープ割り当て)です。

csharp
// Record (C# 9+): value-based equality, immutability
record Point(int X, int Y);

var p1 = new Point(3, 4);
var p2 = new Point(3, 4);
Console.WriteLine(p1 == p2);  // true (value equality)

// with expression (non-destructive mutation)
var p3 = p1 with { X = 10 };

// Struct: value type (stack-allocated)
struct Vector {
    public double X, Y;
    public Vector(double x, double y) { X = x; Y = y; }
}
08

LINQ

Where と Select

LINQ(Language Integrated Query)はコレクションに対する SQL ライクなクエリを提供します。Where はフィルタリング、Select は変換(map)を行います。LINQ は遅延実行です—クエリは列挙時(例:ToList() 経由)にのみ実行されます。これにより中間コレクションなしの効率的なチェーンが可能です。メソッド構文(上記)が最も一般的で、クエリ構文も利用可能です。

csharp
using System.Linq;
int[] nums = { 1, 2, 3, 4, 5, 6 };

// Filter
var evens = nums.Where(n => n % 2 == 0);  // 2, 4, 6

// Map/Transform
var squares = nums.Select(n => n * n);  // 1, 4, 9, 16, 25, 36

// Chain operations
var result = nums
    .Where(n => n > 2)
    .Select(n => n * 10)
    .ToList();  // 30, 40, 50, 60

並べ替えとグループ化

OrderBy/OrderByDescending はソートし、ThenBy は第2ソート条件を追加します。GroupBy は要素をキーでクラスタリングし、IGrouping<key, element> グループを返します。各グループは Key を持ち、メンバーの IEnumerable でもあります。これはグループ化のための手動ループとディクショナリを置き換えます。

csharp
var people = new[] {
    new { Name = "Alice", Age = 30 },
    new { Name = "Bob", Age = 25 },
    new { Name = "Charlie", Age = 30 },
};

// Order
var sorted = people.OrderBy(p => p.Age).ThenBy(p => p.Name);
var desc = people.OrderByDescending(p => p.Age);

// Group
var grouped = people.GroupBy(p => p.Age);
foreach (var g in grouped) {
    Console.WriteLine($"Age {g.Key}: {g.Count()} people");
}
// Age 25: 1 people
// Age 30: 2 people

集計

LINQ 集計メソッド(Sum、Average、Min、Max、Count)はコレクションから単一値を計算します。Aggregate は最も汎用的で—関数を累積的に適用する fold/reduce です。これらは空のシーケンスでスローします。*OrDefault バリアントを使用するか、事前に空かどうか確認してください。

csharp
int[] nums = { 1, 2, 3, 4, 5 };

Console.WriteLine(nums.Sum());    // 15
Console.WriteLine(nums.Average()); // 3
Console.WriteLine(nums.Min());    // 1
Console.WriteLine(nums.Max());    // 5
Console.WriteLine(nums.Count());  // 5

// Count with predicate
int evens = nums.Count(n => n % 2 == 0);  // 2

// Aggregate (fold/reduce)
int product = nums.Aggregate((a, b) => a * b);  // 120
string joined = nums.Aggregate("", (s, n) => s + n);  // "12345"

First、Single、ElementAt

First は最初の要素を返します(空の場合スロー)。FirstOrDefault は見つからない場合デフォルト(int なら 0、参照型なら null)を返します。Single は要素が1つであることを要求します(それ以外はスロー)—検証に使用します。Any/All はコレクション全体を列挙せず bool を返します(短絡評価)。

csharp
int[] nums = { 1, 2, 3, 4, 5 };

int first = nums.First();           // 1
int firstEven = nums.First(n => n % 2 == 0);  // 2
int firstOrDef = nums.FirstOrDefault(n => n > 10);  // 0 (default)

int single = new[] { 42 }.Single();  // 42
// nums.Single();  // throws! more than one element

int third = nums.ElementAt(2);  // 3 (0-indexed)

bool any = nums.Any(n => n > 3);  // true
bool all = nums.All(n => n > 0);  // true

Join と Zip

Join は内部結合(SQL のように)をキーでマッチングして実行します。Zip は2つのシーケンスの要素を位置でペアにします。どちらも新しいシーケンスを生成します。LINQ は GroupJoin(グループ化付き左結合)もサポートします。複数ソースからの関連データを組み合わせるのに強力です。

csharp
var users = new[] {
    new { Id = 1, Name = "Alice" },
    new { Id = 2, Name = "Bob" },
};
var orders = new[] {
    new { UserId = 1, Item = "Book" },
    new { Id = 2, Item = "Pen" },
};

// Join (inner join)
var joined = users.Join(orders,
    u => u.Id, o => o.UserId,
    (u, o) => new { u.Name, o.Item });
// { Alice, Book }

// Zip (pair elements by position)
var names = new[] { "Alice", "Bob" };
var ages = new[] { 30, 25 };
var pairs = names.Zip(ages, (n, a) => $"{n}: {a}");
// "Alice: 30", "Bob: 25"
09

エラー処理

Try / Catch / Finally

最初に具体的な例外をキャッチし、最後に汎用 Exception をキャッチします(最も具体的から最も汎用的へ)。finally は常に実行されます—クリーンアップ(ファイルのクローズ、リソースの解放)に使用します。制御フローに catch (Exception) を避け、処理できるもののみキャッチしてください。スタックトレースを保持するため throw; を使用してください(throw e; ではなく)。

csharp
try {
    int x = 10;
    int y = 0;
    int z = x / y;  // throws DivideByZeroException
} catch (DivideByZeroException e) {
    Console.WriteLine($"Error: {e.Message}");
} catch (Exception e) {
    Console.WriteLine($"Unexpected: {e.Message}");
} finally {
    // always runs, even with return/throw
    Console.WriteLine("Cleanup");
}

カスタム例外

カスタム例外を Exception(またはより具体的な基底クラス)から派生させてください。デバッグに役立つコンテキストフィールドを追加します。常に基底コンストラクタにメッセージを渡してください。慣例として、例外クラス名は Exception で終わります。呼び出し側が処理できるドメイン固有のエラー条件にカスタム例外を使用してください。

csharp
class ValidationError : Exception {
    public string Field { get; }
    public ValidationError(string field, string msg)
        : base(msg) {
        Field = field;
    }
}

throw new ValidationError("email", "Invalid format");

try {
    // ...
} catch (ValidationError e) {
    Console.WriteLine($"{e.Field}: {e.Message}");
}

using と IDisposable

using は例外が発生しても Dispose() が呼ばれることを保証します—これは C# のリソース管理における RAII と同等です。using 宣言(C# 8 以降)はよりクリーンで—スコープ終了時に Dispose が呼ばれます。アンマネージリソース(ファイルハンドル、データベース接続)を保持するクラスに IDisposable を実装してください。

csharp
// using statement (C# 1+)
using (var file = new StreamReader("data.txt")) {
    string content = file.ReadToEnd();
}  // file.Dispose() called automatically

// using declaration (C# 8+)
using var file2 = new StreamReader("data.txt");
string content2 = file2.ReadToEnd();
// Dispose called at end of scope

// Implementing IDisposable
class Resource : IDisposable {
    public void Dispose() {
        // release unmanaged resources
        GC.SuppressFinalize(this);
    }
}

Null 処理

C# 8 以降の null 許容参照型(string?)はコンパイル時の null 安全性を有効にします。?. は NullReferenceException をスローせず null を返します。?? はフォールバックを提供します。throw 式(?? throw)は検証に簡潔です。プロジェクト全体の null チェックには .csproj で <Nullable>enable</Nullable> を有効にしてください。

csharp
string? name = null;

// Null-conditional operator
int? length = name?.Length;  // null (no exception)

// Null-coalescing operator
string display = name ?? "Unknown";  // "Unknown"

// Throw if null
string required = name ?? throw new ArgumentNullException(nameof(name));

// Pattern matching null check
if (name is null) { /* ... */ }
if (name is { Length: > 5 }) { /* not null and length > 5 */ }

例外フィルター

例外フィルター(when)は catch ブロックに条件を追加します—フィルターが true の場合のみ例外がキャッチされます。これはキャッチして再スローするより強力で、スタックトレースを保持します。ログパターン(when が false を返す)で例外を処理せずに観察できます。

csharp
try {
    // ...
} catch (HttpRequestException e) when (e.StatusCode == 404) {
    Console.WriteLine("Not found");
} catch (HttpRequestException e) when (e.StatusCode >= 500) {
    Console.WriteLine("Server error");
} catch (HttpRequestException e) {
    Console.WriteLine($"HTTP error: {e.Message}");
}

// Log without catching
catch (Exception e) when (LogError(e)) {
    // never enters if LogError returns false
}
private bool LogError(Exception e) { /* log */ return false; }
10

ファイル I/O と非同期

File クラス(簡易 I/O)

File は簡易なワンショットファイル操作の静的メソッドを提供します—シンプルですがファイル全体をメモリに読み込みます。大きなファイルには StreamReader/StreamWriter で1行ずつ処理してください。File.Exists で存在を確認できますが TOCTOU レースがあります—ファイル操作の例外は常に処理してください。

csharp
using System.IO;

// Read all text at once
string content = File.ReadAllText("input.txt");
string[] lines = File.ReadAllLines("input.txt");

// Write
File.WriteAllText("output.txt", "Hello\n");
File.WriteAllLines("output.txt", new[] { "Line 1", "Line 2" });

// Append
File.AppendAllText("log.txt", $"[{DateTime.Now}] Event\n");

// File info
bool exists = File.Exists("data.txt");
var info = new FileInfo("data.txt");
Console.WriteLine(info.Length);  // bytes

ストリーム(StreamReader/Writer)

StreamReader/StreamWriter はファイル全体をメモリに読み込まず1行ずつ処理します—大きなファイルに使用します。using ステートメントで例外が発生してもファイルがクローズされます。StreamWriter はデータをバッファリングします。Flush() で即座に書き込むか、Dispose に任せてください。

csharp
using System.IO;

// Reading line by line
using var reader = new StreamReader("input.txt");
string? line;
while ((line = reader.ReadLine()) != null) {
    Console.WriteLine(line);
}

// Writing
using var writer = new StreamWriter("output.txt");
writer.WriteLine("First line");
writer.WriteLine("Second line");
writer.Flush();  // optional, using disposes and flushes

async / await

async/await はノンブロッキングな非同期操作を可能にします。メソッドは Task<T>(または void の場合は Task)を返します。await はスレッドをブロックせずメソッドを一時停止します—スレッドは他の作業に解放されます。これは UI や Web アプリケーションで I/O バウンド操作(ファイル、ネットワーク、データベース)に不可欠です。

csharp
async Task<string> ReadFileAsync(string path) {
    using var reader = new StreamReader(path);
    return await reader.ReadToEndAsync();
}

// Calling async method
string content = await ReadFileAsync("data.txt");
Console.WriteLine(content.Length);

// Async doesn't block the calling thread
// The thread is freed while waiting for I/O

Task と Parallel

Task.Run はスレッドプールで作業をスケジュールします。Task.WhenAll は複数のタスクを並行して待機します(CPU バウンド作業に async/await は使用しないでください—Task.Run を使用)。Parallel.For は CPU バウンドの並列ループ用です。PLINQ(AsParallel)は LINQ クエリを並列化します。I/O には async を、CPU には Parallel/PLINQ を使用してください。

csharp
// Run work on a thread pool thread
var task = Task.Run(() => HeavyComputation());
int result = await task;

// Run multiple tasks concurrently
var t1 = Task.Run(() => DownloadAsync("url1"));
var t2 = Task.Run(() => DownloadAsync("url2"));
await Task.WhenAll(t1, t2);  // wait for both

// Parallel.For for CPU-bound parallelism
Parallel.For(0, 100, i => {
    Process(i);  // runs on multiple threads
});

// PLINQ
var results = nums.AsParallel()
    .Where(n => IsPrime(n))
    .ToArray();

JSON シリアライズ

System.Text.Json はモダンで高性能な JSON シリアライザです(ほとんどのケースで Newtonsoft.Json を置き換えます)。Serialize/Deserialize がオブジェクトと JSON 間の変換を処理します。書式設定(インデント、camelCase)には JsonSerializerOptions を使用してください。非同期の場合、大きな JSON をメモリに読み込まないようストリームベースのメソッドを使用してください。

csharp
using System.Text.Json;

var person = new { Name = "Alice", Age = 30 };

// Serialize
string json = JsonSerializer.Serialize(person);
// {"Name":"Alice","Age":30}

// Deserialize
Person p = JsonSerializer.Deserialize<Person>(json);

// With options
var options = new JsonSerializerOptions {
    WriteIndented = true,
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};
string pretty = JsonSerializer.Serialize(person, options);

// Async (for streams)
using var stream = File.OpenRead("data.json");
var data = await JsonSerializer.DeserializeAsync<Person>(stream);
11

LINQ 深掘り

クエリ構文とメソッド構文

LINQ には2つの同等の構文があります。クエリ構文は SQL ライクで複雑な結合/グループ化に読みやすいです。メソッド構文(fluent)はより一般的で、すべての演算子をサポートし、自然にチェーンできます。どちらも同じ IL にコンパイルされます。複雑な複数句クエリにはクエリ構文を、単純なチェーンにはメソッド構文を使用してください。どちらも遅延評価(遅延実行)です。

csharp
List<int> nums = new() { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// Query syntax (SQL-like)
var evens = from n in nums
            where n % 2 == 0
            orderby n descending
            select n * 2;

// Method syntax (fluent) — equivalent
var evens2 = nums.Where(n => n % 2 == 0)
                  .OrderByDescending(n => n)
                  .Select(n => n * 2);

// Query syntax supports: from, where, orderby, select, group, join, let
// Method syntax supports everything query does, plus more

遅延実行と即時実行

LINQ は遅延実行を使用します—Where/Select/OrderBy はクエリを構築するだけで、列挙時に実行されます。つまり結果はクエリ作成時ではなく列挙時のソースの状態を反映します。ToList/ToArray/Count などは即時実行を強制し、スナップショットを取得します。注意:遅延クエリを2回列挙すると2回実行されます。安定した結果が必要な場合は ToList でキャッシュしてください。

csharp
var nums = new List<int> { 1, 2, 3 };

// Deferred: query not run until enumerated
var query = nums.Where(n => n > 1);
nums.Add(4);  // query sees this!
Console.WriteLine(query.Count());  // 3 (2, 3, 4)

// Immediate: forces execution now
var list = nums.Where(n => n > 1).ToList();  // runs now
nums.Add(5);
Console.WriteLine(list.Count);  // still 3 (snapshot)

// Operators that force immediate execution:
// ToList, ToArray, ToDictionary, ToLookup,
// Count, Sum, Min, Max, Average, First, Last, Any, All

グループ化と結合

GroupBy は要素をキーでクラスタリングし、IGrouping<key, element> シーケンスを返します。Join は内部結合(キーのマッチング)を実行します。グループ結合(join...into)は階層的な結果を作成し、DefaultIfEmpty() で左外部結合が可能です。これらの操作はデータ分析に強力です。大規模データセットの場合、繰り返しルックアップに ToLookup を検討してください(事前計算されたグループ化です)。

csharp
var students = new[] {
    new { Name = "Alice", Grade = "A", Dept = "CS" },
    new { Name = "Bob", Grade = "B", Dept = "Math" },
    new { Name = "Carol", Grade = "A", Dept = "CS" },
};

// Group by department
var byDept = students.GroupBy(s => s.Dept);
foreach (var g in byDept) {
    Console.WriteLine(`{g.Key}: {g.Count()}`);
    // CS: 2, Math: 1
}

// Inner join
var courses = new[] { new { Dept = "CS", Course = "Algo" } };
var joined = from s in students
             join c in courses on s.Dept equals c.Dept
             select new { s.Name, c.Course };

// Group join (left outer join with DefaultIfEmpty)
var groupJoin = from s in students
                join c in courses on s.Dept equals c.Dept into sc
                from c in sc.DefaultIfEmpty()
                select new { s.Name, Course = c?.Course };

集計と量指定子

LINQ は標準集計(Sum、Min、Max、Average、Count)とカスタム削減用の汎用 Aggregate(JavaScript の reduce のようなもの)を提供します。量指定子(Any、All、Contains)はブール値を返し短絡します—Any は最初のマッチで停止、All は最初の非マッチで停止します。存在確認には Count() > 0 ではなく Any() を使用してください—より効率的で読みやすいです。

csharp
var nums = new[] { 1, 2, 3, 4, 5 };

// Aggregations
int sum = nums.Sum();              // 15
double avg = nums.Average();       // 3.0
int min = nums.Min();              // 1
int max = nums.Max();              // 5
int count = nums.Count();          // 5

// Custom aggregation (like reduce/fold)
int product = nums.Aggregate((a, b) => a * b);  // 120
int withSeed = nums.Aggregate(10, (a, b) => a + b);  // 25

// Quantifiers (return bool)
bool anyEven = nums.Any(n => n % 2 == 0);  // true
bool allPos = nums.All(n => n > 0);        // true
bool contains3 = nums.Contains(3);         // true

// LongCount for > int.MaxValue elements
long big = largeList.LongCount();

IEnumerable と IQueryable

IEnumerable<T> はインメモリコレクション用(LINQ to Objects)—デリゲートを使用し、ローカルで実行されます。IQueryable<T> はリモートソース用(Entity Framework、LINQ to SQL)—式ツリーを使用し、ソースのクエリ言語(SQL)に変換されます。IQueryable はサーバーサイドクエリを効率的に構成します。変換できないクライアントサイドロジックが必要な場合は AsEnumerable() で IEnumerable に切り替えてください。

csharp
// IEnumerable<T>: in-memory, LINQ to Objects
List<int> nums = new() { 1, 2, 3 };
var q1 = nums.Where(n => n > 1);  // executes in memory

// IQueryable<T>: translates to query language (SQL)
// Expression trees, not delegates
using var db = new MyDbContext();
var q2 = db.Users.Where(u => u.Age > 18);  // generates SQL
// SELECT * FROM Users WHERE Age > 18

// IQueryable composes queries that execute server-side
var query = db.Users
    .Where(u => u.Active)
    .OrderBy(u => u.Name)
    .Take(10);
// All translated to one SQL query

// Force client-side with AsEnumerable()
var clientSide = db.Users.AsEnumerable()
    .Where(u => ExpensiveCheck(u));
12

Async/Await 深掘り

Task と Task<T>

Task は結果のない非同期操作を表し、Task<T> は T を返します。ValueTask<T>(C# 7)は結果が同期的に利用可能な場合(キャッシュシナリオ)にヒープ割り当てを回避します—ただし1回しか待機できません。非同期 API で同期的な結果には Task.FromResult を使用してください。非同期メソッドには void より Task を優先してください(void はイベントハンドラ専用です)。

csharp
// Task: represents an async operation (no result)
async Task DoWorkAsync() {
    await Task.Delay(1000);
    Console.WriteLine("Done");
}

// Task<T>: async operation returning T
async Task<int> GetCountAsync() {
    await Task.Delay(500);
    return 42;
}

// ValueTask<T>: lightweight, avoids allocation
ValueTask<int> GetCachedAsync() {
    if (_cache != null) return new ValueTask<int>(_cache);
    return new ValueTask<int>(LoadFromDbAsync());
}

// Converting to Task
Task t = Task.Run(() => Console.WriteLine("hi"));
Task<int> t2 = Task.FromResult(42);  // already complete
Task completed = Task.CompletedTask;

CancellationToken

CancellationToken は協調的キャンセルを可能にします。非同期メソッドに渡し、ループ内で ThrowIfCancellationRequested() をチェックしてください。呼び出し側は CancellationTokenSource.Cancel() でキャンセルし、メソッドがいつ/どう応答するかを決定します。非同期 API(特にライブラリコード)では常に CancellationToken パラメータを受け取ってください。タイムアウトには TimeSpan を指定した CancellationTokenSource を使用してください。キャンセルは OperationCanceledException をスローします。

csharp
async Task DownloadAsync(string url, CancellationToken ct) {
    using var client = new HttpClient();
    // Pass token to async operations
    var data = await client.GetByteArrayAsync(url, ct);
    // Check manually in loops
    foreach (var chunk in data) {
        ct.ThrowIfCancellationRequested();
        Process(chunk);
    }
}

// Usage with timeout
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
try {
    await DownloadAsync("https://example.com", cts.Token);
} catch (OperationCanceledException) {
    Console.WriteLine("Timed out or cancelled");
}

// Cooperative cancellation: caller cancels, method checks
cts.Cancel();  // request cancellation

Task.WhenAll と WhenAny

Task.WhenAll は複数のタスクを並行して待機し、すべて完了すると続行します(並列性)。Task.WhenAny は最初のタスクが完了すると続行します(レース/冗長性)。WhenAll はデフォルトで最初の例外のみスローします。すべての失敗を確認するには各タスクの IsFaulted/Exception を調べてください。ファイアアンドフォーゲットには Task.Run を使用し、未観察例外を避けるため内部で例外を処理してください。

csharp
// WhenAll: wait for ALL to complete (like Promise.all)
var tasks = new[] {
    FetchAsync("url1"),
    FetchAsync("url2"),
    FetchAsync("url3"),
};
string[] results = await Task.WhenAll(tasks);
// Continues when all 3 done; throws AggregateException if any fails

// WhenAny: wait for FIRST to complete (race)
var racing = new[] { PrimaryAsync(), FallbackAsync() };
Task<string> firstDone = await Task.WhenAny(racing);
string result = await firstDone;

// WhenAll with exception handling
try {
    await Task.WhenAll(tasks);
} catch (Exception ex) {
    // Only first exception is thrown; access all via Task.Exception
    foreach (var inner in tasks.Where(t => t.IsFaulted))
        Console.WriteLine(inner.Exception);
}

非同期ストリーム(IAsyncEnumerable)

IAsyncEnumerable<T>(C# 8)は非同期ストリームです—yield 間に await を挟みながらアイテムを利用可能になった順に生成します。await foreach で消費します。大きなデータセットやリアルタイムデータをすべてメモリにバッファせずストリーミングするのに最適です。[EnumeratorCancellation] 属性により、WithCancellation で消費時にキャンセルトークンが正しくフローします。

csharp
// C# 8: async iterator (like async generator)
async IAsyncEnumerable<int> GenerateAsync(
    [EnumeratorCancellation] CancellationToken ct = default)
{
    for (int i = 0; i < 10; i++) {
        await Task.Delay(100, ct);
        yield return i;  // async yield
    }
}

// Consume with await foreach
await foreach (int n in GenerateAsync().WithCancellation(ct)) {
    Console.WriteLine(n);
}

// Use case: streaming data from a database or API
async IAsyncEnumerable<Record> StreamRecordsAsync() {
    await using var reader = await cmd.ExecuteReaderAsync();
    while (await reader.ReadAsync())
        yield return MapRecord(reader);
}

非同期の落とし穴(デッドロック)

非同期の最大の落とし穴:SynchronizationContext が存在する場合(UI アプリ、レガシー ASP.NET)、Task で .Result や .Wait() を呼ぶとデッドロックする可能性があります。await はコンテキストをキャプチャし、.Result は継続に必要なスレッドをブロックします。解決策:メソッドを完全に非同期にするか、ライブラリコードで ConfigureAwait(false) を使用してください。async void は危険です—例外をキャッチできず、待機もできません。代わりに async Task を使用してください。

csharp
// BAD: .Result blocks (can deadlock in UI/ASP.NET classic)
public string BadMethod() {
    string data = GetDataAsync().Result;  // DEADLOCK risk!
    return data;
}

// GOOD: async all the way
public async Task<string> GoodMethodAsync() {
    string data = await GetDataAsync();
    return data;
}

// Why deadlock: in UI/ASP.NET context, await captures
// SynchronizationContext. .Result blocks the thread,
// but the continuation needs that same thread → deadlock.

// Fix if you MUST block (last resort):
string data = Task.Run(GetDataAsync).Result;  // offloads
// Or ConfigureAwait(false) in library code:
await GetDataAsync().ConfigureAwait(false);

// async void is EVIL (except event handlers)
async void Bad() { await Task.Delay(1); throw new Exception(); }
// Exception can't be caught by caller!
13

イベントとデリゲート

デリゲートの基礎

デリゲートはタイプセーフな関数ポインタです。カスタムデリゲート(delegate int MathOp(int,int))はほとんど組み込みの Action(void 戻り値)と Func<T>(T を返す)に置き換えられました。デリゲートはマルチキャストです—複数のメソッドを保持できます(+ で結合)。呼び出すとすべてのメソッドが実行されます。マルチキャストデリゲートの戻り値は最後のメソッドの結果です。最新のコードには Action/Func を使用してください。

csharp
// Declare a delegate type
public delegate int MathOp(int a, int b);

// Methods matching the signature
static int Add(int a, int b) => a + b;
static int Mul(int a, int b) => a * b;

MathOp op = Add;
Console.WriteLine(op(2, 3));  // 5
op = Mul;
Console.WriteLine(op(2, 3));  // 6

// Multicast: combine delegates
MathOp multi = (MathOp)Add + Mul;
// Note: returns last result (Add's), but both run
// (return value of multicast is the last one's)

// Built-in: Action (void), Func<T> (returns T)
Action<string> log = s => Console.WriteLine(s);
Func<int, int, int> add = (a, b) => a + b;
Predicate<int> isEven = n => n % 2 == 0;  // returns bool

イベント(パブリッシャー/サブスクライバー)

イベントはデリゲートの制限された形式です—宣言クラスのみが呼び出せますが、外部コードはサブスクライブ/サブスクライブ解除(+=/-=)できます。標準パターンには EventHandler<TArgs> を使用してください。サブスクライバーのないイベントは null なので、常に null 条件演算子(Click?.Invoke)を使用してください。メモリリークを防ぐため必ずサブスクライブ解除してください(イベントはハンドラへの強い参照を保持します)。

csharp
public class Button {
    // Event: restricted multicast delegate
    public event EventHandler<EventArgs> Click;

    public void SimulateClick() {
        // Null-conditional invoke pattern (thread-safe)
        Click?.Invoke(this, EventArgs.Empty);
    }
}

// Custom event args
public class SearchEventArgs : EventArgs {
    public string Query { get; init; }
}

// Subscribe
var btn = new Button();
btn.Click += (sender, e) => Console.WriteLine("Clicked!");
btn.Click += HandlerMethod;  // method group

// Unsubscribe (MUST to avoid memory leaks)
btn.Click -= HandlerMethod;

void HandlerMethod(object sender, EventArgs e) { /* ... */ }

INotifyPropertyChanged(データバインディング)

INotifyPropertyChanged は WPF、MAUI、WinForms でのデータバインディングの標準パターンです。プロパティが変更されたら PropertyChanged を発生させ、バインドされた UI が更新されます。[CallerMemberName](C# 5)が呼び出し元プロパティの名前を自動的に渡します—マジックストリング不要です。チェック(if != value)で不要な通知を防ぎます。ViewModel で多用されます。ソースジェネレータ(CommunityToolkit.Mvvm)でこのボイラープレートを自動生成できます。

csharp
using System.ComponentModel;
using System.Runtime.CompilerServices;

public class ViewModel : INotifyPropertyChanged {
    private string _name;
    public string Name {
        get => _name;
        set {
            if (_name != value) {
                _name = value;
                OnPropertyChanged();  // raises event
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    // CallerMemberName auto-fills the property name
    protected void OnPropertyChanged([CallerMemberName] string prop = null) {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
    }
}

// WPF/MAUI bindings automatically update UI when Name changes

イベントとデリゲート

主な違い:public デリゲートフィールドは誰でも呼び出し、再割り当て、クリアできます。イベントは外部アクセスを +=(サブスクライブ)と -=(サブスクライブ解除)のみに制限します—宣言クラスのみが呼び出しやクリアができます。このカプセル化がイベントがオブザーバーパターンの標準である理由です。イベントはスレッドセーフな add/remove アクセサも生成し、デザイナツールと統合します。

csharp
// Public delegate: anyone can invoke or clear
public delegate void Notify();
public class Bad {
    public Notify OnDone;  // field-like, public
}
// External code can do:
//   bad.OnDone();        // invoke (BAD: not the owner)
//   bad.OnDone = null;   // clear all subscribers (BAD)

// Event: only owner can invoke/clear
public class Good {
    public event Notify OnDone;  // restricted
    public void Complete() {
        OnDone?.Invoke();  // OK: owner invokes
    }
}
// External code can ONLY += or -= (subscribe/unsubscribe)
// Cannot invoke or clear from outside

// Event = delegate + access restriction
// Under the hood: event generates add/remove accessors

弱いイベント(メモリリーク防止)

イベントはサブスクライバーへの強い参照を保持するため、サブスクライバーがガベージコレクションされるべきでもパブリッシャーが存続するとメモリリークが発生します。解決策:明示的にサブスクライブ解除する(最善)、WeakEventManager(WPF)を使用する、または WeakReference で弱いイベントパターンを実装する。これは長時間実行アプリ(サーバー、デスクトップアプリ)でリークの一般的な原因です。サブスクライバーのライフタイム終了時や Dispose で必ずサブスクライブ解除してください。

csharp
// Problem: event holds strong ref to subscriber → leak
// if subscriber should be GC'd but publisher lives long

// Solution 1: WeakEventManager (WPF)
WeakEventManager<Publisher, EventArgs>.AddHandler(pub, "Event", Handler);

// Solution 2: WeakReference in custom pattern
public class WeakEvent<TArgs> where TArgs : EventArgs {
    private List<WeakReference<EventHandler<TArgs>>> _handlers = new();
    public void Subscribe(EventHandler<TArgs> h) =>
        _handlers.Add(new WeakReference<EventHandler<TArgs>>(h));
    public void Raise(object sender, TArgs args) {
        _handlers.RemoveAll(wr => !wr.TryGetTarget(out _));
        foreach (var wr in _handlers) {
            if (wr.TryGetTarget(out var h)) h(sender, args);
        }
    }
}

// Best practice: unsubscribe (-=) when done, or use
// IDisposable pattern with weak references for long-lived publishers
14

リフレクション

型情報

リフレクションは実行時に型メタデータを検査します。typeof(T) はコンパイル時に Type を取得し、obj.GetType() は実行時に取得します。Type は Name、IsClass、IsValueType とメンバーを列挙するメソッド(GetMethods、GetProperties、GetFields)を提供します。BindingFlags が返される内容を制御します(NonPublic、Instance、Static、Public)。リフレクションは強力ですが遅いです—可能な限り結果をキャッシュしてください。

csharp
using System;

Type t = typeof(string);
// Or: Type t = "hello".GetType();

Console.WriteLine(t.Name);          // String
Console.WriteLine(t.FullName);      // System.String
Console.WriteLine(t.IsClass);       // true
Console.WriteLine(t.IsValueType);   // false
Console.WriteLine(t.IsAbstract);    // false

// Get methods
foreach (var m in t.GetMethods())
    Console.WriteLine(m.Name);

// Get properties
var props = t.GetProperties();

// Get fields
var fields = t.GetFields(BindingFlags.NonPublic | BindingFlags.Instance);

// Check if assignable
Type ienum = typeof(IEnumerable<int>);
Console.WriteLine(ienum.IsAssignableFrom(t));  // false

インスタンス化と呼び出し

Activator.CreateInstance はオブジェクトを動的に作成します。MethodInfo.Invoke はリフレクションでメソッドを呼び出します(引数のボックス化とセキュリティチェックのため遅い)。繰り返し呼び出しには CreateDelegate がはるかに高速です—リフレクションのオーバーヘッドをバイパスする強く型付けされたデリゲートを作成します。リフレクションはコンパイル時の型安全性を壊し直接呼び出しより遅いです。フレームワーク(シリアライズ、DI、ORM)とキャッシュシナリオに使用してください。

csharp
using System.Reflection;

// Create instance
Type t = typeof(StringBuilder);
object obj = Activator.CreateInstance(t);
// Or with args: Activator.CreateInstance(t, "initial")

// Invoke method
MethodInfo method = t.GetMethod("Append", new[] { typeof(string) });
object result = method.Invoke(obj, new object[] { "hello" });

// Get/set property
PropertyInfo prop = t.GetProperty("Length");
int len = (int)prop.GetValue(obj);

// Get/set field
FieldInfo field = t.GetField("_field", BindingFlags.NonPublic | BindingFlags.Instance);
field.SetValue(obj, 42);

// Create delegate from MethodInfo (faster than Invoke)
var appendDel = (Func<StringBuilder, string, StringBuilder>)
    method.CreateDelegate(typeof(Func<StringBuilder, string, StringBuilder>));

属性

属性はコード要素(クラス、メソッド、プロパティ)にメタデータを付与します。[AttributeUsage(...)] で有効なターゲットを指定して定義します。GetCustomAttributes<T>()(ジェネリック、モダン)または Attribute.GetCustomAttribute で取得します。組み込み属性:[Obsolete]、[Serializable]、[DllImport]、[Conditional]。フレームワークは属性を広範に使用します:[HttpGet](ASP.NET)、[JsonProperty](JSON)、[Table](EF Core)。

csharp
// Define an attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class AuthorAttribute : Attribute {
    public string Name { get; }
    public AuthorAttribute(string name) => Name = name;
}

// Apply
[Author("Alice")]
[Author("Bob")]
public class Widget {
    [Author("Charlie")]
    public void DoWork() { }
}

// Read via reflection
var attrs = typeof(Widget).GetCustomAttributes<AuthorAttribute>();
foreach (var a in attrs) Console.WriteLine(a.Name);  // Alice, Bob

// Check if attribute is applied
bool hasAuthor = Attribute.IsDefined(typeof(Widget), typeof(AuthorAttribute));

Emit と動的コード生成

Reflection.Emit は動的シナリオ(式コンパイル、シリアライザ、モックフレームワーク)で最大パフォーマンスのために実行時に IL を生成します。型/メソッドを構築し IL オペコードを直接発行します。これは高度です—よりシンプルな動的コードには式ツリー(Expression.Compile)を使用してください。Emit は DLR(動的言語)、ORM、シリアライズライブラリが実行時に高速な型付きコードを生成するために使用します。

csharp
using System.Reflection;
using System.Reflection.Emit;

// Create a dynamic assembly/module/type
var asmName = new AssemblyName("Dynamic");
var asm = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run);
var mod = asm.DefineDynamicModule("Main");
var type = mod.DefineType("Calculator", TypeAttributes.Public);

// Add a method
var method = type.DefineMethod("Add",
    MethodAttributes.Public | MethodAttributes.Static,
    typeof(int), new[] { typeof(int), typeof(int) });

// Emit IL
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldarg_0);  // load first arg
il.Emit(OpCodes.Ldarg_1);  // load second arg
il.Emit(OpCodes.Add);      // add
il.Emit(OpCodes.Ret);      // return

Type created = type.CreateType();
int result = (int)created.GetMethod("Add").Invoke(null, new object[] { 3, 4 });  // 7

式ツリー

式ツリーはコードをデータとして表現します(Expression ノードのツリー)。これらは IQueryable の基盤です(LINQ プロバイダが SQL などに変換)。Compile() が式を実行可能なデリゲートに変換します。動的コード生成のために手動で式を構築できます(Reflection.Emit より読みやすい)。式ツリーは ORM、ルールエンジン、動的 LINQ を可能にします。

csharp
using System.Linq.Expressions;

// Expression<Func<T>>: code as data (tree), not executable
Expression<Func<int, int, int>> expr = (a, b) => a + b + 1;

// Inspect the tree
Console.WriteLine(expr.Body.NodeType);  // Add
Console.WriteLine(expr.Body.Type);      // int

// Compile to a delegate (fast, runs in memory)
Func<int, int, int> compiled = expr.Compile();
int result = compiled(2, 3);  // 6

// EF Core translates expressions to SQL
Expression<Func<User, bool>> filter = u => u.Age > 18;
// → WHERE Age > 18

// Build expressions manually (advanced)
ParameterExpression x = Expression.Parameter(typeof(int), "x");
Expression body = Expression.Multiply(x, Expression.Constant(2));
var lambda = Expression.Lambda<Func<int, int>>(body, x);
Func<int, int> doubler = lambda.Compile();
15

拡張メソッドとパターン

拡張メソッド

拡張メソッドは既存の型を変更せずにメソッドを「追加」できます。最初のパラメータに 'this' を付けた静的クラスで定義します。これは構文糖です—コンパイラが email.IsEmail() を StringExtensions.IsEmail(email) に書き換えます。LINQ は完全に IEnumerable 上の拡張メソッドです。シールドクラス、インターフェース、サードパーティ型にユーティリティメソッドを追加するのに使用します。using でスコープに取り込みます。

csharp
public static class StringExtensions {
    // Must be static, in static class, first param with 'this'
    public static bool IsEmail(this string s) =>
        s.Contains('@') && s.Contains('.');

    public static string Repeat(this string s, int n) =>
        string.Concat(Enumerable.Repeat(s, n));

    // Chainable
    public static string Truncate(this string s, int max) =>
        s.Length <= max ? s : s[..max];
}

// Usage: appears as instance method
string email = "[email protected]";
bool valid = email.IsEmail();
string repeated = "ab".Repeat(3);  // "ababab"
string short_ = "hello world".Truncate(5);  // "hello"

// LINQ is built on extension methods (Enumerable class)
var evens = nums.Where(n => n % 2 == 0);  // extension

流暢なインターフェース

流暢なインターフェースは 'this' を返してメソッドチェーンを可能にし、文のように読める読みやすいコードを生成します。ビルダー(QueryBuilder)、設定(ASP.NET、EF)、DSL で一般的です。拡張メソッドでプリミティブ型に流暢なメソッドを追加できます(5.Seconds())。不変の流暢な API では純粋なメソッド(新しいインスタンスを返す)に、ビルダーでは this を変更して返すようにしてください。

csharp
public class QueryBuilder {
    private string _table = "";
    private string _where = "";
    private int _limit;

    public QueryBuilder From(string table) { _table = table; return this; }
    public QueryBuilder Where(string clause) { _where = clause; return this; }
    public QueryBuilder Limit(int n) { _limit = n; return this; }

    public string Build() =>
        $"SELECT * FROM {_table} WHERE {_where} LIMIT {_limit}";
}

// Fluent chaining
var sql = new QueryBuilder()
    .From("users")
    .Where("age > 18")
    .Limit(10)
    .Build();

// Extension methods enable fluent APIs on existing types
public static class IntExtensions {
    public static TimeSpan Seconds(this int n) => TimeSpan.FromSeconds(n);
    public static TimeSpan Minutes(this int n) => TimeSpan.FromMinutes(n);
}
var delay = 5.Seconds();  // TimeSpan
var timeout = 2.Minutes();

イテレータメソッド(yield)

yield return はメソッドをイテレータに変換します—コンパイラが状態機械を生成します。実行は遅延します:アイテムがプルされるたびにコードが実行されます。これにより無限シーケンスと効率的なストリーミング(バッファせず1アイテムずつ処理)が可能です。yield break で反復を終了します。メソッドは IEnumerable<T> または IEnumerator<T> を返す必要があります。LINQ 演算子は yield で構築されています。再列挙するとメソッドが再実行されます。

csharp
// yield return: lazy iterator (state machine generated)
public static IEnumerable<int> Evens(int max) {
    for (int i = 0; i <= max; i += 2)
        yield return i;
}

// Lazy: nothing runs until enumerated
var evens = Evens(100);  // no execution yet
foreach (var e in evens) { /* runs here */ }

// yield break: end iteration
public static IEnumerable<T> TakeWhile<T>(IEnumerable<T> src, Func<T, bool> pred) {
    foreach (var item in src) {
        if (!pred(item)) yield break;
        yield return item;
    }
}

// Infinite sequence (lazy, safe)
public static IEnumerable<int> Naturals() {
    int n = 0;
    while (true) yield return n++;
}
var first10 = Naturals().Take(10);  // 0..9

依存性注入

依存性注入(DI)は依存関係を内部で作成するのではなくコンストラクタ経由で渡します。利点:テスト容易性(テストで実装を差し替え)、疎結合、単一責任。.NET の組み込み DI コンテナ(Microsoft.Extensions.DependencyInjection)が登録とライフタイム(AddTransient、AddScoped、AddSingleton)を処理します。柔軟性のため常に具象クラスではなくインターフェースに依存してください。

csharp
// Interface for the dependency
public interface IEmailSender {
    Task SendAsync(string to, string body);
}

// Implementation
public class SmtpEmailSender : IEmailSender {
    public async Task SendAsync(string to, string body) { /* SMTP */ }
}

// Consumer: depends on abstraction, not concretion
public class UserService {
    private readonly IEmailSender _email;
    public UserService(IEmailSender email) => _email = email;  // inject

    public async Task RegisterAsync(string email) {
        // ... save user ...
        await _email.SendAsync(email, "Welcome!");
    }
}

// Register in DI container (ASP.NET Core)
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();
builder.Services.AddScoped<UserService>();

// Resolve
var userService = app.Services.GetRequiredService<UserService>();

ファクトリとビルダーパターン

ファクトリパターンはオブジェクト作成を一元化し、具象クラスを呼び出し側から隠します(作成が複雑な場合や実行時に型が選択される場合に有用)。ビルダーパターンは流暢なメソッドで複雑なオブジェクトを段階的に構築し、「telescoping コンストラクタ」(多数のパラメータ)を回避します。どちらも可読性と保守性を向上させます。C# レコードの 'with' 式は不変データのビルダーを置き換えることが多いです。

csharp
// Factory: create objects without specifying exact class
public interface IShape { void Draw(); }
public class Circle : IShape { public void Draw() => Console.WriteLine("○"); }
public class Square : IShape { public void Draw() => Console.WriteLine("□"); }

public static class ShapeFactory {
    public static IShape Create(string type) => type switch {
        "circle" => new Circle(),
        "square" => new Square(),
        _ => throw new ArgumentException()
    };
}

// Builder: step-by-step construction of complex objects
public class Pizza {
    public string Size { get; set; }
    public bool Cheese { get; set; }
    public bool Pepperoni { get; set; }
}
public class PizzaBuilder {
    private readonly Pizza _p = new();
    public PizzaBuilder WithSize(string s) { _p.Size = s; return this; }
    public PizzaBuilder AddCheese() { _p.Cheese = true; return this; }
    public PizzaBuilder AddPepperoni() { _p.Pepperoni = true; return this; }
    public Pizza Build() => _p;
}

var pizza = new PizzaBuilder()
    .WithSize("large").AddCheese().AddPepperoni().Build();
16

パターンマッチング(C# 7-10 以降)

型パターンと is

パターンマッチング(C# 7 以降)は型チェックと変数バインディングを1ステップで組み合わせます:'o is string s' がチェックして代入します。プロパティパターン({ Length: > 3 })はオブジェクトのプロパティをマッチします。C# 9 は 'not'、'and'、'or' コンビネータを追加します。switch 式(C# 8)は値を返しパターンを使用します—if-else チェインよりはるかにクリーンです。_ は破棄パターン(デフォルトケース)です。

csharp
object o = "hello";

// Type pattern (C# 7)
if (o is string s) {
    Console.WriteLine(s.Length);  // s is string here
}

// Combined with null check
if (o is string { Length: > 3 } longStr) {
    Console.WriteLine(longStr);  // string with length > 3
}

// Negation and conjunction (C# 9)
if (o is not null) { /* ... */ }
if (o is string and not null) { /* ... */ }

// Switch on type
string Describe(object o) => o switch {
    int i => $"Integer: {i}",
    string s => $"String: {s}",
    double d => $"Double: {d}",
    null => "null",
    _ => "Unknown"
};

プロパティと位置パターン

プロパティパターンはオブジェクトのプロパティをマッチします:{ Prop: pattern }。位置パターンは Deconstruct メソッドで分解します(レコードはこれを自動的に持ちます)。パターンは合成可能で、関係演算子(<、> など)と組み合わせられます。C# 10 はネストされたプロパティの短縮形(Name.Length の代わりに Name: { Length: })を許可します。パターンマッチングは複雑な条件ロジックを宣言的で読みやすくします。

csharp
public record Point(int X, int Y);
public record Person(string Name, int Age);

// Property pattern
string Describe(Person p) => p switch {
    { Age: < 18 } => "Minor",
    { Age: >= 65 } => "Senior",
    { Name.Length: > 10 } => "Long name",
    { } => "Adult"  // non-null Person
};

// Positional pattern (deconstruction)
string Quadrant(Point p) => p switch {
    (0, 0) => "Origin",
    ( > 0, > 0) => "Q1",
    ( < 0, > 0) => "Q2",
    ( < 0, < 0) => "Q3",
    ( > 0, < 0) => "Q4",
    _ => "On axis"
};

// Nested patterns
bool IsValid(Person p) => p is { Name: { Length: > 0 }, Age: > 0 };
// C# 10: nested property shorthand
bool IsValid2(Person p) => p is { Name.Length: > 0, Age: > 0 };

関係パターンと論理パターン(C# 9)

C# 9 は関係パターン(<、>、<=、>=)と論理コンビネータ(and、or、not)を導入しました。これにより switch 式が範囲チェックやカテゴリロジックで表現力を持ちます。'or' はパターンを組み合わせ(いずれかにマッチ)、'and' は両方を要求、'not' は否定します。括弧で優先順位をグループ化します。これは冗長な if-else チェインを宣言的で網羅的(コンパイラチェック付き)なロジックに置き換えます。

csharp
// Relational patterns: <, >, <=, >=
string Classify(int n) => n switch {
    < 0 => "Negative",
    0 => "Zero",
    > 0 and < 10 => "Small positive",
    >= 10 and < 100 => "Medium",
    >= 100 => "Large"
};

// Logical patterns: and, or, not
bool IsWeekend(DayOfWeek d) => d is DayOfWeek.Saturday or DayOfWeek.Sunday;

// Combined with type patterns
decimal GetDiscount(object customer) => customer switch {
    Student => 0.2m,
    Senior or Veteran => 0.15m,
    not null => 0.0m,
    null => throw new ArgumentNullException()
};

// Parenthesized patterns for grouping
bool IsValid(int n) => n is (> 0 and < 10) or (> 100 and < 200);

リストパターン(C# 11)

リストパターン(C# 11)は配列とリストの形状をマッチします:空の []、単一要素 [x]、正確な [a, b]、またはスライス付き [first, .., last]。スライスパターン(..)は0個以上の中間要素を変数にキャプチャします。var とガード(when)と組み合わせることで、シーケンス上の表現力豊かなマッチングが可能です。解析、検証、アルゴリズムの実装に有用です。

csharp
// List patterns match array/list shapes
int[] nums = { 1, 2, 3, 4, 5 };

string Describe(int[] arr) => arr switch {
    [] => "Empty",
    [single] => $"Single: {single}",
    [first, second] => $"Pair: {first}, {second}",
    [first, .., last] => $"First: {first}, Last: {last}",  // slice
    [.., var last] => $"Last: {last}",
    _ => "Other"
};

// Slice pattern (..) captures middle elements
if (nums is [var first, .. var middle, var last]) {
    Console.WriteLine(`First {first}, middle count {middle.Length}, last {last}`);
}

// With guards
string Classify(int[] arr) => arr switch {
    [var a, var b] when a == b => "Equal pair",
    [var a, var b] => "Unequal pair",
    _ => "Other"
};

switch 式

switch 式(C# 8)は switch 文とは異なり値を返す式です。パターンマッチング(=>)を使用しより簡潔です。タプルスイッチング((a, b) switch)で複数値を処理します。コンパイラは列挙型の網羅性をチェックします(ケースが欠けていると警告)。実行時例外を望まない限り、常にデフォルト(_)を含めてください。値を返すロジックには switch 文より switch 式が推奨されます。

csharp
// Traditional switch statement (verbose)
string Old(int n) {
    switch (n) {
        case 1: return "one";
        case 2: return "two";
        default: return "many";
    }
}

// Switch expression (C# 8): expression, returns value
string New(int n) => n switch {
    1 => "one",
    2 => "two",
    _ => "many"
};

// With multiple values per arm
string Describe(int day, bool holiday) => (day, holiday) switch {
    (6 or 7, _) => "Weekend",
    (_, true) => "Holiday",
    (1, false) => "Monday",
    _ => "Weekday"
};

// Exhaustiveness: compiler warns if not all cases covered
// (especially with enums)
17

レコードと不変性

レコードの基礎(C# 9)

レコード(C# 9)は値ベースの等値性を持つ参照型です—2つのレコードはデータが等しければ等しいとみなされます(参照の等値性を使用するクラスとは異なります)。コンパイラが Equals、GetHashCode、ToString、Deconstruct を自動生成します。'with' 式で変更したコピーを作成できます(非破壊的変更)。レコードは DTO、値オブジェクト、不変データモデルに最適です。

csharp
// Record: value-equality, immutable, concise
public record Person(string Name, int Age);

// Create
var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);

// Value equality (auto-generated)
Console.WriteLine(p1 == p2);  // true (compares properties)
Console.WriteLine(p1.Equals(p2));  // true

// ToString (auto-generated, readable)
Console.WriteLine(p1);  // Person { Name = Alice, Age = 30 }

// Deconstruction
var (name, age) = p1;

// Non-destructive mutation (with-expression)
var older = p1 with { Age = 31 };
// p1 is unchanged, older is a copy with Age = 31

レコード構造体(C# 10)

C# 10 はレコード構造体(レコード機能を持つ値型)と readonly record struct(不変値型)を追加しました。選択:record class(参照型、大きな/共有データ用)、record struct(値型、小さなデータ用、ヒープ割り当て回避)、readonly record struct(不変値型、最も安全)。レコード構造体は record class と同様に値の等値性と 'with' をサポートします。Point、Money のような小さな不変値には readonly record struct を使用してください。

csharp
// record class (default, reference type)
public record PersonRef(string Name, int Age);

// record struct (C# 10): value type with record features
public record struct Point(int X, int Y);

// readonly record struct: fully immutable value type
public readonly record struct Money(decimal Amount, string Currency);

// Mutable record struct (allowed but discouraged)
public record struct Counter(int Value) {
    public int Value { get; set; } = Value;
}

var p = new Point(1, 2);
var p2 = p with { X = 5 };  // copy with X=5
Console.WriteLine(p == p2);  // false

// Records can have additional members
public record Person(string Name) {
    public int Age { get; init; }  // extra init-only prop
    public string DisplayName => Name.ToUpper();
}

init 専用セッター

init 専用セッター(C# 9)はオブジェクト初期化中のみ設定を許可し—コンストラクタのボイラープレートなしで構築後の不変性を実現します。'required'(C# 11)は初期化子でプロパティを設定することを呼び出し側に強制します(コンパイル時チェック)。レコードはデフォルトで init セッターを使用します。これによりオブジェクト初期化子構文で不変オブジェクトパターンが可能になり、コンストラクタパラメータより読みやすくなります。

csharp
public class Config {
    // init: settable only during construction (object initializer)
    public string Name { get; init; } = "";
    public int Timeout { get; init; } = 30;
}

var c = new Config { Name = "MyApp", Timeout = 60 };
// c.Name = "Other";  // ERROR: init-only after construction

// Required properties (C# 11): must be set in initializer
public class User {
    public required string Email { get; init; }
    public string? DisplayName { get; init; }
}

// Must set Email, DisplayName optional
var u = new User { Email = "[email protected]" };

// Records use init by default (positional parameters)
public record Point(int X, int Y);
// Equivalent to: public int X { get; init; }
//                public int Y { get; init; }

レコードの継承

レコードは継承をサポートします:派生レコードはコンストラクタに基底レコードのパラメータを含めます。'with' 式は実行時の型を保持します(Animal ではなく Dog から Dog を作成)。等値性チェックは実行時の型を確認します—2つのレコードは同じ型でデータが等しい場合のみ等しいです。これによりレコードがポリモーフィックなコレクションで正しく動作します(単純な値の等値性とは異なります)。

csharp
// Base record
public record Animal(string Name);

// Derived record: must include base parameters
public record Dog(string Name, string Breed) : Animal(Name);

// Derived can add to base
public record Teacher(string Name, string Subject) : Person(Name);

var d = new Dog("Rex", "Labrador");
Console.WriteLine(d);  // Dog { Name = Rex, Breed = Labrador }

// with preserves the runtime type
var d2 = d with { Name = "Buddy" };
Console.WriteLine(d2.GetType().Name);  // Dog (not Animal)

// Value equality across hierarchy
Animal a = new Dog("Rex", "Lab");
Animal b = new Dog("Rex", "Lab");
Console.WriteLine(a == b);  // true

// Different types are not equal even with same data
Animal c = new Animal("Rex");
Console.WriteLine(a == c);  // false (different runtime types)

レコード vs クラス vs 構造体

クラスは参照の等値性(== は参照を比較)を使用し、レコードは値の等値性(データを比較)を使用し、構造体はデフォルトで値の等値性を使用しますが値型です(代入時にコピー)。不変で値セマンティクスを持つデータ(DTO、値オブジェクト、メッセージ)にはレコードを使用してください。アイデンティティを持つ可変エンティティ(User、Order)や継承階層にはクラスを使用してください。コピーされるべき小さな(16バイト以下)値(Point、Color、Money)には構造体を使用してください。

csharp
// Class: reference type, reference equality
public class PersonClass {
    public string Name { get; set; }
    public PersonClass(string name) => Name = name;
}
var c1 = new PersonClass("Alice");
var c2 = new PersonClass("Alice");
Console.WriteLine(c1 == c2);  // false (different references)

// Record: reference type, value equality
public record PersonRec(string Name);
var r1 = new PersonRec("Alice");
var r2 = new PersonRec("Alice");
Console.WriteLine(r1 == r2);  // true (value equality)

// Struct: value type, value equality (manual)
public struct PointStruct {
    public int X, Y;
    public PointStruct(int x, int y) { X = x; Y = y; }
}
var s1 = new PointStruct(1, 2);
var s2 = new PointStruct(1, 2);
Console.WriteLine(s1.Equals(s2));  // true (default struct equality)

// Choose:
// - record: immutable data with value equality (DTOs, value objects)
// - class: mutable, reference semantics, inheritance (entities, services)
// - struct: small, value type, avoid heap allocation (Point, Color)
18

パターンマッチング 深掘り

プロパティパターン

プロパティパターンはオブジェクトのプロパティに対してインラインでマッチします。switch 式は値を直接返します。合成パターン(and/or)で柔軟な条件を作成します。null と _(破棄)でエッジケースを処理します。これは冗長な if-else チェインを宣言的で読みやすいコードに置き換えます。

csharp
static decimal CalcDiscount(Customer c) => c switch
{
    { IsPremium: true, Orders.Count: > 100 } => 0.20m,
    { IsPremium: true } => 0.10m,
    { Orders.Count: > 50 } => 0.05m,
    { Country: "US" or "CA" } => 0.03m,
    null => 0m,
    _ => 0m
};

リストパターン

リストパターン(C# 11)は配列とインデックス可能なシーケンスをマッチします。[] は空、[single] は1要素、[first, .. rest] はスライスパターンで残りの要素をキャプチャします。コマンドの解析、シーケンスの検証、再帰アルゴリズムに有用です。

csharp
int Sum(int[] n) => n switch
{
    [] => 0,
    [int single] => single,
    [int first, int second] => first + second,
    [int first, .. int[] rest] => first + Sum(rest),
};

// Matching array shapes
var arr = new[] { 1, 2, 3, 4 };
// [1, ..] matches array starting with 1
// [.., 4] matches array ending with 4
// [1, 2, .., 4] matches 1,2,anything,4

タプルパターン

タプルパターンは複数の値を同時にマッチします。各ケースは値のタプルをテストします。関係パターン(>、<、>=)と論理パターン(and、or、not)と組み合わせることで、ネストされた if 文なしの強力な多次元マッチングが作成できます。

csharp
static string Classify(int t, bool rain) => (t, rain) switch
{
    (< 0, _) => "Freezing",
    (>= 0 and < 15, true) => "Cold and wet",
    (>= 0 and < 15, false) => "Cold",
    (>= 15 and < 25, _) => "Mild",
    (>= 25, true) => "Hot and wet",
    (>= 25, false) => "Hot",
};

型パターンとガード

型パターンは実行時の型をマッチします。when 句で追加のガードを追加します。var は何にでも(null を含む)マッチします。順序が重要です:より具体的なパターンを汎用的なものの前に配置してください。これがモダンな C# で型ベースのディスパッチを行う慣用的な方法です。

csharp
object Process(object input) => input switch
{
    int i when i > 0 => $"Positive: {i}",
    int i => $"Non-positive: {i}",
    string s when s.Length > 10 => s.Substring(0, 10),
    string s => s,
    IList list => $"List with {list.Count} items",
    var x => x?.ToString() ?? "null"
};

ネストされたパターン

パターンは任意の深さにネストできます。パターン内の var はマッチした値をキャプチャし、ガードや式本体で使用できます。この例は Line レコード内の Point プロパティをマッチします。ネストされたパターンは複雑なオブジェクトグラフを簡潔に検証するのに強力です。

csharp
record Point(int X, int Y);
record Line(Point Start, Point End);

string Describe(Line line) => line switch
{
    Line({ X: 0, Y: 0 }, { X: 0, Y: 0 }) => "Point at origin",
    Line({ X: var x1, Y: var y1 }, { X: var x2, Y: var y2 })
        when x1 == x2 && y1 == y2 => "Degenerate line",
    Line({ X: var x1 }, { X: var x2 }) when x1 == x2 => "Vertical",
    Line({ Y: var y1 }, { Y: var y2 }) when y1 == y2 => "Horizontal",
    _ => "Diagonal line"
};
19

レコードと With 式

レコードの値の等値性

レコードはデフォルトで値ベースの等値性を提供します:同じデータを持つ2つのインスタンスは等しいです。コンパイラが Equals、GetHashCode、ToString、== 演算子を生成します。レコードは参照型ですが不変性向けに設計されています。DTO、値オブジェクト、コンテンツで比較すべきデータに使用してください。

csharp
public record Person(string Name, int Age);

var p1 = new Person("Alice", 30);
var p2 = new Person("Alice", 30);
Console.WriteLine(p1 == p2);  // True (value equality)
Console.WriteLine(p1.GetHashCode() == p2.GetHashCode()); // True
Console.WriteLine(p1);  // Person { Name = Alice, Age = 30 }

With 式

with 式はレコードの変更したコピーを作成します。元のレコードは変更されません(非破壊的変更)。これが不変データを更新する慣用的な方法です。内部ではコンパイラが protected コピー コンストラクタと init 専用セッターを使用します。

csharp
var alice = new Person("Alice", 30);
var alice31 = alice with { Age = 31 };
var aliceMarried = alice with { Name = "Alice Smith" };

// Original is unchanged (immutable)
Console.WriteLine(alice.Age);      // 30
Console.WriteLine(alice31.Age);    // 31
Console.WriteLine(aliceMarried.Name); // Alice Smith

位置指定 vs init 専用

位置指定レコードはプライマリコンストラクタ構文を使用し、分解メソッドを生成します。init 専用レコードはオブジェクト初期化子構文を使用し、デフォルト値と required 修飾子を許可します。シンプルな値型には位置指定を、オプションや計算プロパティを持つ複雑なレコードには init 専用を選択してください。

csharp
// Positional record (primary constructor)
public record Point(int X, int Y);

// Init-only record (more flexible properties)
public record Employee
{
    public required string Name { get; init; }
    public decimal Salary { get; init; }
    public string Dept { get; init; } = "General";
}

var emp = new Employee { Name = "Bob", Salary = 50000 };
var promoted = emp with { Salary = 60000 };

レコード構造体と継承

レコード構造体は値型(代入時にコピー)で、他のレコード構造体から継承できません。参照型のレコードは継承をサポートします。readonly record struct は変更を防ぎます。小さな不変値には record struct を、参照セマンティクスと継承の恩恵を受ける大きなオブジェクトには record class を使用してください。

csharp
// Record struct (value type)
public readonly record struct Money(decimal Amount, string Currency);

// Record inheritance (reference type only)
public record Animal(string Name);
public record Dog(string Name, string Breed) : Animal(Name);

var dog = new Dog("Rex", "Labrador");
Console.WriteLine(dog is Animal);  // True
var copied = dog with { Breed = "Poodle" };

Deconstruct とマッチング

位置指定レコードは Deconstruct メソッドを自動生成し、タプル分解を可能にします。これはパターンマッチングとシームレスに統合します:レコードのプロパティを位置または名前でマッチできます。分解は var パターン、タプルパターン、switch 式で機能します。

csharp
var person = new Person("Alice", 30);
var (name, age) = person;  // Deconstruct
Console.WriteLine($"{name} is {age}");

// Works with pattern matching
string Greet(Person p) => p switch
{
    ("Alice", _) => "Hi Alice!",
    (_, < 18) => "Hello young one",
    var (n, a) => $"Hello {n}, age {a}"
};
20

ソースジェネレータ

基本的なソースジェネレータ

ソースジェネレータはコンパイル時に実行され、コンパイルに C# ソースファイルを追加します。IIncrementalGenerator がモダンな API(C# 9 以降)です。RegisterPostInitializationOutput はユーザーコードを解析せずに静的コードを追加します。ジェネレータは読み取り専用です:ファイルを追加できますが既存のコードは変更できません。

csharp
[Generator]
public class HelloGenerator : IIncrementalGenerator
{
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
        context.RegisterPostInitializationOutput(ctx =>
        {
            ctx.AddSource("Hello.g.cs", """
                namespace Generated;
                public static class Hello
                {
                    public static string World => "Hello!";
                }
                """);
        });
    }
}

属性からの生成

ForAttributeWithMetadataName は特定の属性でマークされた型を見つけます。パイプライン:構文プロバイダがノードをフィルタ、transform がデータを抽出、Collect が結果をバッチ、RegisterSourceOutput がコードを生成。このインクリメンタルパイプラインは結果をキャッシュし、入力が変更された場合のみ再生成し、ビルドを高速に保ちます。

csharp
// Find types marked with an attribute
context.SyntaxProvider.ForAttributeWithMetadataName(
    "MyApp.AutoToStringAttribute",
    predicate: (node, _) => node is ClassDeclarationSyntax,
    transform: (ctx, _) => (ClassDeclarationSyntax)ctx.TargetNode)
    .Collect()
    .RegisterSourceOutput((spc, classes) =>
    {
        foreach (var cls in classes)
            GenerateToString(spc, cls);
    });

StringBuilder 出力

ジェネレータはソースを文字列として生成します。StringBuilder は複数行出力に効率的です。ジェネレータがメンバーを追加するためには対象クラスが partial である必要があります。生成されたファイルは IDE の Dependencies > Analyzers の下に表示されます。慣例として生成ファイルを区別するため .g.cs サフィックスを使用してください。

csharp
void GenToString(SourceProductionContext ctx, ClassDeclarationSyntax cls)
{
    var sb = new StringBuilder();
    sb.AppendLine($"namespace {cls.ContainingNamespace};");
    sb.AppendLine($"public partial class {cls.Identifier}");
    sb.AppendLine("{");
    sb.AppendLine("  public override string ToString()");
    sb.AppendLine("  {");
    var props = cls.Members.OfType<PropertyDeclarationSyntax>();
    sb.AppendLine($"    return $"{cls.Identifier}: " +
      string.Join(", ", props.Select(p => $"{p.Identifier}={{{p.Identifier}}}")) + "";");
    sb.AppendLine("  }");
    sb.AppendLine("}");
    ctx.AddSource($"{cls.Identifier}_ToString.g.cs", sb.ToString());
}

JsonSerializerContext

System.Text.Json にはシリアライズロジックを事前コンパイルする組み込みソースジェネレータが含まれ、実行時リフレクションを排除します。これによりパフォーマンスが向上し、AOT コンパイル(Native AOT)をサポートします。JsonSerializable で型を宣言し、生成されたコンテキストをシリアライズ/デシリアライズ呼び出しに使用してください。

csharp
[JsonSourceGenerationOptions(
    PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase,
    WriteIndented = true)]
[JsonSerializable(typeof(List<Person>))]
[JsonSerializable(typeof(Dictionary<string, object>))]
public partial class AppJsonContext : JsonSerializerContext { }

// Usage (no runtime reflection):
var json = JsonSerializer.Serialize(people, AppJsonContext.Default.ListPerson);
var data = JsonSerializer.Deserialize(json, AppJsonContext.Default.ListPerson);

一般的なユースケース

人気のあるジェネレータのユースケース:DTO 生成、DI 登録、強く型付けされた設定、AutoMapper プロファイル、ログテンプレート、JSON シリアライズ。MediatR、AutoMapper、Microsoft.Extensions.Logging などのライブラリがボイラープレートを削減し AOT 互換性を向上させるためにジェネレータを使用しています。

csharp
// 1. DTO generation from interfaces
public interface IUserDto { string Name { get; } int Age { get; } }
// Generator creates: public class UserDto : IUserDto { ... }

// 2. Dependency injection registration
[RegisterService(ServiceLifetime.Scoped)]
public class MyService : IService { }
// Generator creates: services.AddScoped<MyService>();

// 3. Strongly-typed configuration
[ConfigurationSection("App:Db")]
public class DbSettings { public string Conn { get; set; } }
// Generator creates: services.Configure<DbSettings>(...)
21

Span と Memory

Span の基礎

Span<T> はコピーせずに連続したメモリに対するタイプセーフでメモリセーフなビューを提供します。配列、stackalloc メモリ、アンマネージメモリ、文字列をラップできます。Span は ref struct(スタック専用)なので、ボックス化、フィールドに格納、ラムダでキャプチャできません。高性能なスライシングと解析に使用してください。

csharp
// Span<T> is a stack-only view over contiguous memory
Span<int> numbers = stackalloc int[] { 1, 2, 3, 4, 5 };

// From array
int[] arr = { 10, 20, 30, 40, 50 };
Span<int> slice = arr.AsSpan(1, 3);  // [20, 30, 40]
slice[0] = 99;  // Modifies original array

// From string
ReadOnlySpan<char> text = "Hello, World".AsSpan();
ReadOnlySpan<char> hello = text.Slice(0, 5);  // "Hello"

非同期用 Memory

Memory<T> は Span<T> のヒープセーフな対応物です。フィールドに格納、ラムダでキャプチャ、await 境界を越えて使用できます。実際の作業時には .Span で Span<T> に変換します。非同期 API には Memory を、同期ホットパスには Span を使用してください。

csharp
// Memory<T> can live on the heap and cross async boundaries
async Task ProcessAsync(Memory<byte> buffer)
{
    await Task.Delay(100);
    Span<byte> span = buffer.Span;
    for (int i = 0; i < span.Length; i++)
        span[i] = (byte)(span[i] * 2);
}

byte[] data = new byte[1024];
await ProcessAsync(data);  // Implicit conversion to Memory<byte>

割り当ての回避

Span は解析とスライシング中の中間文字列割り当てを排除します。大きな入力を処理するホットパス(HTTP 解析、JSON、ログファイル)では、GC 負荷を劇的に削減できます。int.TryParse などのプリミティブには Span オーバーロードがあります。実際に文字列が必要な場合のみ ToString() を呼び出してください。

csharp
// Traditional (allocates new string)
string Extract(string input, int start, int len)
    => input.Substring(start, len);

// Zero-allocation with Span
ReadOnlySpan<char> ExtractFast(ReadOnlySpan<char> input, int start, int len)
    => input.Slice(start, len);

// Parse without allocating substrings
bool TryParseInt(ReadOnlySpan<char> span, out int result)
    => int.TryParse(span, out result);

stackalloc とアンマネージ

stackalloc はコールスタックに割り当てます(自動解放、GC 負荷なし)が小さなサイズに制限されます。大きなバッファには NativeMemory.Alloc/Free(C# 7 以降)または Marshal.AllocHGlobal を使用してください。アンマネージメモリは常に Span でラップし、リークを防ぐため finally ブロックで解放してください。

csharp
// Stack allocation (small, fast, auto-freed)
Span<int> buffer = stackalloc int[128];
for (int i = 0; i < buffer.Length; i++)
    buffer[i] = i;

// Unmanaged memory (large buffers, manual lifetime)
unsafe
{
    int* ptr = (int*)NativeMemory.Alloc(1024, sizeof(int));
    try
    {
        Span<int> span = new Span<int>(ptr, 1024);
        span.Fill(42);
    }
    finally
    {
        NativeMemory.Free(ptr);
    }
}

Span フレンドリーな API

バッファには Span<T> または ReadOnlySpan<T> を受け取るよう API を設計し、配列割り当てを回避してください。呼び出し側が stackalloc、配列、アンマネージメモリのいずれを使用するかを決定します。Span.TryWrite(C# 10 以降)は文字列を割り当てずにスパンに直接フォーマットし、ログやシリアライズに有用です。

csharp
// Writing to a Span buffer
int WriteValues(Span<int> dest)
{
    int count = 0;
    foreach (var v in new[] { 1, 2, 3 })
    {
        if (count >= dest.Length) break;
        dest[count++] = v;
    }
    return count;
}

// Span-based string formatting (C# 10+)
Span<char> dest = stackalloc char[64];
bool ok = dest.TryWrite($"Value: {42:N2}", out int charsWritten);
Console.WriteLine(dest.Slice(0, charsWritten).ToString());
22

補間文字列 深掘り

書式指定子

文字列補間($"...")は中括弧内に式を埋め込みます。コロン後の書式指定子が出力を制御します:C は通貨、F は固定小数点、X は16進数、D はパディング付き桁数、P はパーセント、yyyy-MM-dd は日付。コンパイラはこれを string.Format または FormattableString に変換します。

csharp
string name = "Alice";
int age = 30;
decimal price = 19.99m;
DateTime now = DateTime.Now;

Console.WriteLine($"Name: {name}, Age: {age}");
Console.WriteLine($"Price: {price:C}");  // $19.99
Console.WriteLine($"Date: {now:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine($"Hex: {255:X}");  // FF
Console.WriteLine($"Padded: {age:D5}");  // 00030
Console.WriteLine($"Percent: {0.85:P0}");  // 85%

生文字列補間

逐語的補間文字列($@)はバックスラッシュをそのまま保持し、複数行コンテンツを許可します。生文字列補間(C# 11)は三重引用符を使用し、すべてのエスケープを排除します:中括弧や引用符のエスケープが不要です。$ の数が補間に必要な中括弧の数を制御します。

csharp
// Verbatim interpolated ($@)
string path = $@"C:\Users\{name}\Documents";
string json = $@"{{
  ""name"": ""{name}"",
  ""age"": {age}
}}";

// Raw string interpolation (C# 11)
string query = $$"""
    SELECT * FROM Users
    WHERE Name = '{{name}}'
    AND Age > {{age - 5}}
    """;
// $$ means {{ }} for interpolation, no escaping needed

FormattableString とカルチャ

FormattableString は補間構造を保持し、特定のカルチャで遅延フォーマットを可能にします。これは現在のカルチャに依存すべきでないライブラリに重要です。FormattableString.Invariant はインバリアントカルチャを強制し、シリアライズとログでのロケール固有の問題を防ぎます。

csharp
// FormattableString captures the format for deferred execution
FormattableString msg = $"Hello {name}, you are {age} years old";

// Explicit culture (avoid CurrentCulture issues in libraries)
string german = msg.ToString(CultureInfo.GetCultureInfo("de-DE"));
string invariant = msg.ToString(CultureInfo.InvariantCulture);

// Invariant by default
string formatted = FormattableString.Invariant($"Total: {price}");

補間文字列ハンドラ

補間文字列ハンドラ(C# 10)はコンパイラが補間コンポーネントを直接メソッドに渡すことを可能にし、結果が不要な場合の文字列割り当てを回避します。これが高性能なログの仕組みです:ログレベルが無効なら文字列は構築されません。ハンドラはリテラルとフォーマットされた値を別々に受け取ります。

csharp
// Custom handler (C# 10+) for conditional allocation
[InterpolatedStringHandler]
public ref struct LogStringHandler
{
    public LogStringHandler(int literal, int hole, Logger logger, out bool enabled)
    {
        enabled = logger.IsEnabled;
        // Skip allocation if logging is disabled
    }
    public void AppendLiteral(string s) { /* ... */ }
    public void AppendFormatted<T>(T value) { /* ... */ }
}

// Usage: no string allocation if log level is off
logger.Info($"Processing {item} with {count} items");

Span ベースのフォーマット

Span.TryWrite は補間文字列をスタック割り当てバッファに直接フォーマットし、ヒープ割り当てを完全に回避します。string.Create に補間文字列とカルチャを渡すと、中間割り当てなしでカルチャ対応フォーマットが可能です。これらは高スループットアプリケーションのホットパスに不可欠な技術です。

csharp
// Format directly into a buffer (C# 10+)
Span<char> buffer = stackalloc char[256];

if (buffer.TryWrite($"User: {name}, Age: {age}", out int chars))
{
    var result = buffer[..chars];
    Console.WriteLine(result);
}

// Concatenation with DefaultInterpolatedStringHandler
string built = string.Create(CultureInfo.InvariantCulture, $"Total={price}");
23

ファイルスコープ名前空間

ファイルスコープ構文

ファイルスコープ名前空間(C# 10)は中括弧の代わりにセミコロンでファイル全体の名前空間を宣言します。これによりインデントレベルが1つ減り、コードがフラットで読みやすくなります。ファイルにつき1つのファイルスコープ名前空間のみ許可されます。ファイル内のすべての型がその名前空間に属します。

csharp
// Traditional (adds a level of indentation)
namespace MyApp.Services
{
    public class UserService
    {
        public void Process() { }
    }
}

// File-scoped (C# 10+, no indentation)
namespace MyApp.Services;

public class UserService
{
    public void Process() { }
}

グローバル using

グローバル using(C# 10)は1つのファイルで宣言するとプロジェクトの全ファイルに適用されます。ファイルスコープ名前空間と組み合わせてクリーンで最小限のボイラープレートを実現します。.NET SDK プロジェクトは一般的な名前空間のグローバル using を自動生成します(ImplicitUsings)。順序:グローバル using、ファイル using、名前空間。

csharp
// Global usings (apply to all files in the project)
global using System.Collections.Generic;
global using System.Linq;

// File-scoped namespace with usings
using System.Text.Json;
using MyApp.Models;

namespace MyApp.Api;

public class UserController
{
    // Can use List, LINQ, Json, Models without per-file usings
}

ネストされた名前空間

ファイルスコープ名前空間は階層にドット区切り名を使用します:MyApp.Services.Authentication はネストされた名前空間ブロックと同等です。複数のファイルが同じ名前空間を共有できます。同じファイル内で異なる名前空間に型をネストすることはできません。代わりに別々のファイルを使用してください。

csharp
// File-scoped: flat name with dots
namespace MyApp.Services.Authentication;

public class TokenService { }

// Multiple files can use the same namespace
// File 1: namespace MyApp.Services;
// File 2: namespace MyApp.Services;  // Same namespace

プロジェクト設定

プロジェクトファイルの ImplicitUsings は一般的な System 名前空間のグローバル using を自動生成します。ファイルスコープ名前空間と組み合わせることでほとんどのボイラープレートを排除します。Nullable は null 許容参照型を有効にします。これらは新しい .NET 6 以降のプロジェクトのデフォルトです。

csharp
<!-- .csproj -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
</Project>

<!-- ImplicitUsings generates:
     global using System;
     global using System.Collections.Generic;
     global using System.IO;
     global using System.Linq;
     global using System.Net.Http;
     global using System.Threading;
     global using System.Threading.Tasks;
-->

移行とベストプラクティス

.editorconfig でファイルスコープ名前空間を強制してください(IDE0161)。IDE は従来形式からファイルスコープへのワンクリック移行を提供します。ベストプラクティス:ファイルにつき1つの public 型、ファイル名を型名に一致させる。これによりファイルが小さく保たれナビゲーションが容易になります。

csharp
// .editorconfig to enforce file-scoped namespaces
[*.{cs,vb}]
dotnet_diagnostic.IDE0161.severity = error  // Prefer file-scoped

// Migration: use the IDE fixer to convert
// Right-click > Quick Actions > Convert to file-scoped namespace

// Best practice: one type per file
// UserService.cs
namespace MyApp.Services;
public class UserService { }
24

必須メンバー

必須プロパティ

required 修飾子(C# 11)はオブジェクト作成中にプロパティを初期化することを呼び出し側に強制します。required メンバーが設定されていない場合、コンパイラは CS9035 を発行します。これは必須プロパティのコンストラクタ検証を置き換えます。init 専用セッターとオブジェクト初期化子で機能し、コンパイル時の保証を提供します。

csharp
public class User
{
    public required Guid Id { get; init; }
    public required string Name { get; init; }
    public required string Email { get; init; }
    public string? Phone { get; init; }  // Optional
}

// Compiler enforces initialization
var user = new User
{
    Id = Guid.NewGuid(),
    Name = "Alice",
    Email = "[email protected]"
};

// Error CS9035: Required member 'Name' must be set
// var bad = new User { Id = Guid.NewGuid() };

SetsRequiredMembers

SetsRequiredMembers 属性はコンストラクタがすべての必須メンバーを初期化することをコンパイラに伝え、呼び出し側がオブジェクト初期化子なしでコンストラクタを使用できるようにします。これは必須プロパティと従来のコンストラクタのギャップを埋めます。この属性は実行時に検証しません。コンパイル時の約束です。

csharp
public class Configuration
{
    public required string ConnectionString { get; init; }
    public required int Port { get; init; }

    [SetsRequiredMembers]
    public Configuration(string conn, int port)
    {
        ConnectionString = conn;
        Port = port;
    }
}

// Both work: constructor or initializer
var c1 = new Configuration("localhost", 5432);
var c2 = new Configuration
{
    ConnectionString = "localhost",
    Port = 5432
};

レコードと required

必須メンバーはレコードで機能します。位置指定レコードの場合、required プロパティはプライマリコンストラクタではなく本体で宣言します。with 式は必須値を自動的に保持します。デフォルト値(OpenedAt など)で required 修飾子なしでプロパティをオプションにできます。

csharp
public record Account
{
    public required string AccountNumber { get; init; }
    public required decimal Balance { get; init; }
    public DateTime OpenedAt { get; init; } = DateTime.UtcNow;
}

// Positional record with required
public record Customer(string Name)
{
    public required string Email { get; init; }
}

var cust = new Customer("Alice") { Email = "[email protected]" };

init vs required vs set

init は構築中のみプロパティを設定可能にします(以降は不変)。required は初期化を強制します。両方(required + init)で必須の不変プロパティを作成します。required + set は構築後の変更を許可します。作成後に値を変更すべきかどうかに基づいて選択してください。

csharp
// Init-only: can be set in initializer, but not required
public class Settings
{
    public string Name { get; init; } = "Default";
}

// Required + init: must be set, immutable after
public class SecureSettings
{
    public required string ApiKey { get; init; }
}

// Required + set: must be set, but can be modified later
public class MutableConfig
{
    public required string Environment { get; set; }
}

JSON シリアライズ

System.Text.Json は required 修飾子を尊重します:JSON に必須プロパティがない場合、デシリアライズは JsonException をスローします。これによりコンパイル時コントラクトに一致する実行時検証を提供します。init と組み合わせて、すべての必須フィールドが確実に設定された不変 DTO を実現します。

csharp
using System.Text.Json;

public class ApiResponse
{
    public required string Status { get; init; }
    public required int Code { get; init; }
    public string? Message { get; init; }
}

// Deserialization enforces required at runtime
var json = """{"Status":"OK","Code":200}""";
var response = JsonSerializer.Deserialize<ApiResponse>(json);

// Throws JsonException if required member is missing
// var bad = JsonSerializer.Deserialize<ApiResponse>("""{"Code":200}""");
25

コレクション式

コレクション式の構文

コレクション式(C# 12)は任意のコレクション型を初期化するための統一構文 [..] を提供します。コンパイラが型を推論し適切なビルダーを使用します。これは冗長な new List<int> { 1, 2, 3 } 構文を置き換えます。配列、List<T>、Span<T>、IEnumerable<T>、および CollectionBuilder 属性を持つカスタムコレクションで機能します。

csharp
// C# 12 collection expressions
int[] numbers = [1, 2, 3, 4, 5];
List<string> names = ["Alice", "Bob", "Charlie"];
Span<int> span = [10, 20, 30];
IEnumerable<int> seq = [1, 2, 3];

// Empty collections
int[] empty = [];
List<int> emptyList = [];

// Works with any collection type that has a builder
HashSet<string> set = ["a", "b", "c"];

スプレッド演算子

スプレッド演算子(..)はコレクションを外側のコレクション式にフラット化します。これは Concat、AddRange、手動ループを置き換えます。スプレッドと個別要素を混在できます。スプレッドは任意の IEnumerable<T> で機能し、複数のデータソースを簡潔に組み合わせられます。

csharp
int[] a = [1, 2, 3];
int[] b = [4, 5, 6];

// Spread elements with ..
int[] combined = [..a, ..b, 7, 8];  // [1,2,3,4,5,6,7,8]

// Mix spreads and literals
List<int> result = [0, ..a, 100, ..b];

// Spread any IEnumerable
List<int> evens = [.. Enumerable.Range(0, 10).Where(x => x % 2 == 0)];

戻り値とパラメータパターン

コレクション式は return 文、引数、代入で機能します。コンパイラがターゲット型に自動的に変換します。条件付きスプレッド(.. を伴う三項)で要素をオプションで含められます。これにより複数のオプションソースからのコレクション構築がクリーンで読みやすくなります。

csharp
// Return collection expressions
List<int> GetNumbers() => [1, 2, 3];
int[] GetArray() => [10, 20];

// Pass as arguments
void Process(IEnumerable<int> data) { }
Process([1, 2, 3]);

// Conditional spread
bool includeExtra = true;
var items = [
    1, 2, 3,
    .. includeExtra ? [4, 5] : []
];

カスタムコレクションビルダー

CollectionBuilder 属性はコレクション型をファクトリメソッドに接続し、コレクション式サポートを有効にします。Create メソッドは要素の ReadOnlySpan<T> を受け取ります。これによりサードパーティコレクションが統一された [..] 構文に参加できます。ビルダーパターンにより初期化が効率的に保たれます。

csharp
[CollectionBuilder(typeof(MyCollection), "Create")]
public class MyCollection<T> : IEnumerable<T>
{
    private readonly List<T> _items = new();
    public IEnumerator<T> GetEnumerator() => _items.GetEnumerator();

    public static MyCollection<T> Create(ReadOnlySpan<T> items)
    {
        var col = new MyCollection<T>();
        foreach (var item in items) col._items.Add(item);
        return col;
    }
}

// Now usable with collection expressions
MyCollection<int> custom = [1, 2, 3, 4];

params コレクション

C# 12 は配列だけでなく任意のコレクション型で params を許可します。コレクション式を params メソッドに渡せます。ターゲットが Span<T> の場合、コンパイラは小さなコレクションにスタック割り当てを使用する可能性があり、ヒープ割り当てを完全に回避します。これにより可変長メソッドが人間工学的かつ効率的になります。

csharp
// C# 12: params with collection expressions
void Log(params string[] messages)
{
    foreach (var m in messages) Console.WriteLine(m);
}

// Traditional params call
Log("Error", "Warning", "Info");

// Collection expression call (C# 12)
Log(["Error", "Warning", "Info"]);

// params with Span for zero-allocation
void ProcessFast(params Span<int> values) { }
ProcessFast([1, 2, 3]);  // May use stackalloc internally

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.