変数・型・null 許容
基礎struct は値型でスタックに割り当てられ、コピーセマンティクスを持つ。class は参照型でヒープに割り当てられ、参照セマンティクスを持つ。
// 値型(struct)
struct Point { public int X; public int Y; }
var p1 = new Point { X = 1, Y = 2 };
var p2 = p1; // コピー
p2.X = 99;
Console.WriteLine(p1.X); // 1(p1 は影響を受けない)
// 参照型(class)
class Box { public int Value; }
var b1 = new Box { Value = 1 };
var b2 = b1; // 参照コピー
b2.Value = 99;
Console.WriteLine(b1.Value); // 99(同じオブジェクトを指す)var はコンパイル時に型が確定するローカル変数の型推論。dynamic は実行時に型が解決される(パフォーマンスに注意)。
var name = "Alice"; // string
var count = 42; // int
var items = new List<int>(); // List<int>
// 明示的型も同義
string name2 = "Bob";
// dynamic(型チェックが実行時)
dynamic d = 10;
d = "now a string"; // OK(実行時エラーにはならない)C# 8 から nullable reference types が導入された。? を付けることで「null を許容する」意図を明示し、コンパイラが null 安全性を静的解析する。
#nullable enable
string nonNull = "hello"; // null 代入するとワーニング
string? nullable = null; // OK
// null 合体演算子
string result = nullable ?? "default";
// null 条件演算子
int? len = nullable?.Length;
// null 免除演算子(null でないことを保証するとき)
string forced = nullable!; // CS8600 ワーニングを抑制! 演算子の乱用は NullReferenceException の温床になる。使用は慎重に。_ で桁区切り、0x で 16 進数、0b で 2 進数を表現できる。サフィックスで型を指定する。
int million = 1_000_000;
int hex = 0xFF_EC_D8_12;
int binary = 0b_1010_0101;
long big = 9_876_543_210L;
double pi = 3.141_592_653;
float f = 1.5f;
decimal d = 19.99m;
Console.WriteLine(million); // 1000000
Console.WriteLine(hex); // 4293820434制御フロー
基礎C# 8 で導入された switch 式は値を返す。アーム(=>)形式で簡潔に書ける。パターンマッチと組み合わせると強力。
string GetLabel(int score) => score switch
{
>= 90 => "A",
>= 80 => "B",
>= 70 => "C",
_ => "F", // default
};
Console.WriteLine(GetLabel(85)); // B
// 型パターン
object obj = 3.14;
string desc = obj switch
{
int i => `整数: ${i}`,
double d => `実数: ${d}`,
string s => `文字列: ${s}`,
_ => "不明",
};
Console.WriteLine(desc); // 実数: 3.14基本的な条件分岐。三項演算子 ? : は式として値を返す。
int x = 10;
if (x > 5)
Console.WriteLine("大きい");
else if (x == 5)
Console.WriteLine("等しい");
else
Console.WriteLine("小さい");
// 三項演算子
string sign = x >= 0 ? "正" : "負";
Console.WriteLine(sign); // 正for はインデックスループ、foreach はシーケンス走査、while/do-while は条件ループ。
// for
for (int i = 0; i < 3; i++)
Console.Write(`${i} `); // 0 1 2
Console.WriteLine();
// foreach
int[] arr = [10, 20, 30];
foreach (var v in arr)
Console.Write(`${v} `); // 10 20 30
Console.WriteLine();
// while
int n = 3;
while (n > 0)
Console.Write(`${n--} `); // 3 2 1
Console.WriteLine();goto は通常避けるべきだが、多重ネストループから抜け出す際に限定的に使われる。C# では switch の fallthrough にも必要。
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
if (i == 1 && j == 1)
goto done;
Console.WriteLine(`(${i},${j})`);
}
}
done:
Console.WriteLine("完了");goto の多用はコードの可読性を著しく下げる。通常は bool フラグや抽出メソッドで代替する。メソッド
基礎同じ名前で異なるシグネチャ(引数型・数)を持つメソッドを定義できる。戻り値型だけの違いはオーバーロードにならない。
static int Add(int a, int b) => a + b;
static double Add(double a, double b) => a + b;
static string Add(string a, string b) => a + b;
Console.WriteLine(Add(1, 2)); // 3
Console.WriteLine(Add(1.5, 2.5)); // 4
Console.WriteLine(Add("foo", "bar")); // foobarデフォルト引数はメソッド定義時に省略値を指定。名前付き引数は呼び出し時に引数名を明示し順序を変えられる。
static void Greet(string name, string greeting = "Hello", bool upper = false)
{
var msg = `${greeting}, ${name}!`;
Console.WriteLine(upper ? msg.ToUpper() : msg);
}
Greet("Alice"); // Hello, Alice!
Greet("Bob", "Hi"); // Hi, Bob!
Greet("Carol", upper: true); // HELLO, CAROL!
Greet(greeting: "Hey", name: "Dave"); // Hey, Dave!ref:呼び出し元変数への参照渡し(初期化必須)。out:メソッド内で代入して返す(初期化不要)。in:読み取り専用の参照渡し(コピーを避けつつ変更不可)。
static void Swap(ref int a, ref int b)
{
(a, b) = (b, a);
}
static bool TryParse(string s, out int result)
{
return int.TryParse(s, out result);
}
int x = 1, y = 2;
Swap(ref x, ref y);
Console.WriteLine(`${x}, ${y}`); // 2, 1
if (TryParse("42", out int n))
Console.WriteLine(n); // 42params キーワードで任意個数の引数を配列として受け取れる。メソッドシグネチャの最後のパラメータにのみ付与可能。
static int Sum(params int[] numbers)
{
int total = 0;
foreach (var n in numbers) total += n;
return total;
}
Console.WriteLine(Sum(1, 2, 3)); // 6
Console.WriteLine(Sum(10, 20, 30, 40)); // 100
Console.WriteLine(Sum()); // 0
// 配列を直接渡すことも可能
int[] arr = [5, 6, 7];
Console.WriteLine(Sum(arr)); // 18配列・スパン
基礎多次元配列は [,] で矩形行列、ジャグ配列(配列の配列)は [][] で各行の長さを変えられる。
// 1次元
int[] a = [1, 2, 3];
Console.WriteLine(a[1]); // 2
// 多次元(矩形)
int[,] matrix = { {1,2}, {3,4} };
Console.WriteLine(matrix[1, 0]); // 3
// ジャグ配列
int[][] jagged = new int[3][];
jagged[0] = [1];
jagged[1] = [2, 3];
jagged[2] = [4, 5, 6];
Console.WriteLine(jagged[2][1]); // 5Span はスタック上のスライスを表す構造体。ヒープ割り当てなしに配列の一部を参照できる。Memory は非同期メソッドでも使える heap ベースの版。
int[] source = [0, 1, 2, 3, 4];
// スライス(コピーなし)
Span<int> span = source.AsSpan(1, 3); // [1, 2, 3]
span[0] = 99;
Console.WriteLine(source[1]); // 99(元配列が変わる)
// スタック割り当て
Span<byte> buf = stackalloc byte[128];
buf.Fill(0xFF);
Console.WriteLine(buf[0]); // 255C# 12 でコレクション式 [...] が導入。配列・List・Span など多くのコレクション型に統一構文を使える。.. でスプレッド展開も可能。
int[] a = [1, 2, 3];
List<int> b = [4, 5, 6];
// スプレッド演算子
int[] merged = [..a, ..b, 7];
Console.WriteLine(string.Join(", ", merged));
// 1, 2, 3, 4, 5, 6, 7
// Span にも使える
Span<int> s = [10, 20, 30];
Console.WriteLine(s[1]); // 20文字列
基礎$"..." でブレース内の式を文字列に埋め込む。フォーマット指定子や整列指定子も使える。
string name = "Alice";
int age = 30;
double pi = 3.14159;
Console.WriteLine(`Hello, ${name}! Age: ${age}`);
Console.WriteLine(`Pi = ${pi:F2}`); // 小数点以下2桁
Console.WriteLine(`${name,-10}|${age,5}`); // 整列"""...""" で囲むとエスケープ不要の raw 文字列。JSON や正規表現の記述が簡潔になる。$"""...""" で補間と組み合わせ可能。
// エスケープ不要
string json = """
{
"name": "Alice",
"path": "C:\\Users\\Alice"
}
""";
// 補間 raw 文字列
string user = "Bob";
string msg = $"""
Hello, {user}!
Path: C:\Users\{user}
""";
Console.WriteLine(msg);ループ内で文字列を連結する際は StringBuilder を使う。+ 演算子の連続使用は都度新しい文字列オブジェクトを生成するため非効率。
using System.Text;
var sb = new StringBuilder();
for (int i = 0; i < 5; i++)
{
sb.Append(i);
if (i < 4) sb.Append(", ");
}
Console.WriteLine(sb.ToString()); // 0, 1, 2, 3, 4
sb.Insert(0, "Items: ");
sb.Replace("0", "zero");
Console.WriteLine(sb.ToString()); // Items: zero, 1, 2, 3, 4string.Format は書式文字列と引数を組み合わせる古典的な方法。String.Create はバッファを直接操作し高パフォーマンスな文字列生成に使う。
// string.Format
string s = string.Format("{0,-10} | {1:C}", "Apple", 1.5);
Console.WriteLine(s); // Apple | ¥1.50
// String.Create(.NET 6+)
int len = 5;
string result = string.Create(len, 0, (buf, _) =>
{
for (int i = 0; i < buf.Length; i++)
buf[i] = (char)('A' + i);
});
Console.WriteLine(result); // ABCDE例外処理
基礎try ブロックで例外が発生すると対応する catch へジャンプ。finally は例外の有無にかかわらず必ず実行される。throw で再スローできる。
try
{
int[] arr = [1, 2, 3];
Console.WriteLine(arr[5]); // IndexOutOfRangeException
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine(`配列外: ${ex.Message}`);
}
catch (Exception ex)
{
Console.WriteLine(`その他: ${ex.Message}`);
throw; // スタックトレースを保持して再スロー
}
finally
{
Console.WriteLine("finally 実行");
}catch (Exception e) when (条件) で条件付きの catch ができる。条件が false のときはその catch ブロックをスキップし、スタックを巻き戻さない(デバッグに有利)。
static void Process(int code)
{
try
{
throw new InvalidOperationException("error") { HResult = code };
}
catch (InvalidOperationException ex) when (ex.HResult == 1)
{
Console.WriteLine("コード 1 のエラー");
}
catch (InvalidOperationException ex) when (ex.HResult == 2)
{
Console.WriteLine("コード 2 のエラー");
}
}
Process(1); // コード 1 のエラー
Process(2); // コード 2 のエラーusing 文でスコープ終了時に自動的に Dispose() を呼ぶ。C# 8 の using 宣言(ブロックなし)はスコープ末まで有効。
// 旧スタイル(ブロックあり)
using (var conn = new System.IO.StringReader("data"))
{
Console.WriteLine(conn.ReadToEnd());
} // ここで Dispose
// C# 8+ using 宣言(ブロックなし)
static void WriteFile(string path)
{
using var writer = new System.IO.StreamWriter(path);
writer.WriteLine("Hello");
} // メソッド終了時に DisposeException を継承して独自例外を作成する。メッセージを受け取るコンストラクタと、インナー例外を受け取るコンストラクタを実装するのが慣例。
public class ValidationException : Exception
{
public string Field { get; }
public ValidationException(string field, string message)
: base(message)
{
Field = field;
}
public ValidationException(string field, string message, Exception inner)
: base(message, inner)
{
Field = field;
}
}
throw new ValidationException("Email", "無効なメールアドレスです");クラス・構造体・レコード
基礎struct は小さく不変(または短命)なデータ向け。16 バイト以下が目安。頻繁にボックス化される場面では逆にオーバーヘッドになる。
// struct: 小さなデータの値セマンティクス
readonly struct Color(byte r, byte g, byte b)
{
public byte R => r;
public byte G => g;
public byte B => b;
public override string ToString() => `rgb(${R},${G},${B})`;
}
var red = new Color(255, 0, 0);
Console.WriteLine(red); // rgb(255,0,0)record は不変データに最適。値等価性・ToString のオーバーライド・with 式によるコピーが自動生成される。record struct(C# 10+)は値型版。
record Person(string Name, int Age);
var alice = new Person("Alice", 30);
var alice2 = alice with { Age = 31 }; // コピー+変更
Console.WriteLine(alice); // Person { Name = Alice, Age = 30 }
Console.WriteLine(alice2); // Person { Name = Alice, Age = 31 }
Console.WriteLine(alice == alice2); // False(値等価性)
// record struct(C# 10+)
record struct Point3D(double X, double Y, double Z);自動実装プロパティで get/set を簡潔に書ける。init アクセサ(C# 9+)はオブジェクト初期化子でのみ代入可能にする。
class Product
{
public string Name { get; init; } = "";
public decimal Price { get; set; }
public Product(string name, decimal price)
{
Name = name;
Price = price;
}
}
var p = new Product("Widget", 9.99m);
// p.Name = "Other"; // エラー: init-only
p.Price = 12.99m;
Console.WriteLine(`${p.Name}: ${p.Price:C}`); // Widget: ¥12.99sealed は継承禁止。abstract は抽象メンバを持ち直接インスタンス化不可。partial はファイルをまたいでクラス定義を分割できる(ソースジェネレータでも多用)。
abstract class Shape
{
public abstract double Area();
public virtual string Describe() => `面積: ${Area():F2}`;
}
sealed class Circle(double radius) : Shape
{
public override double Area() => Math.PI * radius * radius;
}
var c = new Circle(5);
Console.WriteLine(c.Describe()); // 面積: 78.54インターフェース・継承
基礎C# 8 からインターフェースにデフォルト実装を持たせられる。既存の実装クラスを壊さずにインターフェースを拡張できる。
interface ILogger
{
void Log(string message);
// デフォルト実装(C# 8+)
void LogError(string msg) => Log(`[ERROR] ${msg}`);
}
class ConsoleLogger : ILogger
{
public void Log(string message) => Console.WriteLine(message);
}
ILogger logger = new ConsoleLogger();
logger.Log("Info"); // Info
logger.LogError("Oops"); // [ERROR] Oops抽象クラスは共通実装を持ちつつ、サブクラスに特定メソッドの実装を強制する。インターフェースと異なり状態(フィールド)を持てる。
abstract class Validator<T>
{
public bool Validate(T value)
{
if (!IsValid(value))
{
OnInvalid(value);
return false;
}
return true;
}
protected abstract bool IsValid(T value);
protected virtual void OnInvalid(T value) =>
Console.WriteLine(`Invalid: ${value}`);
}
class PositiveValidator : Validator<int>
{
protected override bool IsValid(int value) => value > 0;
}
var v = new PositiveValidator();
Console.WriteLine(v.Validate(5)); // True
Console.WriteLine(v.Validate(-1)); // Invalid: -1 → False同名のメンバを持つ複数インターフェースを実装する場合、インターフェース名.メンバ名 で明示的に実装する。インターフェース経由でのみアクセスできる。
interface IArea { double Calculate(); }
interface IPerimeter { double Calculate(); }
class Rectangle(double w, double h) : IArea, IPerimeter
{
double IArea.Calculate() => w * h;
double IPerimeter.Calculate() => 2 * (w + h);
}
var rect = new Rectangle(3, 4);
Console.WriteLine(((IArea)rect).Calculate()); // 12
Console.WriteLine(((IPerimeter)rect).Calculate()); // 14is は型チェック(C# 7+ はパターン変数を同時宣言可)。as は失敗時に null を返すキャスト。(T)obj は失敗時に例外を投げる。
object obj = "hello";
// is パターン変数(C# 7+)
if (obj is string s)
Console.WriteLine(`文字列: ${s.ToUpper()}`); // 文字列: HELLO
// as(失敗時 null)
var num = obj as int?;
Console.WriteLine(num.HasValue ? num.Value.ToString() : "null"); // null
// switch 式でのキャスト
string kind = obj switch
{
string => "string",
int => "int",
_ => "other",
};
Console.WriteLine(kind); // stringジェネリクス
基礎where T : で型パラメータに制約を加える。class(参照型)、struct(値型)、new()(デフォルトコンストラクタ)、インターフェース名などを指定できる。
static T Max<T>(T a, T b) where T : IComparable<T>
=> a.CompareTo(b) >= 0 ? a : b;
Console.WriteLine(Max(3, 7)); // 7
Console.WriteLine(Max("apple", "mango")); // mango
static T CreateAndInit<T>() where T : new()
{
return new T();
}ジェネリックインターフェースに out(共変)を付けると派生型として扱え、in(反変)を付けると基底型として扱える。
// IEnumerable<out T> は共変
IEnumerable<string> strings = ["a", "b"];
IEnumerable<object> objects = strings; // OK(string は object の派生)
// IComparer<in T> は反変
IComparer<object> objCmp = Comparer<object>.Default;
IComparer<string> strCmp = objCmp; // OK
Console.WriteLine(strCmp.Compare("a", "b")); // -1メソッドに型パラメータを付けると、呼び出し時の引数から型が推論される。明示的に指定することもできる。
static void Swap<T>(ref T a, ref T b)
{
(a, b) = (b, a);
}
int x = 1, y = 2;
Swap(ref x, ref y); // 型推論で T = int
Console.WriteLine(`${x}, ${y}`); // 2, 1
string s1 = "foo", s2 = "bar";
Swap<string>(ref s1, ref s2); // 明示的に T = string
Console.WriteLine(`${s1}, ${s2}`); // bar, foodefault(T) は型のデフォルト値を返す(参照型は null、値型はゼロ相当)。typeof(T) は Type オブジェクトを返す。nameof はシンボル名を文字列にする(リネームリファクタリング安全)。
Console.WriteLine(default(int)); // 0
Console.WriteLine(default(bool)); // False
Console.WriteLine(default(string)); // (空)
Console.WriteLine(typeof(List<int>).Name); // List`1
string firstName = "Alice";
Console.WriteLine(nameof(firstName)); // firstName
static T GetDefault<T>() => default!;LINQ
基礎LINQ はクエリ構文(SQL 風)とメソッド構文(ラムダ式チェーン)の 2 通りで書ける。どちらもコンパイル後は同じになる。
int[] nums = [1, 2, 3, 4, 5, 6];
// クエリ構文
var q = from n in nums
where n % 2 == 0
select n * n;
// メソッド構文(同義)
var m = nums.Where(n => n % 2 == 0).Select(n => n * n);
Console.WriteLine(string.Join(", ", q)); // 4, 16, 36
Console.WriteLine(string.Join(", ", m)); // 4, 16, 36主要 LINQ 演算子。SelectMany はネストしたシーケンスをフラット化。GroupBy はキーでグループ化。Join は 2 つのシーケンスを結合。
string[] words = ["Hello", "World", "C#"];
// SelectMany: 文字列を文字にフラット化
var chars = words.SelectMany(w => w).Distinct().OrderBy(c => c);
Console.WriteLine(new string(chars.ToArray())); // #CHWdelor
// GroupBy
var groups = words.GroupBy(w => w.Length);
foreach (var g in groups)
Console.WriteLine(`len=${g.Key}: ${string.Join(",", g)}`);集計演算子。Aggregate は fold 相当で任意の累積処理ができる。
int[] data = [3, 1, 4, 1, 5, 9, 2, 6];
Console.WriteLine(data.Sum()); // 31
Console.WriteLine(data.Average()); // 3.875
Console.WriteLine(data.Min()); // 1
Console.WriteLine(data.Max()); // 9
// Aggregate: 文字列を ", " で結合
string joined = data.Select(n => n.ToString())
.Aggregate((acc, s) => `${acc}, ${s}`);
Console.WriteLine(joined); // 3, 1, 4, 1, 5, 9, 2, 6LINQ クエリは IEnumerable を返し、実際の処理は列挙時に行われる(遅延評価)。同じクエリを 2 回列挙すると 2 回処理される。ToList()/ToArray() で即時評価してキャッシュできる。
int count = 0;
int[] source = [1, 2, 3, 4, 5];
var query = source.Where(n => { count++; return n > 2; });
Console.WriteLine(`定義後 count=${count}`); // 0(未実行)
var result = query.ToList();
Console.WriteLine(`ToList後 count=${count}`); // 5(全走査)
_ = query.ToList();
Console.WriteLine(`再列挙 count=${count}`); // 10(もう一度走査)コレクション
基礎List は可変長配列。Dictionary はキー・値の連想配列(O(1) 平均)。HashSet は重複なし集合(O(1) 平均)。
var list = new List<int> { 1, 2, 3 };
list.Add(4);
list.Remove(2);
Console.WriteLine(string.Join(", ", list)); // 1, 3, 4
var dict = new Dictionary<string, int>
{
["apple"] = 1,
["banana"] = 2,
};
dict["cherry"] = 3;
Console.WriteLine(dict["banana"]); // 2
var set = new HashSet<int> { 1, 2, 3 };
set.Add(2); // 無視(重複)
Console.WriteLine(set.Count); // 3IEnumerable はシーケンスの最小インターフェース。yield return で遅延シーケンスを生成できる。IReadOnlyList は読み取り専用のインデックスアクセスを提供。
static IEnumerable<int> Fibonacci()
{
int a = 0, b = 1;
while (true)
{
yield return a;
(a, b) = (b, a + b);
}
}
var fibs = Fibonacci().Take(8);
Console.WriteLine(string.Join(", ", fibs)); // 0, 1, 1, 2, 3, 5, 8, 13
IReadOnlyList<string> names = ["Alice", "Bob"];
Console.WriteLine(names[0]); // AliceQueue は FIFO、Stack は LIFO。PriorityQueue(.NET 6+)は優先度付きキューで、優先度の小さい要素から取り出す。
var queue = new Queue<string>();
queue.Enqueue("first");
queue.Enqueue("second");
Console.WriteLine(queue.Dequeue()); // first
var stack = new Stack<int>();
stack.Push(1); stack.Push(2); stack.Push(3);
Console.WriteLine(stack.Pop()); // 3
// PriorityQueue(.NET 6+): 優先度が小さい順
var pq = new PriorityQueue<string, int>();
pq.Enqueue("low", 10);
pq.Enqueue("high", 1);
pq.Enqueue("medium", 5);
Console.WriteLine(pq.Dequeue()); // highIEnumerable を実装し Add メソッドを持つクラスはコレクション初期化子 { } が使える。C# 12 のコレクション式 [ ] とは別物。
// コレクション初期化子(C# 3+)
var dict = new Dictionary<string, int>
{
{ "a", 1 },
{ "b", 2 },
};
// インデックス初期化子(C# 6+)
var dict2 = new Dictionary<string, int>
{
["x"] = 10,
["y"] = 20,
};
Console.WriteLine(dict["a"]); // 1
Console.WriteLine(dict2["x"]); // 10async/await
基礎async メソッドは await でスレッドをブロックせずに非同期操作を待機する。await を使うとスレッドプールに制御を戻し、完了後に再開する。
static async Task<string> FetchAsync(string url)
{
using var client = new System.Net.Http.HttpClient();
string content = await client.GetStringAsync(url);
return content[..50]; // 先頭 50 文字
}
// トップレベル async(C# 9+ / .NET 6+)
var result = await FetchAsync("https://example.com");
Console.WriteLine(result);Task はヒープ割り当てを伴う。既にキャッシュされた結果を返す場合など、ほぼ同期的に完了するメソッドには ValueTask を使うと高パフォーマンス。
static ValueTask<int> GetCachedAsync(bool cached)
{
if (cached)
return new ValueTask<int>(42); // 同期的に完了(割り当て不要)
return new ValueTask<int>(Task.FromResult(99)); // 非同期
}
int v1 = await GetCachedAsync(true);
int v2 = await GetCachedAsync(false);
Console.WriteLine(`${v1}, ${v2}`); // 42, 99Task.WhenAll は複数タスクを並列実行してすべての完了を待つ。Task.WhenAny は最初に完了したタスクを返す。
static async Task<int> DelayedValue(int ms, int val)
{
await Task.Delay(ms);
return val;
}
// 並列実行
var results = await Task.WhenAll(
DelayedValue(100, 1),
DelayedValue(200, 2),
DelayedValue(50, 3)
);
Console.WriteLine(string.Join(", ", results)); // 1, 2, 3
// 最初に完了したタスク
var first = await Task.WhenAny(
DelayedValue(300, 10),
DelayedValue(100, 20)
);
Console.WriteLine(await first); // 20CancellationTokenSource でトークンを生成し、非同期メソッドに渡す。キャンセル時は OperationCanceledException がスローされる。
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
try
{
await Task.Delay(5000, cts.Token); // 5秒待機するが…
Console.WriteLine("完了");
}
catch (OperationCanceledException)
{
Console.WriteLine("キャンセルされました");
}
// 手動キャンセル
using var cts2 = new CancellationTokenSource();
cts2.Cancel();
Console.WriteLine(cts2.Token.IsCancellationRequested); // True並行処理
応用Thread クラスは OS スレッドを直接扱う。ThreadPool.QueueUserWorkItem はスレッドプールにタスクをキューイングする。通常は Task の利用を優先する。
// Thread
var t = new Thread(() =>
{
Console.WriteLine(`Thread id=${Environment.CurrentManagedThreadId}`);
});
t.Start();
t.Join();
// ThreadPool
ThreadPool.QueueUserWorkItem(_ =>
{
Console.WriteLine("ThreadPool worker");
});
Thread.Sleep(100); // 出力を待つ(本番では避ける)Parallel.For/ForEach は自動的にスレッドプールを使って並列ループを実行する。各イテレーションが独立している場合に有効。
int[] results = new int[5];
Parallel.For(0, 5, i =>
{
results[i] = i * i; // 独立した計算
});
Console.WriteLine(string.Join(", ", results)); // 0, 1, 4, 9, 16
string[] words = ["hello", "world", "csharp"];
Parallel.ForEach(words, w =>
{
Console.WriteLine(w.ToUpper());
});lock が必要。lock は Monitor.Enter/Exit のシュガー。Interlocked はアトミックな加減算・比較交換を提供し、軽量な数値操作に使う。
int counter = 0;
object lockObj = new();
Parallel.For(0, 1000, _ =>
{
lock (lockObj) { counter++; } // 排他制御
});
Console.WriteLine(counter); // 1000
// Interlocked(ロック不要のアトミック操作)
int atomicCounter = 0;
Parallel.For(0, 1000, _ =>
{
Interlocked.Increment(ref atomicCounter);
});
Console.WriteLine(atomicCounter); // 1000Channel は生産者・消費者パターン向けの非同期スレッドセーフキュー。ChannelWriter で書き込み、ChannelReader で非同期読み取りできる。
using System.Threading.Channels;
var channel = Channel.CreateUnbounded<int>();
// 生産者
async Task Producer()
{
for (int i = 0; i < 5; i++)
{
await channel.Writer.WriteAsync(i);
}
channel.Writer.Complete();
}
// 消費者
async Task Consumer()
{
await foreach (var item in channel.Reader.ReadAllAsync())
Console.Write(`${item} `);
Console.WriteLine();
}
await Task.WhenAll(Producer(), Consumer()); // 0 1 2 3 4パターンマッチ
応用is 演算子は型・定数・宣言パターンに対応。is not null で null チェックも簡潔に書ける。
object[] items = [42, "hello", null, 3.14, true];
foreach (var item in items)
{
if (item is int n and > 0)
Console.WriteLine(`正の整数: ${n}`);
else if (item is string { Length: > 3 } s)
Console.WriteLine(`長い文字列: ${s}`);
else if (item is null)
Console.WriteLine("null");
else
Console.WriteLine(`その他: ${item}`);
}C# 8〜11 で property・positional・list パターンが追加。複雑な条件分岐を宣言的に書ける。
record Point(int X, int Y);
static string Classify(Point p) => p switch
{
{ X: 0, Y: 0 } => "原点",
{ X: 0 } => "Y軸上",
{ Y: 0 } => "X軸上",
(var x, var y) when x == y => "対角線上",
_ => "その他",
};
Console.WriteLine(Classify(new Point(0, 0))); // 原点
Console.WriteLine(Classify(new Point(3, 3))); // 対角線上
Console.WriteLine(Classify(new Point(1, 2))); // その他
// list パターン(C# 11+)
int[] arr = [1, 2, 3];
if (arr is [1, .., 3])
Console.WriteLine("1 で始まり 3 で終わる");switch 式のアームに when を追加して追加条件を付けられる。パターンマッチと組み合わせ複雑なロジックを宣言的に表現する。
static string FizzBuzz(int n) => n switch
{
_ when n % 15 == 0 => "FizzBuzz",
_ when n % 3 == 0 => "Fizz",
_ when n % 5 == 0 => "Buzz",
_ => n.ToString(),
};
for (int i = 1; i <= 15; i++)
Console.Write(FizzBuzz(i) + " ");
// 1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz 11 Fizz 13 14 FizzBuzzパターンマッチを使うと null チェックをより明示的・安全に書ける。is not null や switch の null アームを活用する。
string? GetName(bool flag) => flag ? "Alice" : null;
string? name = GetName(false);
// パターンマッチで null を明示的に処理
if (name is not null)
Console.WriteLine(`Hello, ${name}!`);
else
Console.WriteLine("名前がありません");
// switch 式での null ハンドリング
string result = name switch
{
null => "(未設定)",
{ Length: 0 } => "(空文字)",
var n => `こんにちは、${n}!`,
};
Console.WriteLine(result); // (未設定)デリゲート・イベント・ラムダ
応用Func は戻り値あり、Action は戻り値なし、Predicate は bool を返す組み込みデリゲート型。高階関数を簡潔に書ける。
Func<int, int, int> add = (a, b) => a + b;
Action<string> print = s => Console.WriteLine(s);
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(add(3, 4)); // 7
print("Hello!"); // Hello!
Console.WriteLine(isEven(10)); // True
// 高階関数
static List<T> Filter<T>(List<T> list, Predicate<T> pred)
=> list.FindAll(pred);
var evens = Filter([1, 2, 3, 4, 5], isEven);
Console.WriteLine(string.Join(", ", evens)); // 2, 4デリゲートは += で複数のメソッドを登録(マルチキャスト)できる。呼び出すと登録順に全メソッドが実行される。
Action<string> log = s => Console.WriteLine(`[LOG] ${s}`);
log += s => Console.WriteLine(`[FILE] ${s}`);
log += s => Console.WriteLine(`[NET] ${s}`);
log("テストメッセージ");
// [LOG] テストメッセージ
// [FILE] テストメッセージ
// [NET] テストメッセージ
// 削除
log -= s => Console.WriteLine(`[FILE] ${s}`);event は特定の delegate を外部から直接呼び出せないようにカプセル化する。+=/-= で購読・解除のみ許可する。
class Button
{
public event Action<string>? Clicked;
public void Click(string label)
{
Console.WriteLine(`ボタン'${label}'クリック`);
Clicked?.Invoke(label);
}
}
var btn = new Button();
btn.Clicked += label => Console.WriteLine(`ハンドラ1: ${label}`);
btn.Clicked += label => Console.WriteLine(`ハンドラ2: ${label}`);
btn.Click("OK");C# 10 でラムダ式に自然型(natural type)が付き、var で受けられるようになった。static ラムダはクロージャ変数をキャプチャしない。
// C# 10: var でラムダを受ける(natural type)
var square = (int x) => x * x;
Console.WriteLine(square(5)); // 25
// 戻り値型の明示(C# 10+)
var divide = int (int a, int b) => a / b;
Console.WriteLine(divide(10, 3)); // 3
// static ラムダ(クロージャ禁止)
int multiplier = 3;
// static な場合は multiplier を使うとコンパイルエラー
var triple = static (int x) => x * 3;
Console.WriteLine(triple(4)); // 12最新機能(C# 10-12)
応用global using はプロジェクト全体に using を適用する(C# 10+)。SDK スタイルプロジェクトでは で一般的な名前空間が自動 import される。
// GlobalUsings.cs に書くと全ファイルで有効
global using System;
global using System.Collections.Generic;
global using System.Linq;
global using System.Threading.Tasks;
// .csproj
// <ImplicitUsings>enable</ImplicitUsings>
// → System, System.Linq, System.IO などが自動 import
// これ以降のファイルで using 不要
var list = new List<int> { 1, 2, 3 };
Console.WriteLine(list.Sum()); // 6required を付けたプロパティ/フィールドは、オブジェクト初期化子で必ず指定しないとコンパイルエラーになる。コンストラクタなしで必須初期化を強制できる。
class User
{
public required string Name { get; init; }
public required string Email { get; init; }
public int Age { get; init; }
}
// OK: required メンバをすべて指定
var user = new User { Name = "Alice", Email = "alice@example.com" };
// NG: Name が未指定 → CS9035 コンパイルエラー
// var bad = new User { Email = "bob@example.com" };
Console.WriteLine(`${user.Name} <${user.Email}>`);C# 12 で class と struct にもプライマリコンストラクタが使えるようになった(record のみだった制限を解除)。パラメータはクラス全体で参照できる。
class Logger(string prefix)
{
// prefix はクラス全体のスコープで利用可能
public void Log(string message) =>
Console.WriteLine(`[${prefix}] ${message}`);
public void Error(string message) =>
Console.WriteLine(`[${prefix}][ERROR] ${message}`);
}
var logger = new Logger("App");
logger.Log("起動しました"); // [App] 起動しました
logger.Error("接続に失敗"); // [App][ERROR] 接続に失敗file 修飾子を付けた型はそのファイル内のみで可視。ソースジェネレータが生成する補助型の名前衝突を避けるために使われる。インターセプターはメソッド呼び出しをコンパイル時に差し替える実験的機能(C# 12 preview)。
// このファイルだけで使える型
file class InternalHelper
{
public static string Format(int n) => `[${n}]`;
}
Console.WriteLine(InternalHelper.Format(42)); // [42]
// 別ファイルから InternalHelper は見えない(コンパイルエラー)
// インターセプター(実験的 / C# 12 preview)
// [System.Runtime.CompilerServices.InterceptsLocation(...)]
// static void MyInterceptor(...) { ... }