C#
目次
主要項目のみを表示しています。詳細な小見出しは本文内で確認できます。
- 概要
- 1. C#とは何か
- 2. .NETランタイムとCLR
- 3. 型システム
- 4. 変数・式・演算子
- 5. 制御フローとパターンマッチング
- 6. メソッドとパラメータ
- 7. クラス・継承・interface
- 8. ジェネリクス
- 9. プロパティ・インデクサ・イベント
- 10. delegateとevent
- 11. LINQ
- 12. async / await
- 13. 例外処理
- 14. recordとstructs
- 15. Nullable Reference Types
- 16. パッケージ管理(NuGet)
- 17. テストとパフォーマンス
- 18. C# 8〜12の進化
- 19. よくある落とし穴FAQ
- 20. 図解: CLR / GC / async
- 21. 学習ロードマップ(30日)
- 22. 用語集
- 発展: .NETと言語機能
- 23. 値型と参照型の深層
- 24. 文字列の詳細
- 25. プロパティとフィールドの深層
- 26. ジェネリクス深掘り
- 27. LINQ深掘り
- 28. async / await深掘り
- 29. Span
とMemory - 30. リフレクションと属性
- 実践: アプリケーション開発
- 応用: 分散システムと運用
- 上級: ランタイムと相互運用
- C# 12.0 の最新機能と.NET 8アーキテクチャ
- まとめ
- 参考文献
概要
まず、この章の中心構造を図で確認します。細部に入る前に、どの概念がどこへつながるかをつかむための地図です。
コード例は、そのまま写すためだけのものではありません。直前の本文で「何を確かめる例か」を押さえ、直後の説明で「どの性質が見えるか」を確認してください。実務では、ここに入力の境界、失敗時の挙動、依存する実行環境を足して読むと判断しやすくなります。
C# は、.NETランタイムの上で、型安全性、非同期処理、実務アプリケーション開発を支える言語です。
このページでは、CLR、型システム、LINQ、async/await、record、nullable reference typesなどを、設計判断と実装感覚の両方から整理します。
1. C#とは何か
C# は、「静的型付け・オブジェクト指向 + 関数型のエッセンス・モダン」 言語。Microsoftが2000年に発表し、当初はJavaの対抗馬として始まりましたが、現代では .NETエコシステムの中核として独自の地位を築いています。
主な用途:
- エンタープライズバックエンド: ASP.NET Core、Azure
- デスクトップアプリ: WPF、WinUI、MAUI
- モバイル: .NET MAUI(iOS / Androidクロスプラットフォーム)
- ゲーム開発: Unity(圧倒的シェア)
- クラウド: Microsoft Azureの主要言語
- Webフロントエンド: Blazor(C# でフロント)
1-1. C# の歴史
Javaへの対抗馬として(2000)
2000年、Anders Hejlsberg(Turbo Pascal、Delphiの設計者、後にTypeScriptも作る)が中心となってC# が設計されました。当初は「Microsoft版のJava」として始まりましたが、より野心的な機能を取り込んでいきます。
2000 C# 1.0 / .NET Framework 1.0
2005 C# 2.0ジェネリクス、Nullable<T>、yield
2007 C# 3.0 LINQ、ラムダ、var、自動プロパティ
2010 C# 4.0 dynamic、共変・反変
2012 C# 5.0 async/await(業界初の言語標準)
2015 C# 6.0 string interpolation、null-conditional
2017 C# 7.0 tuple、pattern matching、ローカル関数
2019 C# 8.0 nullable reference types、async streams、switch式
2020 C# 9.0 record、init、top-level statements
2021 C# 10 file-scoped namespace、global usings
2022 C# 11 raw string、required members
2023 C# 12 primary constructor、collection expressions
.NET Core / .NET 5+ への大進化(2016〜)
長らくWindows専用だった .NET Frameworkに対し、2016年に .NET Coreが登場してクロスプラットフォーム化しました。2020年に 「.NET 5」 として統合(FrameworkとCoreの歴史を1つに)、現在は .NET 8(LTS) が最新の標準。
2016 .NET Core 1.0(Linux/Mac対応)
2020 .NET 5(FrameworkとCoreの統合)
2021 .NET 6(LTS、最初のクロスプラットフォームLTS)
2023 .NET 8(LTS、現役主流)
1-2. async/awaitを業界標準にした
C# 5.0(2012) の async/await は、業界で最初に言語標準として導入された非同期構文でした。これが後のJavaScript ES2017、Python 3.5、Rust 1.39、Kotlin、Swiftなどに広く影響を与えました。
「非同期コードを同期のように書ける」というパラダイムは、AndersとMicrosoftの研究チームがC# に持ち込んだ偉大な貢献です。
1-3. このセクションのまとめ
- 2000年Microsoft、Anders Hejlsbergが設計
- Java対抗馬として始まり、独自進化
- async/awaitを業界に持ち込んだ(C# 5)
- 2016年 .NET Coreでクロスプラットフォーム
- 現在は .NET 8 LTS、Unity・ASP.NETの中核
2. .NETランタイムとCLR
C# は CLR(Common Language Runtime) という仮想マシンの上で動く。JavaのJVMに相当します。
2-1. CLRとCIL
.cs (C# ソース)
↓ csc / Roslynコンパイラ
.dllまたは .exe(CIL = Common Intermediate Languageを含む)
↓ CLRがロード
JITコンパイル → 機械語
↓
実行
CIL(旧MSIL)は JVMのバイトコードに相当するスタックベースの中間表現。F#、Visual Basicも同じCILに変換され、互換性があります。
2-2. ガベージコレクション
.NETのGCは 世代別 + コンカレント + バックグラウンド。
Gen 0: 新しいオブジェクト
Gen 1: Gen 0を生き延びたもの
Gen 2: 長寿命なもの
LOH (Large Object Heap): 85KB超の巨大オブジェクト
POH (Pinned Object Heap): GCが動かさない領域(.NET 5+)
通常使用では GCを意識しなくても性能が出る設計。低レイテンシ要件なら GCSettings.LatencyMode でモード切り替え可能。
2-3. AOTコンパイル
.NET 7+ で ネイティブAOT が本格的に。
dotnet publish -c Release -r win-x64 --self-contained -p:PublishAot=true
JITを使わず 事前にネイティブコード化。起動が瞬時、メモリフットプリントが小さい、リフレクションに制約あり。クラウドのコールドスタート対策・組み込みで重要。
2-4. .NETツールチェイン
dotnet new console -n MyApp # プロジェクト作成
dotnet build # ビルド
dotnet run # ビルド + 実行
dotnet test # テスト
dotnet add package Newtonsoft.Json # NuGetパッケージ追加
dotnet publish -c Release # 配布用ビルド
dotnet ef migrations add Init # EF Coreマイグレーション
dotnet CLIが統一インターフェース。Goの go やRustの cargo 同様、すべて1コマンドで完結。
2-5. このセクションのまとめ
- CLRがVM、CILがバイトコード
- 世代別 + コンカレントGC
- AOTで起動瞬時のネイティブバイナリ
- dotnet CLIでビルド・テスト・パッケージ管理
- .NET Standard / .NET Core / .NET 5+ の歴史
3. 型システム
C# の型システムは 「値型と参照型の二分」。Javaと似ているが、struct でユーザ定義の値型が作れる点が違います。
3-1. 値型と参照型
値型(struct, primitive, enum):
- スタックに置かれる(ことが多い)
- 代入でコピー
- int / double / bool / DateTime / 自作struct
参照型(class, interface, delegate, string, array):
- ヒープに置かれる
- 代入で参照を共有
- object / string / List<T>
int a = 10;
int b = a; // コピー
b = 20;
Console.WriteLine(a); // 10(変わらない)
var list = new List<int> { 1 };
var list2 = list;
list2.Add(2);
Console.WriteLine(list.Count); // 2(同じインスタンスを共有)
3-2. プリミティブ型
| 型 | サイズ | エイリアス |
|---|---|---|
int |
4 byte | Int32 |
long |
8 byte | Int64 |
short |
2 byte | Int16 |
byte |
1 byte | Byte |
float |
4 byte | Single |
double |
8 byte | Double |
decimal |
16 byte | Decimal(10進28桁) |
bool |
1 byte | Boolean |
char |
2 byte(UTF-16) | Char |
string |
参照型 | String |
decimal は 金融計算用の10進数型。JavaのBigDecimal相当。
3-3. varと型推論
var x = 10; // int
var s = "hello"; // string
var list = new List<int>(); // List<int>
ローカル変数のみ。フィールドや戻り値には書けません。
3-4. nullable
int x = null; // エラー(intは値型、null不可)
int? y = null; // OK(Nullable<int>)
string s = null; // OKだが警告(C# 8+ Nullable RT)
string? t = null; // OK
C# 8+ で Nullable Reference Types(後述)が導入され、参照型のnullも型レベルで表現できるようになりました。
3-5. このセクションのまとめ
値型vs参照型:
struct/プリミティブ = 値型(コピー)
class/string/array = 参照型(参照共有)
decimal:
10進数の正確な計算(金融)
var:
ローカル変数の型推論
null:
値型は ? 必須(int? = Nullable<int>)
参照型もC# 8+ で型レベルnull区別
4. 変数・式・演算子
4-1. 変数宣言
int x; // 既定値(0)
int x = 10;
const int MAX = 100; // コンパイル時定数
readonly int y = 20; // 実行時1回だけ初期化
4-2. 演算子
C/Javaとほぼ同じ。特徴的なものを抜粋:
// null合体演算子
string name = input ?? "anonymous"; // inputがnullなら "anonymous"
input ??= "default"; // nullなら代入
// null条件演算子
user?.Name?.ToUpper() // userかNameがnullならnull
// 範囲演算子(C# 8+)
arr[1..4] // arr[1], arr[2], arr[3]
arr[..3] // arr[0], arr[1], arr[2]
arr[^1] // 末尾要素
// パターンマッチング
if (obj is string s && s.Length > 0) { ... }
4-3. 文字列補間
var name = "Alice";
var msg = {{CONTENT}}quot;Hello, {name}!";
var msg2 = {{CONTENT}}quot;Total: {amount:C}"; // 通貨フォーマット
Raw string(C# 11+)
var json = """
{
"name": "Alice"
}
""";
複数行とエスケープを楽に。
4-4. このセクションのまとめ
- ?? null合体、??= null代入
- ?. null条件アクセス
- ^indexで末尾、a..bで範囲
- {{CONTENT}}quot;..." で文字列補間
- """..."""でraw string(C# 11+)
5. 制御フローとパターンマッチング
5-1. if / for / foreach / while
if (x > 0) { ... } else { ... }
for (int i = 0; i < 10; i++) { ... }
foreach (var item in list) { ... }
while (cond) { ... }
do { ... } while (cond);
5-2. switch式(C# 8+)
var label = day switch
{
1 or 2 or 3 or 4 or 5 => "weekday",
6 or 7 => "weekend",
_ => "invalid"
};
パターンマッチング
var area = shape switch
{
Circle { Radius: var r } => Math.PI * r * r,
Rectangle { W: var w, H: var h } => w * h,
_ => throw new ArgumentException()
};
型パターン + when
var msg = obj switch
{
int n when n > 0 => "positive int",
int n => "non-positive int",
string s => {{CONTENT}}quot;string: {s}",
null => "null",
_ => "other"
};
C# のパターンマッチングはRustやScalaに近い表現力を持ちます。
5-3. このセクションのまとめ
- 古典的なif / for / foreach / while
- switch式(C# 8+)で関数型風
- パターンマッチング(型・分解・ガード)
- when句で条件追加
6. メソッドとパラメータ
6-1. メソッド
public int Add(int a, int b) => a + b; // 式形式
public int Add(int a, int b)
{
return a + b;
}
C# 6+ の => は式メンバ(expression-bodied member)。
6-2. パラメータ修飾子
void Method(
int x, // 値渡し
ref int y, // 参照渡し(呼び出し前に初期化必須)
out int z, // 出力(メソッド内で代入必須)
in int w, // 読み取り専用参照渡し(性能用)
params int[] nums // 可変長引数
) { ... }
int a = 1, b;
Method(a, ref a, out b, in a, 1, 2, 3);
6-3. オプション引数・名前付き引数
void Greet(string name, string greeting = "Hello", string emoji = "👋")
{
Console.WriteLine({{CONTENT}}quot;{greeting}, {name} {emoji}");
}
Greet("Alice");
Greet("Bob", "Hi");
Greet("Charlie", emoji: "😎"); // 名前付き引数
6-4. ローカル関数
int Compute(int x)
{
int Helper(int n) => n * 2;
return Helper(x) + Helper(x + 1);
}
メソッド内に ヘルパー関数を定義できる。スコープが限定されて見通しが良い。
6-5. このセクションのまとめ
- => で式メンバ
- ref / out / in / paramsの修飾子
- オプション引数・名前付き引数
- ローカル関数で関数内ヘルパー
7. クラス・継承・interface
7-1. クラス
public class Person
{
private string _name;
private int _age;
public Person(string name, int age)
{
_name = name;
_age = age;
}
public string Name => _name; // 読み取り専用プロパティ
public string Greet() => {{CONTENT}}quot;Hi, I'm {_name}";
}
アクセス修飾子
public: どこからでも
private: クラス内のみ(デフォルト)
protected: サブクラスから
internal: 同一アセンブリ内のみ
protected internal: protectedまたはinternal
private protected: 派生 かつ 同一アセンブリ
file: 同一ファイル内のみ(C# 11+)
7-2. 継承
public class Animal
{
public virtual void Speak() => Console.WriteLine("...");
}
public class Dog : Animal
{
public override void Speak() => Console.WriteLine("Woof");
}
C# は 単一継承 + 複数interface実装(Javaと同じ)。virtual でオーバーライド可能、override で明示。
7-3. interface
public interface IGreet
{
string Greet();
string Name { get; }
// C# 8+ デフォルト実装
string Shout() => Greet().ToUpper();
}
public class Greeter : IGreet
{
public string Name { get; }
public Greeter(string name) => Name = name;
public string Greet() => {{CONTENT}}quot;hi, {Name}";
}
interface名は慣習として I で始める(IDisposable、IEnumerableなど)。
7-4. abstract / sealed
public abstract class Shape
{
public abstract double Area(); // 実装なし、サブクラスで必須
}
public sealed class Circle : Shape // sealedで継承禁止
{
public override double Area() => Math.PI * R * R;
private double R;
}
7-5. このセクションのまとめ
- 単一継承 + 複数interface実装
- virtual / overrideで多態性
- interfaceはIで始まる慣習、C# 8+ でデフォルト実装
- abstract(実装必須)、sealed(継承禁止)
- アクセス修飾子: public / private / protected / internal
8. ジェネリクス
C# 2.0(2005)で導入。Javaと違い「reified(実体化された)」型パラメータを持つ点が特徴。
8-1. 基本
public class Stack<T>
{
private List<T> _items = new();
public void Push(T item) => _items.Add(item);
public T Pop()
{
var last = _items[^1];
_items.RemoveAt(_items.Count - 1);
return last;
}
}
var s = new Stack<int>();
s.Push(1);
8-2. 型制約
public T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
// その他の制約
where T : class // 参照型
where T : struct // 値型
where T : new() // 引数なしコンストラクタ
where T : SomeBase // 特定クラスの派生
where T : ISomeInterface // 特定interface実装
where T : notnull // null不可(C# 8+)
8-3. reifiedジェネリクス
C# は型消去(Java)と違い、実行時にも型情報が残る。
var list = new List<int>();
list.GetType(); // System.Collections.Generic.List`1[System.Int32]
typeof(T); // メソッド内でTの型を取得可能
これにより typeof(T) が機能し、リフレクションも完全に動作。Javaで List<String>.class が書けない問題がC# にはありません。
8-4. このセクションのまとめ
- Tなどの型パラメータ
- where T : ... で制約
- 実体化される(typeof(T) が動く)
- 共変 (out T) / 反変 (in T)
9. プロパティ・インデクサ・イベント
C# の特徴的な機能。フィールドアクセスのように見えるメソッドを簡潔に書けます。
9-1. プロパティ
public class Person
{
public string Name { get; set; } // 自動プロパティ
public int Age { get; private set; } // 読み取り公開、書き込みprivate
public string FullName { get; init; } // init: 初期化のみ可(C# 9+)
private string _email;
public string Email
{
get => _email;
set => _email = value?.ToLower();
}
}
var p = new Person { Name = "Alice", Age = 30 };
9-2. インデクサ
public class Vector
{
private double[] _data;
public double this[int i]
{
get => _data[i];
set => _data[i] = value;
}
}
var v = new Vector(...);
v[0] = 1.0;
this[index] で 配列のような構文を実現。
9-3. イベント(次章で詳述)
public event EventHandler? Click;
button.Click += (s, e) => { ... };
9-4. このセクションのまとめ
- プロパティ: get/setでフィールドアクセス風
- 自動プロパティ: { get; set; }
- init: 初期化のみ可能(C# 9+)
- インデクサ: this[i] で配列風アクセス
10. delegateとevent
10-1. delegate(関数ポインタの型安全版)
public delegate int BinOp(int a, int b);
BinOp add = (a, b) => a + b;
BinOp mul = (a, b) => a * b;
add(1, 2); // 3
FuncとAction
Func<int, int, int> add = (a, b) => a + b; // 戻り値あり
Action<string> print = s => Console.WriteLine(s); // 戻り値なし
Predicate<int> isEven = n => n % 2 == 0; // bool戻り
Func<...> と Action<...> が標準で、ほとんどの場面で delegate を自前定義しなくて済む。
10-2. ラムダ式
x => x * 2
(a, b) => a + b
// 式形式
var sq = (int x) => x * x;
// 文形式
var process = (int x) => {
var y = x * 2;
return y + 1;
};
10-3. event
public class Button
{
public event EventHandler? Clicked;
public void OnClick()
{
Clicked?.Invoke(this, EventArgs.Empty);
}
}
var b = new Button();
b.Clicked += (sender, e) => Console.WriteLine("clicked");
b.OnClick();
event は 「外から += / -= で登録するdelegate」。GUIフレームワーク・通知パターンで多用。
10-4. このセクションのまとめ
- delegate: 関数ポインタの型安全版
- Func<T, R> / Action<T> / Predicate<T> が標準
- ラムダ式x => x * 2
- eventはdelegateベースの通知機構
11. LINQ
C# 3.0(2007)の象徴的機能。コレクション操作をSQL風に書ける仕組み。
11-1. メソッド構文
var evens = numbers.Where(n => n % 2 == 0);
var doubled = numbers.Select(n => n * 2);
var sum = numbers.Sum();
var max = numbers.Max();
var first = numbers.FirstOrDefault();
var grouped = people.GroupBy(p => p.Age);
var ordered = people.OrderBy(p => p.Name).ThenBy(p => p.Age);
var result = people
.Where(p => p.Age >= 18)
.Select(p => p.Name)
.OrderBy(n => n)
.ToList();
11-2. クエリ構文
var result = from p in people
where p.Age >= 18
orderby p.Name
select p.Name;
SQL風の構文で、メソッド構文とほぼ等価。
11-3. 主要オペレータ
Where filter
Select map
SelectMany flatMap
OrderBy / ThenBy sort
GroupBy group_by
Join SQLのJOIN
Aggregate reduce
Sum / Min / Max / Average / Count
First / Single / Any / All
ToList / ToArray / ToDictionary
11-4. 遅延実行
LINQは 遅延実行。.ToList() などの終端まで実際の処理が走らない。
var query = list.Where(x => Heavy(x)); // 何も実行されない
foreach (var x in query) { ... } // ここで実行
11-5. このセクションのまとめ
- メソッド構文 / クエリ構文の2形式
- Where / Select / OrderBy / GroupBy / Sum
- 遅延実行(ToListなどで実行)
- IEnumerable<T> を返すパイプライン
12. async / await
C# 5.0(2012)で 業界初の言語標準async/await として導入。今やJavaScript/Python/Rustなどが追随した源流。
await はスレッドを占有して待つ構文ではありません。未完了の処理を Task として返し、I/O完了後に続きの処理を再開するため、サーバーでは少ないスレッドで多数の待ち時間を扱いやすくなります。
12-1. 基本
public async Task<string> FetchAsync(string url)
{
using var client = new HttpClient();
var body = await client.GetStringAsync(url);
return body;
}
var body = await FetchAsync("https://example.com");
async メソッドは Task<T> を返す。await で完了を待つ。
12-2. TaskとTask
Task // 戻り値なしの非同期
Task<T> // Tを返す非同期
ValueTask<T> // 性能重視(割り当て削減)
12-3. 並行実行
var t1 = FetchAsync("url1");
var t2 = FetchAsync("url2");
var t3 = FetchAsync("url3");
var results = await Task.WhenAll(t1, t2, t3);
Task.WhenAll で複数のTaskを並行に。Task.WhenAny で最初の1つ。
12-4. CancellationToken
public async Task FetchAsync(string url, CancellationToken ct)
{
var body = await client.GetStringAsync(url, ct);
}
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await FetchAsync("...", cts.Token); // 5秒でキャンセル
CancellationToken を引数で受け渡すのが慣習。Goの context.Context に相当。
12-5. asyncストリーム(C# 8+)
public async IAsyncEnumerable<int> GenerateAsync()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(100);
yield return i;
}
}
await foreach (var i in GenerateAsync())
{
Console.WriteLine(i);
}
非同期に1つずつ要素を生成する asyncジェネレータ。
12-6. 落とし穴
1. async voidは避ける
public async void HandleClick(...) // Bad: 例外が捕まえられない
public async Task HandleClickAsync(...) // Good
2. .Result / .Wait() は避ける
var result = AsyncMethod().Result; // デッドロックの危険
var result = await AsyncMethod(); // 推奨
3. ConfigureAwait(false)
ライブラリでは ConfigureAwait(false) でcontext切り替えを抑制(パフォーマンス)。
var data = await client.GetStringAsync(url).ConfigureAwait(false);
12-7. このセクションのまとめ
- async / awaitでノンブロッキング
- Task<T> / Task / ValueTask<T>
- WhenAllで並行
- CancellationTokenでキャンセル
- asyncストリーム(C# 8+)
- async void / .Resultは避ける
13. 例外処理
try
{
risky();
}
catch (FileNotFoundException ex)
{
Console.WriteLine({{CONTENT}}quot;file: {ex.Message}");
}
catch (Exception ex) when (ex.Message.Contains("specific"))
{
// フィルタ付きcatch
}
finally
{
cleanup();
}
// 再スロー(スタックトレース保持)
catch (Exception)
{
throw;
}
// throw式
public string Validate(string s)
=> string.IsNullOrEmpty(s) ? throw new ArgumentException() : s;
using文
using (var f = File.OpenRead("...")) { ... } // C# 7まで
using var f = File.OpenRead("..."); // C# 8+ シンプル形式
IDisposable を実装したオブジェクトを自動Dispose。Javaのtry-with-resources相当。
14. recordとstructs
14-1. record(C# 9+)
イミュータブルなデータクラスを簡潔に定義。
public record Person(string Name, int Age);
var p = new Person("Alice", 30);
var p2 = p with { Age = 31 }; // 新しいインスタンスを作る
record は自動的に Equals GetHashCode ToString を生成。
record class vs record struct
public record Person(string Name); // class
public record struct Point(int X, int Y); // struct
14-2. struct
public struct Point
{
public int X;
public int Y;
public Point(int x, int y) { X = x; Y = y; }
}
値型。コピーされる。継承不可(interface実装は可能)。
readonly struct
public readonly struct Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) { X = x; Y = y; }
}
イミュータブルstruct。性能と安全性に有利。
15. Nullable Reference Types
C# 8+ で導入された 「nullを型で表現」する仕組み。
#nullable enable
string a = null; // 警告!
string? b = null; // OK
void Process(string? s)
{
Console.WriteLine(s.Length); // 警告!sがnullかもしれない
if (s != null)
Console.WriteLine(s.Length); // OK
}
? を付けないと null不可として扱われる。これにより null参照例外をコンパイル時に検出できます。
// プロジェクトで全体有効化
<Nullable>enable</Nullable> // .csproj
16. パッケージ管理(NuGet)
dotnet add package Newtonsoft.Json
dotnet add package Microsoft.Extensions.Logging --version 8.0.0
dotnet restore # 依存復元
dotnet pack # パッケージ作成
dotnet nuget push *.nupkg --source nuget.org
.csproj ファイルに依存が記録されます:
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
17. テストとパフォーマンス
17-1. xUnit / NUnit / MSTest
public class CalculatorTests
{
[Fact]
public void Add_PositiveNumbers()
{
Assert.Equal(3, 1 + 2);
}
[Theory]
[InlineData(1, 1, 2)]
[InlineData(2, 3, 5)]
public void Add_Theory(int a, int b, int expected)
{
Assert.Equal(expected, a + b);
}
}
xUnit が現代の主流。
17-2. BenchmarkDotNet
[MemoryDiagnoser]
public class StringConcat
{
[Benchmark]
public string Concat() => string.Concat("a", "b", "c");
[Benchmark]
public string Builder() => new StringBuilder().Append("a").Append("b").Append("c").ToString();
}
業界標準のベンチマークライブラリ。
17-3. Span / Memory
低割り当て・高速アクセスのためのビュー型。
ReadOnlySpan<char> span = "Hello, World".AsSpan(0, 5); // "Hello"
18. C# 8〜12の進化
8 (2019) Nullable RT、async streams、switch式、デフォルトinterface実装
9 (2020) record、init、top-level statements、target-typed new
10 (2021) file-scoped namespace、global usings、record struct
11 (2022) raw string、required members、generic math、list patterns
12 (2023) primary constructor、collection expressions [1, 2, 3]
19. よくある落とし穴FAQ
Q1. varを使いすぎていい?
ローカル変数で型が右辺から明らかならOK。可読性で判断。
Q2. == とEqualsは?
参照型で == は参照比較、ただし string は == がオーバーロードされ値比較。equals は値比較(オーバーライド次第)。
Q3. async voidはなぜBad?
例外がスタックを抜けて補足できない。テストもしにくい。async Task を使う。
Q4. structはいつ?
小さい(16 byte以下)・イミュータブル・値セマンティクスが望ましい場合。
Q5. Nullable RTは古いコードで?
警告まみれになる。段階的に有効化(#nullable enable)。
Q6. IDisposableとusing
IDisposable を実装したオブジェクトは using でラップ。
Q7. LINQのSelectManyって?
flatMap。入れ子のコレクションを平坦化。
Q8. Task.Runとawaitの違い
Task.Run はスレッドプールに投げる。await はノンブロッキング待機。CPUバウンドなら Task.Run、I/Oなら直接await。
Q9. dynamicは使うべき?
最終手段。型安全性を捨てるので避ける。COM相互運用などで使う。
Q10. C# vs Java
共通: クラスベースOOP、JIT、GC
違い:
C# はstructでユーザ値型
C# はLINQとasync/awaitが組み込み
C# はrecord / pattern matchingが強力
JavaはWORA、エコシステムはJavaの方が大きい
20. 図解: CLR / GC / async
CLRの動き:
.cs → Roslyn → CIL (.dll) → CLR → JIT → 機械語 → 実行
GC(世代別):
Gen 0 (新規) → Gen 1 → Gen 2 (長寿命)
+ LOH (大きいオブジェクト) + POH (固定)
async/awaitの変換:
async fn → コンパイラがステートマシンに変換
各awaitが状態として保存
Taskが完了したら次の状態へ
21. 学習ロードマップ(30日)
Week 1: 基礎
- 環境構築(dotnet CLI)、Hello World
- 型・var・nullable
- 制御フロー、switch式
Week 2: OOP
- クラス・継承・interface
- プロパティ・インデクサ
- ジェネリクス
Week 3: モダン
- LINQ
- async/await
- record / pattern matching
Week 4: 実装
22. 用語集
- CLR: Common Language Runtime
- CIL: Common Intermediate Language
- GAC: Global Assembly Cache
- AOT: Ahead-Of-Time compilation
- Task
: 非同期計算 - delegate: 関数ポインタの型安全版
- LINQ: Language Integrated Query
- record: イミュータブルデータクラス
- Nullable RT: Nullable Reference Types
発展: .NETと言語機能
ここからはC# の各機能を 実例とともに深掘り。日々のコードに直結するパターン、内部メカニズム、性能チューニングを網羅します。
23. 値型と参照型の深層
C# の型システムを正しく理解する鍵は 「値型と参照型の違い」を完全に把握すること。
23-1. メモリ配置
struct Point { public int X, Y; } // 値型
class Person { public string Name; } // 参照型
void Method() {
Point p = new Point { X = 1, Y = 2 }; // スタックに配置
Person person = new Person { Name = "A" }; // 参照はスタック、実体はヒープ
}
実際にはJITの最適化で 値型もヒープに置かれることがある(クロージャ捕捉など)が、原則として:
値型: スタック / レジスタ / 親オブジェクトのメモリに埋め込み
参照型: ヒープ + 参照(ポインタ)
23-2. ボクシング・アンボクシング
int x = 42;
object obj = x; // ボクシング: 値型がヒープにラップされる
int y = (int)obj; // アンボクシング
// パフォーマンスへの影響
List<object> list = new();
for (int i = 0; i < 1000; i++) list.Add(i); // 1000回のボクシング
ジェネリクスが2.0で導入された主な理由は ボクシングを避けるため。List<int> なら何も発生しません。
23-3. structのサイズ指針
推奨:
- 16 byte以下
- イミュータブル
- 値セマンティクスが論理的に正しい(数値、座標、日付など)
- 短命
避けるべき:
- 大きい(コピーコスト)
- ミュータブル(コピーで挙動が変わる)
- 継承を持つ
// Good
public readonly struct Point(int X, int Y);
public readonly struct Money(decimal Amount, string Currency);
// Bad
public struct LargeData {
public byte[] Buffer; // 配列はもともと参照型なので意味がない
public List<int> Items;
// ...100 fields...
}
23-4. ref struct
Span<T> などで使われる 「ヒープに置けない」 struct。
ref struct StackVector<T> {
public Span<T> Buffer;
// ...
}
ref struct は 絶対にヒープに置かれないことが保証されるので、Span<T> のような 「スタック上の配列ビュー」を安全に表現できる。クロージャに捕捉できない、asyncメソッドの中では使えないなど制約あり。
23-5. record class vs record struct
public record class Person(string Name, int Age); // 参照型
public record struct Point(int X, int Y); // 値型
record class は イミュータブルな参照型、record struct は イミュータブルな値型。両方とも Equals GetHashCode ToString を自動生成。
23-6. このセクションのまとめ
- 値型はスタック・参照型はヒープが基本
- ボクシングはパフォーマンス問題、ジェネリクスで回避
- structは小さい・イミュータブル・短命に
- ref structはヒープ禁止(Span<T>)
- record class / record structを使い分け
24. 文字列の詳細
C# の文字列はイミュータブルな参照型ですが、StringBuilder、string interpolation、Span
24-1. StringBuilder
var sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.Append("item ").Append(i).Append("\n");
}
string result = sb.ToString();
+ での連結は毎回新しいstringを作るので、ループではStringBuilder。
// Bad: O(n²)
string s = "";
for (int i = 0; i < n; i++) s += i;
// Good: O(n)
var sb = new StringBuilder();
for (int i = 0; i < n; i++) sb.Append(i);
string s = sb.ToString();
24-2. string interpolation
var name = "Alice";
var age = 30;
var msg = {{CONTENT}}quot;Hello, {name}! You are {age}.";
// フォーマット指定
var price = 1234.5;
var s = {{CONTENT}}quot;{price:C}"; // "$1,234.50"
var pct = {{CONTENT}}quot;{0.42:P0}"; // "42 %"
var hex = {{CONTENT}}quot;{255:X}"; // "FF"
// 桁数
var aligned = {{CONTENT}}quot;{42,10}"; // " 42"(右寄せ10桁)
var leftA = {{CONTENT}}quot;{42,-10}"; // "42 "
Interpolated string handlers(C# 10+)
// 性能向上のため、フォーマット文字列を「直接Spanに書き込む」最適化
Console.Write({{CONTENT}}quot;x = {x}"); // 内部でStringBuilder相当の効率化
24-3. Raw string literals(C# 11+)
var json = """
{
"name": "Alice",
"age": 30
}
""";
エスケープ不要。Pythonのトリプルクォートに相当。
24-4. Span とReadOnlySpan
ReadOnlySpan<char> span = "Hello, World".AsSpan(0, 5); // "Hello"
// 部分文字列を割り当てなしで取得
ReadOnlySpan<char> firstWord = "Hello World".AsSpan(0, 5);
// substringと違い、新しいstring不要
性能クリティカルな処理で 割り当てを完全回避できる。
24-5. 主要メソッド
var s = "Hello, World";
s.Length; // 12
s.ToUpper(); // "HELLO, WORLD"
s.ToLower();
s.Trim();
s.Replace(",", "");
s.Split(' '); // ["Hello,", "World"]
s.Contains("World");
s.StartsWith("Hello");
s.EndsWith("d");
s.IndexOf("World");
s.Substring(7); // "World"
s.Substring(7, 5);
string.Join(", ", new[] { "a", "b" });
string.IsNullOrEmpty(s);
string.IsNullOrWhiteSpace(s);
24-6. このセクションのまとめ
- 連結はStringBuilder
- {{CONTENT}}quot;" string interpolationで読みやすく
- """ raw string literal(C# 11+)
- Span<char> で割り当て回避
- IsNullOrEmpty / IsNullOrWhiteSpaceでチェック
25. プロパティとフィールドの深層
25-1. プロパティの内部
public class Person {
public string Name { get; set; } // 自動プロパティ
}
// コンパイラが生成するもの:
// private string <Name>k__BackingField;
// public string get_Name() { return <Name>k__BackingField; }
// public void set_Name(string value) { <Name>k__BackingField = value; }
プロパティは メソッドの糖衣構文。バイトコード上は get_ / set_ 関数になる。
25-2. init only setter(C# 9+)
public class Config {
public string Host { get; init; } // 初期化時のみセット可
public int Port { get; init; }
}
var c = new Config { Host = "localhost", Port = 8080 };
c.Host = "other"; // エラー!init only
required(C# 11+)
public class Config {
public required string Host { get; init; }
public required int Port { get; init; }
}
var c = new Config { Host = "localhost", Port = 8080 }; // OK
var c2 = new Config(); // エラー!requiredを初期化していない
25-3. expression-bodied properties
public class Circle {
public double Radius { get; }
public double Area => Math.PI * Radius * Radius;
public double Circumference => 2 * Math.PI * Radius;
}
計算プロパティを簡潔に。
25-4. プロパティアクセサの個別アクセス制御
public class Counter {
public int Value { get; private set; } // 読み取りはpublic、書き込みはprivate
}
25-5. このセクションのまとめ
- プロパティはgetter/setterメソッドの糖衣
- init only setter(C# 9+)でイミュータブル
- required(C# 11+)で必須プロパティ
- expression-bodiedで簡潔
- アクセサごとに修飾子可能
26. ジェネリクス深掘り
26-1. 共変・反変
public interface IProducer<out T> { // out: 共変
T Produce();
}
public interface IConsumer<in T> { // in: 反変
void Consume(T item);
}
IProducer<string> sp = ...;
IProducer<object> op = sp; // OK(共変)
IConsumer<object> oc = ...;
IConsumer<string> sc = oc; // OK(反変)
out/in は 「読み取り専用 / 書き込み専用」 の制約と組み合わせて型変換可能性を表現。IEnumerable<out T>、Action<in T> など標準で活用。
26-2. ジェネリック制約の組み合わせ
public class Repository<T>
where T : class, IEntity, new()
where TKey : struct, IComparable<TKey>
{
public T Create() => new T();
}
複数の制約を組み合わせ可能。
26-3. デフォルト値
public T GetOrDefault<T>() {
return default(T); // 値型なら0、参照型ならnull
// C# 7+
return default; // 型推論
}
26-4. ジェネリックメソッドの型推論
public T Identity<T>(T x) => x;
var n = Identity(42); // T = int推論
var s = Identity("hello"); // T = string
// 明示も可能
Identity<int>(42);
26-5. C# 11+ generic math
public T Sum<T>(IEnumerable<T> items) where T : INumber<T> {
T total = T.Zero;
foreach (var x in items) total += x;
return total;
}
Sum(new[] { 1, 2, 3 }); // intで動く
Sum(new[] { 1.5, 2.5 }); // doubleで動く
INumber<T> などの 静的抽象メンバーを持つinterfaceにより、数値型を抽象的に扱える。
26-6. このセクションのまとめ
- 共変 (out) / 反変 (in)
- 制約: where T : class, IFoo, new(), struct
- default(T) / default
- 型推論で多くの場面で <T> 不要
- C# 11+ でstatic abstract member(generic math)
27. LINQ深掘り
27-1. 遅延実行(deferred execution)
var query = numbers.Where(n => {
Console.WriteLine({{CONTENT}}quot;checking {n}");
return n > 2;
});
// ここまでは何も実行されていない
foreach (var n in query) { ... } // ここで初めて実行
LINQの 多くのメソッド(Where、Select、OrderBy)は遅延。ToList、ToArray、Count、First などで実行される。
27-2. IEnumerable vs IQueryable
IEnumerable<User> users1 = dbContext.Users.Where(u => u.Age > 18);
// ローカルでフィルタ(DBから全件取得後)
IQueryable<User> users2 = dbContext.Users.Where(u => u.Age > 18);
// SQLに翻訳される(DB側でフィルタ)
IQueryable<T> は 「LINQをSQLなどの式ツリーに変換」する。Entity Frameworkで重要。
27-3. メソッド構文vsクエリ構文
// メソッド構文
var result = users
.Where(u => u.Age >= 18)
.OrderBy(u => u.Name)
.Select(u => new { u.Name, u.Email });
// クエリ構文
var result = from u in users
where u.Age >= 18
orderby u.Name
select new { u.Name, u.Email };
両者は等価。複雑なjoinやグループ化はクエリ構文の方が読みやすい場合も。
27-4. グループ化
// メソッド構文
var byAge = users.GroupBy(u => u.AgeGroup);
foreach (var group in byAge) {
Console.WriteLine({{CONTENT}}quot;Group {group.Key}:");
foreach (var user in group) {
Console.WriteLine({{CONTENT}}quot; {user.Name}");
}
}
// クエリ構文
var byAge = from u in users
group u by u.AgeGroup;
GroupBy + Select
var stats = users
.GroupBy(u => u.Age)
.Select(g => new {
Age = g.Key,
Count = g.Count(),
AverageScore = g.Average(u => u.Score)
});
27-5. join
// メソッド構文
var query = users.Join(orders,
u => u.Id,
o => o.UserId,
(u, o) => new { u.Name, o.Total });
// クエリ構文
var query = from u in users
join o in orders on u.Id equals o.UserId
select new { u.Name, o.Total };
27-6. パフォーマンスの注意
// Bad: N+1問題
foreach (var user in users) {
var orderCount = orders.Count(o => o.UserId == user.Id); // 毎回フルスキャン
}
// Good: GroupByで一度集約
var counts = orders.GroupBy(o => o.UserId).ToDictionary(g => g.Key, g => g.Count());
foreach (var user in users) {
var orderCount = counts.GetValueOrDefault(user.Id);
}
27-7. ToList / ToArrayの使い分け
.ToList() // List<T>、追加可能
.ToArray() // T[]、固定長、軽量
.ToDictionary(k => k.Id)
.ToHashSet()
.ToImmutableList() // System.Collections.Immutable
「何度も列挙する」なら必ずどれかで物質化(materialize)する。
27-8. このセクションのまとめ
- 遅延実行: ToList / ToArrayで物質化
- IEnumerable vs IQueryable(DB実行か)
- GroupBy / Joinで集約
- 集計: Sum / Average / Min / Max / Count
- パフォーマンス: 何度も列挙しない、N+1を避ける
28. async / await深掘り
28-1. 状態機械への変換
public async Task<string> FetchAsync(string url) {
var client = new HttpClient();
var resp = await client.GetStringAsync(url);
return resp;
}
これがコンパイラによって 状態機械に変換される。各 await の前で状態を保存し、完了したら再開する。
TaskとTask
Task // 戻り値なし
Task<T> // Tを返す
ValueTask<T> // 性能用、割り当て削減(同期完了時)
ValueTask<T> は 「ほとんどの場合同期で完了する」シナリオでTaskの割り当てを節約する。乱用は逆効果。
28-2. 並行と並列
// 並行(並列ではない)
var t1 = FetchAsync("a");
var t2 = FetchAsync("b");
var t3 = FetchAsync("c");
var results = await Task.WhenAll(t1, t2, t3);
// 並列(CPU並列)
Parallel.ForEach(items, item => Process(item));
await Parallel.ForEachAsync(items, async (item, ct) => await ProcessAsync(item));
Task.WhenAll は I/O並行、Parallel.ForEach は CPU並列。
28-3. Channel(Producer/Consumer)
using System.Threading.Channels;
var channel = Channel.CreateBounded<int>(100);
// プロデューサ
_ = Task.Run(async () => {
for (int i = 0; i < 1000; i++) {
await channel.Writer.WriteAsync(i);
}
channel.Writer.Complete();
});
// コンシューマ
await foreach (var item in channel.Reader.ReadAllAsync()) {
Process(item);
}
Goのchannelに近いAPI。
28-4. CancellationTokenの伝播
public async Task DoWorkAsync(CancellationToken ct) {
await Task.Delay(1000, ct);
ct.ThrowIfCancellationRequested();
await SomeOtherOperationAsync(ct);
}
// 呼び出し側
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try {
await DoWorkAsync(cts.Token);
} catch (OperationCanceledException) {
// タイムアウト
}
CancellationToken を すべてのasync APIで第二引数として受け渡すのが規律。Goの context.Context 相当。
28-5. ConfigureAwait(false)
// ライブラリで
public async Task<T> LibraryMethodAsync() {
var data = await client.GetAsync(url).ConfigureAwait(false);
// 続きの処理
}
ConfigureAwait(false) で 元のSynchronizationContextに戻らない。UIスレッドへの不要な切り替えを避け、デッドロックを防ぐ。ライブラリでは付けるのが推奨。
ASP.NET CoreではSynchronizationContextがないので不要、というガイドラインもある。
28-6. asyncストリーム
public async IAsyncEnumerable<int> GenerateAsync(
[EnumeratorCancellation] CancellationToken ct = default
) {
for (int i = 0; i < 100; i++) {
await Task.Delay(100, ct);
yield return i;
}
}
await foreach (var x in GenerateAsync(cts.Token)) {
Console.WriteLine(x);
}
C# 8で 非同期ジェネレータが標準化。Stream API(リアクティブ)的に使える。
28-7. async voidの罠
public async void OnClick(...) { // Bad
await DoWorkAsync();
}
問題:
- 例外が補足できない(プロセスがクラッシュ)
- 完了を待てない
- テストしにくい
例外: イベントハンドラだけは仕方なく async void。それ以外は async Task。
28-8. このセクションのまとめ
- asyncは状態機械にコンパイル
- Task<T> / ValueTask<T>
- WhenAllで並行、Parallelで並列
- Channel<T> でproducer/consumer
- CancellationTokenを引数で伝播
- ConfigureAwait(false) はライブラリで
- async voidは避ける(イベントハンドラを除く)
29. Span とMemory
「割り当てゼロ」を実現する性能用ツール。
29-1. Span
Span<int> span = stackalloc int[10]; // スタックに確保
for (int i = 0; i < span.Length; i++) span[i] = i * i;
// 配列のスライス
int[] arr = new int[100];
Span<int> slice = arr.AsSpan(10, 20);
slice[0] = 42; // arr[10] が変わる
// 文字列のスライス
ReadOnlySpan<char> chars = "Hello, World".AsSpan(7, 5);
Span<T> は 任意の連続メモリのビュー。配列・スタック・unmanagedメモリを統一的に扱える。ヒープ割り当てなし。
29-2. Memory
// Span<T> はヒープに置けない(ref struct)
// asyncメソッドで使えない
// Memory<T> は ヒープ可、async OK
public async Task ProcessAsync(Memory<byte> data) {
await stream.ReadAsync(data);
// data.Spanでアクセス
var span = data.Span;
}
Memory<T> は Span<T> の 「保存可能版」。
29-3. 実用例: 高速パース
ReadOnlySpan<char> input = "1234,5678,9012".AsSpan();
while (!input.IsEmpty) {
int comma = input.IndexOf(',');
ReadOnlySpan<char> token = comma >= 0 ? input[..comma] : input;
int number = int.Parse(token); // Span版Parse、割り当てなし
Process(number);
input = comma >= 0 ? input[(comma + 1)..] : ReadOnlySpan<char>.Empty;
}
文字列の Split だと毎回配列を作るが、Spanベースなら ゼロ割り当て。
29-4. このセクションのまとめ
- Span<T>: 連続メモリのビュー、ref struct(ヒープ禁止)
- ReadOnlySpan<T>: 読み取り専用
- Memory<T>: ヒープ可、async可
- ゼロ割り当ての高速パース等で活用
30. リフレクションと属性
30-1. 型情報
var type = typeof(Person);
type.Name; // "Person"
type.FullName; // "MyApp.Person"
type.GetProperties(); // PropertyInfo[]
type.GetMethods();
type.GetCustomAttributes<MyAttribute>();
// インスタンスから
var p = new Person();
p.GetType(); // typeof(Person) と同じ
30-2. 動的呼び出し
var method = typeof(Person).GetMethod("Greet");
var result = method.Invoke(person, null);
// プロパティ
var prop = typeof(Person).GetProperty("Name");
prop.SetValue(person, "Bob");
prop.GetValue(person);
30-3. カスタム属性
[AttributeUsage(AttributeTargets.Property)]
public class ColumnAttribute : Attribute {
public string Name { get; }
public ColumnAttribute(string name) => Name = name;
}
public class User {
[Column("user_id")]
public int Id { get; set; }
[Column("user_name")]
public string Name { get; set; }
}
// 読み取り
var props = typeof(User).GetProperties();
foreach (var prop in props) {
var attr = prop.GetCustomAttribute<ColumnAttribute>();
if (attr != null) {
Console.WriteLine({{CONTENT}}quot;{prop.Name} -> {attr.Name}");
}
}
ORM、シリアライザ、バリデータなどで多用される。
30-4. Source Generator(C# 9+)
リフレクションの代替として コンパイル時にコードを生成する仕組み。
[Generator]
public class MyGenerator : IIncrementalGenerator {
public void Initialize(IncrementalGeneratorInitializationContext context) {
// クラスを見つけて、メソッドを追加生成
}
}
Json Serializer、Dapper、ASP.NET CoreなどがSource Generatorを活用してリフレクションのコストを削減。
30-5. このセクションのまとめ
- typeof / GetTypeでメタデータ
- リフレクションでメソッド・プロパティ動的呼び出し
- カスタム属性で宣言的メタデータ
- ORM/シリアライザ/バリデータで活用
- Source Generatorでコンパイル時生成(モダン)
31. delegate / event / Action / Func詳細
31-1. delegateの仕組み
public delegate int BinOp(int a, int b);
BinOp add = (a, b) => a + b;
BinOp mul = (a, b) => a * b;
// マルチキャストdelegate
BinOp combo = add;
combo += mul; // 両方呼ばれる、戻り値は最後のもの
combo(1, 2); // 3と2が両方呼ばれ、最後2が返る
FuncとActionの階層
Func<TResult>
Func<T1, TResult>
Func<T1, T2, TResult>
... 16引数まで
Action
Action<T1>
Action<T1, T2>
... 16引数まで
Predicate<T> // boolを返すFunc
31-2. eventの本質
public class Button {
public event EventHandler? Clicked;
protected virtual void OnClicked() {
Clicked?.Invoke(this, EventArgs.Empty);
}
}
event キーワードは 「外から += / -= しか使えない」delegate。
button.Clicked += MyHandler; // OK
button.Clicked -= MyHandler; // OK
button.Clicked = null; // エラー!外から代入禁止
button.Clicked(); // エラー!外から呼び出し禁止
「通知の発行はクラス側、購読は外」を強制する。
31-3. EventHandler
public class MyEventArgs : EventArgs {
public string Message { get; }
public MyEventArgs(string msg) => Message = msg;
}
public event EventHandler<MyEventArgs>? OnSomething;
カスタムイベント引数の標準パターン。
31-4. weak event
イベントハンドラが オブジェクトの寿命を不当に延ばす問題(メモリリークの典型例)。
button.Clicked += (s, e) => instance.Method();
// instanceがbuttonより長生きすると、buttonがGCされない
対策:
- 明示的に
-= - WeakEventManager(WPF)
IDisposableで購読解除を構造化
31-5. このセクションのまとめ
- delegateは型安全な関数ポインタ
- マルチキャストで複数を保持
- Func<T,R> / Action<T> / Predicate<T> が標準
- eventは外から += / -= のみ
- EventHandler<T> でカスタム引数
- 購読解除を忘れないとリークの元
32. パターンマッチング詳細
32-1. is演算子
if (obj is string s) {
Console.WriteLine(s.Length);
}
if (obj is int n and > 0) { ... }
if (obj is null) { ... }
if (obj is { Length: > 0 }) { ... }
32-2. switch式の網羅
public string Describe(Shape shape) => shape switch {
Circle { Radius: > 10 } c => {{CONTENT}}quot;big circle {c.Radius}",
Circle c => {{CONTENT}}quot;small circle {c.Radius}",
Square s => {{CONTENT}}quot;square {s.Side}",
null => "null",
_ => "unknown",
};
sealedと組み合わせ
// Javaのsealedのような完全網羅はC# にない(まだ)
// いずれにせよdefaultを書くのが安全
C# はJava/Rustのsealedのような コンパイル時の網羅性検証が完全には強制されません(CS8509警告は出る)。
32-3. プロパティパターン
public string Status(Order o) => o switch {
{ Total: 0 } => "empty",
{ Total: < 100, Items.Count: > 5 } => "small but many",
{ CreatedAt: var d } when d < DateTime.Today.AddDays(-30) => "old",
_ => "normal"
};
ネストした分解、ガード(when)、変数束縛が組み合わせ可能。
32-4. リストパターン(C# 11+)
int[] arr = ...;
var description = arr switch {
[] => "empty",
[var x] => {{CONTENT}}quot;single: {x}",
[var first, .., var last] => {{CONTENT}}quot;first={first}, last={last}",
[_, _, _] => "exactly 3",
_ => "other",
};
配列・リストのパターンマッチング。
32-5. このセクションのまとめ
- is演算子で型 + 束縛 + 条件
- switch式で網羅的分解
- プロパティパターン { Name: var n, ... }
- リストパターン(C# 11+)
- ガードwhen条件
33. デバッグとテスト
33-1. xUnit詳細
public class CalculatorTests {
private readonly Calculator _sut;
public CalculatorTests() { // コンストラクタがsetup
_sut = new Calculator();
}
[Fact]
public void Add_Positive() {
Assert.Equal(3, _sut.Add(1, 2));
}
[Theory]
[InlineData(1, 1, 2)]
[InlineData(2, 3, 5)]
[InlineData(-1, 1, 0)]
public void Add_Theory(int a, int b, int expected) {
Assert.Equal(expected, _sut.Add(a, b));
}
[Fact]
public void Add_Throws_OnOverflow() {
Assert.Throws<OverflowException>(() => _sut.Add(int.MaxValue, 1));
}
}
Fixture(共有セットアップ)
public class DatabaseFixture : IAsyncLifetime {
public Connection Conn { get; private set; }
public async Task InitializeAsync() {
Conn = await Connection.OpenAsync();
}
public Task DisposeAsync() => Conn.DisposeAsync().AsTask();
}
public class UserTests : IClassFixture<DatabaseFixture> {
private readonly DatabaseFixture _db;
public UserTests(DatabaseFixture db) => _db = db;
}
33-2. Moqでモック
public interface IRepository {
User GetUser(int id);
}
public class UserService {
private readonly IRepository _repo;
public UserService(IRepository repo) => _repo = repo;
public string GetName(int id) => _repo.GetUser(id)?.Name ?? "Unknown";
}
[Fact]
public void GetName_Test() {
var mock = new Mock<IRepository>();
mock.Setup(r => r.GetUser(1)).Returns(new User { Name = "Alice" });
var service = new UserService(mock.Object);
var name = service.GetName(1);
Assert.Equal("Alice", name);
mock.Verify(r => r.GetUser(1), Times.Once);
}
33-3. NSubstitute(Moqの代替)
var repo = Substitute.For<IRepository>();
repo.GetUser(1).Returns(new User { Name = "Alice" });
var service = new UserService(repo);
service.GetName(1);
repo.Received(1).GetUser(1);
より自然な構文。
33-4. Visual Studio / Riderのデバッガ
- ブレークポイント
- 条件付きブレークポイント
- ヒットカウント
- ログポイント(コードを変えずに出力)
- イミディエイトウィンドウで式評価
- ホットリロード(実行中にコード変更を反映)
33-5. このセクションのまとめ
- xUnitが現代の主流
- [Fact] と [Theory] + [InlineData]
- IClassFixtureで共有セットアップ
- Moq / NSubstituteでモック
- Visual Studio / Riderの対話的デバッガ
- ホットリロードで開発効率
34. .NETエコシステム
34-1. ASP.NET Core
// Program.cs(C# 9+ のtop-level statements)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddDbContext<AppDbContext>();
var app = builder.Build();
app.MapControllers();
app.Run();
最小限のWeb APIがほぼこれだけ。Spring BootやExpress.jsより簡潔。
[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase {
[HttpGet("{id}")]
public ActionResult<User> Get(int id) {
return _service.GetUser(id);
}
[HttpPost]
public ActionResult<User> Create(CreateUserDto dto) {
var user = _service.Create(dto);
return CreatedAtAction(nameof(Get), new { id = user.Id }, user);
}
}
34-2. Entity Framework Core
public class AppDbContext : DbContext {
public DbSet<User> Users { get; set; }
public DbSet<Order> Orders { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder o) {
o.UseNpgsql("Host=localhost;Database=mydb");
}
}
// Code-first migration
// dotnet ef migrations add InitialCreate
// dotnet ef database update
var users = await _db.Users
.Where(u => u.Age >= 18)
.Include(u => u.Orders)
.ToListAsync();
LINQ to SQLの現代版。Code-first / Migration対応。
34-3. MAUI(クロスプラットフォームUI)
public partial class MainPage : ContentPage {
public MainPage() {
InitializeComponent();
}
void OnCounterClicked(object sender, EventArgs e) {
count++;
CounterBtn.Text = {{CONTENT}}quot;Clicked {count} times";
}
}
iOS / Android / macOS / Windows統一コードベース。Xamarinの後継。
34-4. Blazor
@page "/counter"
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount() => currentCount++;
}
「C# でフロントエンド」を実現。WebAssemblyかServer-side rendering。
34-5. このセクションのまとめ
- ASP.NET Core: Web API / MVC
- EF Core: ORM
- MAUI: クロスプラットフォームUI
- Blazor: C# でフロント(WASM / Server)
- SignalR: リアルタイム通信
- Identity: 認証・認可
35. パフォーマンスチューニング
35-1. プロファイリング
- Visual Studio Profiler
- dotnet-trace(CLI)
- BenchmarkDotNet(マイクロベンチ)
- PerfView(Windows、ETWベース)
- JetBrains dotTrace
35-2. BenchmarkDotNet
[MemoryDiagnoser]
[SimpleJob(RuntimeMoniker.Net80)]
public class StringBenchmarks {
private string[] words = Enumerable.Range(0, 100)
.Select(i => {{CONTENT}}quot;word{i}").ToArray();
[Benchmark(Baseline = true)]
public string Concat() {
string s = "";
foreach (var w in words) s += w;
return s;
}
[Benchmark]
public string Builder() {
var sb = new StringBuilder();
foreach (var w in words) sb.Append(w);
return sb.ToString();
}
[Benchmark]
public string Join() {
return string.Join("", words);
}
}
| Method | Mean | Allocated | Ratio |
|---------|----------:|----------:|------:|
| Concat | 30.2 us | 89.6 KB | 1.00 |
| Builder | 3.4 us | 4.0 KB | 0.11 |
| Join | 1.8 us | 1.2 KB | 0.06 |
35-3. メモリ最適化
- structの活用(小さくイミュータブル)
- StringBuilder
- ArrayPool<T> でバッファ再利用
- Span<T> で割り当て削減
- recordよりclassの方が比較が安い場合も
- LINQのmaterializeを最小限に
ArrayPool
byte[] buffer = ArrayPool<byte>.Shared.Rent(1024);
try {
// 使う
} finally {
ArrayPool<byte>.Shared.Return(buffer);
}
35-4. AOT(Native AOT)
dotnet publish -c Release -p:PublishAot=true
JITを使わず 事前にネイティブコード化。
- 起動が瞬時(数ms)
- バイナリが自己完結(.NETランタイム同梱)
- リフレクション・dynamicに制約あり
サーバレス・CLIツール・コンテナ向け。
35-5. このセクションのまとめ
- BenchmarkDotNetで計測
- StringBuilder / Span / ArrayPool
- structで小さい値型
- AOTで起動瞬時
- LINQは便利だが多用は遅さの元
36. C# 拡張FAQ
Q1. varを使うべきか
右辺で型が明らかならOK。var x = process(); のように戻り値型が読み手に伝わらない場合は明示的に型を書く。
Q2. constとreadonly
const はコンパイル時定数(プリミティブ・stringのみ)。readonly は実行時に1回だけ初期化される。
Q3. structはいつ使う?
小さい(16 byte以下)・イミュータブル・値セマンティクス。それ以外はclass。
Q4. record class vs class
「データを保持するだけ」ならrecord。メソッドや状態管理が中心ならclass。
Q5. Nullable Reference Typesを有効化すべきか
新規プロジェクトでは必須。既存は段階的に。#nullable enable。
Q6. async voidはOK?
イベントハンドラ以外はNG。例外が補足できない、テストできない、完了を待てない。
Q7. ConfigureAwait(false) を毎回付けるべきか
ライブラリでは付ける。アプリ(特にASP.NET Core)では不要(SynchronizationContextがない)。
Q8. .Result / .Wait() を呼んでいい?
NG。デッドロックや例外の再ラップが起こる。await を使う。
Q9. TaskとValueTask
「ほぼ同期で完了する」場合だけValueTask。乱用すると逆効果。
Q10. dynamicは使うべきか
最終手段。COM相互運用やExpandoObjectを扱うときだけ。型安全性を捨てる。
Q11. lockとMutex
lock はプロセス内のクリティカルセクション。Mutex はプロセスをまたぐ(重い)。SemaphoreSlim はasync対応。
Q12. interface vs abstract class
interface: 契約のみ、複数実装可
abstract: 共通実装も持てる、単一継承
C# 8+ でinterfaceにdefault実装が書けるようになり、両者の差は縮まった。
Q13. partial class
ファイルを分割してクラス定義を書ける。Source Generatorや自動生成コードで重宝。
Q14. yield return
ジェネレータ。遅延評価でIEnumerable
public IEnumerable<int> Naturals() {
int n = 1;
while (true) yield return n++;
}
Q15. isとas
if (obj is string s) { ... } // 型チェック + 束縛
var s = obj as string; // キャスト、失敗でnull
var s = (string)obj; // キャスト、失敗で例外
Q16. unsafeコード
unsafe {
int x = 42;
int* p = &x;
}
ポインタ・ピン留めなど。FFIや性能クリティカルな場面で。
Q17. P/InvokeでCを呼ぶ
[DllImport("kernel32.dll")]
static extern uint GetTickCount();
ネイティブDLLの関数を呼べる。
Q18. T4テンプレート
コード生成テンプレート。Source Generatorが現代の代替。
Q19. ベンチマークでパフォーマンスを測る
BenchmarkDotNet 一択。Stopwatch での自前計測はJITを考慮できない。
Q20. なぜC# 12でprimary constructor
public class Person(string name, int age) {
public string Greet() => {{CONTENT}}quot;Hi, {name}";
}
Kotlin / Scalaに近い構文。プライマリコンストラクタの引数がフィールドの代わりに。
37. 拡張用語集
A〜Z(C# 関連)
- AOT: Ahead-Of-Time(事前ネイティブコンパイル)
- ASP.NET Core: 現代のWebフレームワーク
- Blazor: C# でフロントエンド
- CLR: Common Language Runtime
- CIL/MSIL: 中間言語
- EF Core: Entity Framework Core(ORM)
- GAC: Global Assembly Cache
- IL: Intermediate Language
- JIT: Just-In-Time
- LINQ: Language Integrated Query
- MAUI: Multi-platform App UI
- MSBuild: ビルドエンジン
- NuGet: パッケージマネージャ
- PInvoke: Platform Invoke(ネイティブ呼び出し)
- PMR: 該当機能なし、概念的にArrayPool
- Roslyn: C# コンパイラ
- Source Generator: コンパイル時コード生成
- Span
: メモリビュー、ref struct - xUnit: テストフレームワーク
38. 拡張学習ロードマップ(60日)
Phase 1: 基礎(Day 1-15)
- 環境(dotnet CLI、IDE)
- 型・var・null
- 制御フロー、switch式
- メソッド・プロパティ
- collections / LINQ基礎
Phase 2: OOP / 関数型(Day 16-30)
- クラス・継承・interface
- delegate / event
- LINQ深掘り
- async / await
- 例外処理
Phase 3: モダンC#(Day 31-45)
- record / sealed
- pattern matching
- Nullable Reference Types
- Span
/ Memory - generic math(C# 11+)
- primary constructor(C# 12)
Phase 4: 実践(Day 46-60)
実践: アプリケーション開発
40. 実用パターン集
40-1. Builderパターン
public class HttpRequest {
public required string Url { get; init; }
public string Method { get; init; } = "GET";
public Dictionary<string, string> Headers { get; init; } = new();
public string? Body { get; init; }
public class Builder {
private string _url = "";
private string _method = "GET";
private Dictionary<string, string> _headers = new();
private string? _body;
public Builder Url(string u) { _url = u; return this; }
public Builder Method(string m) { _method = m; return this; }
public Builder Header(string k, string v) {
_headers[k] = v;
return this;
}
public Builder Body(string b) { _body = b; return this; }
public HttpRequest Build() => new() {
Url = _url,
Method = _method,
Headers = _headers,
Body = _body
};
}
}
var req = new HttpRequest.Builder()
.Url("https://api.example.com")
.Method("POST")
.Header("Content-Type", "application/json")
.Body("{\"key\":\"value\"}")
.Build();
C# 11+ の required プロパティで強制も可能:
var req = new HttpRequest {
Url = "...", // requiredなので省略不可
Method = "POST",
Body = "..."
};
40-2. Disposableパターン
public class FileResource : IDisposable {
private FileStream? _stream;
private bool _disposed;
public FileResource(string path) {
_stream = File.Open(path, FileMode.Open);
}
public void Dispose() {
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing) {
if (_disposed) return;
if (disposing) {
_stream?.Dispose();
_stream = null;
}
_disposed = true;
}
~FileResource() => Dispose(false); // デストラクタ
}
// 使用
using (var res = new FileResource("data.txt")) {
// 使う
} // 自動Dispose
// C# 8+
using var res = new FileResource("data.txt"); // スコープ終了でDispose
IAsyncDisposable
public class AsyncResource : IAsyncDisposable {
public async ValueTask DisposeAsync() {
await CleanupAsync();
}
}
await using var res = new AsyncResource();
40-3. Result / Eitherパターン
C# 標準ではないので自作するか、OneOf などのライブラリ:
public abstract record Result<T, E> {
public record Ok(T Value) : Result<T, E>;
public record Err(E Error) : Result<T, E>;
public bool IsOk => this is Ok;
public T GetValueOrDefault(T defaultValue) =>
this is Ok ok ? ok.Value : defaultValue;
}
Result<int, string> Divide(int a, int b) =>
b == 0 ? new Result<int, string>.Err("zero") : new Result<int, string>.Ok(a / b);
var result = Divide(10, 2) switch {
Result<int, string>.Ok ok => {{CONTENT}}quot;value: {ok.Value}",
Result<int, string>.Err err => {{CONTENT}}quot;error: {err.Error}",
_ => "unknown"
};
40-4. Fluent API
var query = new QueryBuilder()
.From("users")
.Where("age > 18")
.OrderBy("name")
.Take(10)
.Build();
return this; で連鎖。EF Core、Configuration、Routingで多用。
40-5. Specificationパターン
public abstract class Specification<T> {
public abstract bool IsSatisfiedBy(T entity);
public Specification<T> And(Specification<T> other) =>
new AndSpec<T>(this, other);
public Specification<T> Or(Specification<T> other) =>
new OrSpec<T>(this, other);
}
public class AdultSpec : Specification<User> {
public override bool IsSatisfiedBy(User u) => u.Age >= 18;
}
public class ActiveSpec : Specification<User> {
public override bool IsSatisfiedBy(User u) => u.Active;
}
var spec = new AdultSpec().And(new ActiveSpec());
var validUsers = users.Where(spec.IsSatisfiedBy);
40-6. このセクションのまとめ
- Builderパターン(C# 11+ requiredで代替も)
- IDisposable / IAsyncDisposable
- Result / Either(OneOfライブラリ)
- Fluent API
- Specification
41. データベースアクセス(EF Core詳細)
41-1. DbContext
public class AppDbContext : DbContext {
public DbSet<User> Users { get; set; } = null!;
public DbSet<Order> Orders { get; set; } = null!;
protected override void OnConfiguring(DbContextOptionsBuilder o) {
o.UseNpgsql("Host=localhost;Database=mydb;Username=user;Password=pass");
}
protected override void OnModelCreating(ModelBuilder mb) {
mb.Entity<User>(b => {
b.HasKey(u => u.Id);
b.Property(u => u.Email).IsRequired().HasMaxLength(200);
b.HasIndex(u => u.Email).IsUnique();
b.HasMany(u => u.Orders).WithOne(o => o.User);
});
}
}
41-2. クエリ
// 単純な取得
var user = await _db.Users.FindAsync(1);
var user = await _db.Users.FirstOrDefaultAsync(u => u.Email == "a@b.com");
// 条件 + ソート
var adults = await _db.Users
.Where(u => u.Age >= 18)
.OrderBy(u => u.Name)
.ToListAsync();
// Includeで関連を取得
var userWithOrders = await _db.Users
.Include(u => u.Orders)
.ThenInclude(o => o.Items)
.FirstOrDefaultAsync(u => u.Id == 1);
// Projection
var summaries = await _db.Users
.Select(u => new { u.Name, OrderCount = u.Orders.Count })
.ToListAsync();
41-3. CUD(Create / Update / Delete)
// Create
var user = new User { Name = "Alice", Email = "a@b.com" };
_db.Users.Add(user);
await _db.SaveChangesAsync();
// Update
var existing = await _db.Users.FindAsync(1);
existing.Email = "new@b.com";
await _db.SaveChangesAsync(); // 変更追跡で自動的にUPDATE
// Delete
_db.Users.Remove(existing);
await _db.SaveChangesAsync();
// Bulk update(EF Core 7+)
await _db.Users
.Where(u => u.Active == false)
.ExecuteDeleteAsync();
await _db.Users
.Where(u => u.Age < 18)
.ExecuteUpdateAsync(s => s.SetProperty(u => u.IsMinor, true));
41-4. マイグレーション
dotnet ef migrations add InitialCreate
dotnet ef database update
# rollback
dotnet ef database update Previous
# script
dotnet ef migrations script
スキーマ変更がコードとして版管理される。
41-5. パフォーマンス考慮
N+1問題:
foreach (var user in users) Console.WriteLine(user.Orders.Count);
→ 各userでOrdersを別クエリでロード(遅い)
対策:
Include / Selectで一度に
AsNoTracking() で読み取り専用クエリ高速化
AsSplitQuery() で別クエリに分割(Cartesian Explosion対策)
// 読み取り専用、変更追跡なし(速い)
var users = await _db.Users.AsNoTracking().ToListAsync();
// 大量の関連をCartesian Explosionなく取得
var users = await _db.Users
.Include(u => u.Orders)
.Include(u => u.Addresses)
.AsSplitQuery()
.ToListAsync();
41-6. Raw SQL
var users = await _db.Users
.FromSqlInterpolated({{CONTENT}}quot;SELECT * FROM users WHERE age > {minAge}")
.ToListAsync();
// Stored procedure
var result = await _db.Database.ExecuteSqlInterpolatedAsync(
{{CONTENT}}quot;CALL update_users({param})");
EFだけで書きにくい複雑なクエリは生SQLで。
41-7. このセクションのまとめ
- DbContext + DbSet<T>
- Where / Include / Select / FirstAsync
- Add / Update / Remove / SaveChangesAsync
- ExecuteDelete / ExecuteUpdate(一括SQL)
- Migrationでスキーマバージョン管理
- AsNoTracking / AsSplitQueryで性能調整
- 必要ならFromSqlInterpolated
42. ASP.NET Core Web API詳細
42-1. ControllerベースのAPI
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase {
private readonly IUserService _service;
public UsersController(IUserService service) => _service = service;
[HttpGet]
public async Task<ActionResult<IEnumerable<UserDto>>> GetAll() {
var users = await _service.GetAllAsync();
return Ok(users);
}
[HttpGet("{id}")]
public async Task<ActionResult<UserDto>> Get(int id) {
var user = await _service.GetByIdAsync(id);
return user is null ? NotFound() : Ok(user);
}
[HttpPost]
public async Task<ActionResult<UserDto>> Create([FromBody] CreateUserDto dto) {
var user = await _service.CreateAsync(dto);
return CreatedAtAction(nameof(Get), new { id = user.Id }, user);
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, [FromBody] UpdateUserDto dto) {
await _service.UpdateAsync(id, dto);
return NoContent();
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id) {
await _service.DeleteAsync(id);
return NoContent();
}
}
42-2. Minimal API(C# 9+)
var app = WebApplication.CreateBuilder(args).Build();
app.MapGet("/users", async (IUserService svc) => await svc.GetAllAsync());
app.MapGet("/users/{id}", async (int id, IUserService svc) => {
var user = await svc.GetByIdAsync(id);
return user is null ? Results.NotFound() : Results.Ok(user);
});
app.MapPost("/users", async (CreateUserDto dto, IUserService svc) => {
var user = await svc.CreateAsync(dto);
return Results.Created({{CONTENT}}quot;/users/{user.Id}", user);
});
app.Run();
Express.js / Flaskに近いシンプルさ。マイクロサービスや小規模APIに最適。
42-3. Dependency Injection
var builder = WebApplication.CreateBuilder(args);
// サービス登録
builder.Services.AddScoped<IUserService, UserService>(); // リクエスト毎
builder.Services.AddSingleton<ICache, MemoryCache>(); // アプリ全体で1つ
builder.Services.AddTransient<IRandomService, RandomService>(); // 注入のたび新規
builder.Services.AddDbContext<AppDbContext>(o =>
o.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddHttpClient<IExternalApiClient, ExternalApiClient>(c => {
c.BaseAddress = new Uri("https://api.example.com");
});
var app = builder.Build();
ASP.NET Coreは DIが組み込み。Springの @Autowired のような外部フレームワーク不要。
42-4. ミドルウェア
app.Use(async (context, next) => {
var sw = Stopwatch.StartNew();
await next();
sw.Stop();
Console.WriteLine({{CONTENT}}quot;{context.Request.Path}: {sw.ElapsedMilliseconds}ms");
});
app.UseAuthentication();
app.UseAuthorization();
app.UseRouting();
app.UseStaticFiles();
リクエストパイプラインを コンポーザブルに組み立てる。
42-5. エンドポイントフィルタ(Minimal API)
app.MapGet("/users/{id}", async (int id, IUserService svc) => { ... })
.AddEndpointFilter(async (ctx, next) => {
// 前処理
var result = await next(ctx);
// 後処理
return result;
})
.RequireAuthorization()
.WithName("GetUser")
.WithOpenApi();
42-6. バリデーション
public record CreateUserDto(
[Required] [StringLength(100)] string Name,
[Required] [EmailAddress] string Email,
[Range(0, 150)] int Age
);
[ApiController] 属性付きのControllerでは 自動的に検証され、失敗すると400を返す。
FluentValidation
public class CreateUserValidator : AbstractValidator<CreateUserDto> {
public CreateUserValidator() {
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
RuleFor(x => x.Email).NotEmpty().EmailAddress();
RuleFor(x => x.Age).InclusiveBetween(0, 150);
}
}
DataAnnotationより柔軟。
42-7. 認証・認可
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(o => {
o.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidIssuer = "myapp",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret))
};
});
builder.Services.AddAuthorization(o => {
o.AddPolicy("AdminOnly", p => p.RequireRole("Admin"));
});
// Controllerで
[Authorize]
[Authorize(Policy = "AdminOnly")]
public class UsersController : ControllerBase { ... }
42-8. このセクションのまとめ
- Controller / Minimal APIの選択
- DIが言語標準(Scoped / Singleton / Transient)
- ミドルウェアでパイプライン
- DataAnnotations / FluentValidation
- JWT認証 + Policy認可
- OpenAPI / Swagger自動生成
43. ロギングと観測
43-1. 標準ILogger
public class UserService {
private readonly ILogger<UserService> _logger;
public UserService(ILogger<UserService> logger) => _logger = logger;
public async Task<User?> GetUserAsync(int id) {
_logger.LogInformation("Fetching user {UserId}", id);
try {
var user = await _repo.FindAsync(id);
if (user is null) {
_logger.LogWarning("User {UserId} not found", id);
return null;
}
return user;
} catch (Exception ex) {
_logger.LogError(ex, "Error fetching user {UserId}", id);
throw;
}
}
}
{UserId} のような 構造化ロギングを使うのが現代的。文字列補間({{CONTENT}}quot;...")より検索性が高い。
43-2. ログレベル
Trace詳細トレース
Debugデバッグ情報
Information通常のイベント
Warning警告(処理は続く)
Errorエラー(リカバリ可能)
Critical致命的(クラッシュ寸前)
43-3. Serilog
標準ILoggerより柔軟。
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo.Console()
.WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day)
.Enrich.FromLogContext()
.CreateLogger();
builder.Host.UseSerilog();
43-4. OpenTelemetry
分散トレーシング・メトリクスの業界標準。
builder.Services.AddOpenTelemetry()
.WithTracing(t => t
.AddAspNetCoreInstrumentation()
.AddHttpClientInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddOtlpExporter())
.WithMetrics(m => m
.AddAspNetCoreInstrumentation()
.AddRuntimeInstrumentation()
.AddPrometheusExporter());
43-5. このセクションのまとめ
- ILogger<T> + 構造化ロギング ({UserId})
- Trace / Debug / Info / Warn / Error / Critical
- SerilogでJSON / ファイル
- OpenTelemetryで分散観測
- ASP.NET Coreで組み込みサポート
44. テスト深掘り
44-1. テストの分類
Unit: 単一クラス・関数(依存をモック)
Integration: 複数コンポーネント(DB、Redis、外部API)
E2E: 本物のHTTP / UI経由
Performance: BenchmarkDotNet
44-2. WebApplicationFactory(統合テスト)
public class UsersApiTests : IClassFixture<WebApplicationFactory<Program>> {
private readonly HttpClient _client;
public UsersApiTests(WebApplicationFactory<Program> factory) {
_client = factory.WithWebHostBuilder(b => {
b.ConfigureServices(s => {
// 本番DIを上書きしてテスト用に
s.RemoveAll<DbContextOptions<AppDbContext>>();
s.AddDbContext<AppDbContext>(o => o.UseInMemoryDatabase("test"));
});
}).CreateClient();
}
[Fact]
public async Task GetUsers_ReturnsOk() {
var response = await _client.GetAsync("/api/users");
response.EnsureSuccessStatusCode();
}
}
44-3. Testcontainers
実DB(PostgreSQL、Redis)をコンテナで起動してテスト:
public class DatabaseFixture : IAsyncLifetime {
private readonly PostgreSqlContainer _container = new PostgreSqlBuilder()
.WithImage("postgres:16")
.WithDatabase("test")
.WithUsername("test")
.WithPassword("test")
.Build();
public string ConnectionString => _container.GetConnectionString();
public Task InitializeAsync() => _container.StartAsync();
public Task DisposeAsync() => _container.StopAsync();
}
44-4. Snapshotテスト(Verify)
[Fact]
public async Task User_Serialization() {
var user = new User { Name = "Alice", Age = 30 };
var json = JsonSerializer.Serialize(user);
await Verify(json); // 初回はファイル作成、以降は比較
}
期待値を別ファイルに保存し、変化したら検出。
44-5. Property-based testing(FsCheck)
[Property]
public bool Reverse_Twice_Returns_Original(int[] arr) {
return arr.Reverse().Reverse().SequenceEqual(arr);
}
ランダム入力で性質を検証。エッジケースを自動発見。
44-6. このセクションのまとめ
- xUnit + Moq / NSubstitute
- WebApplicationFactoryで統合テスト
- Testcontainersで実DB / Redis
- Verifyでスナップショット
- FsCheckでproperty-based
45. 配信とデプロイ
45-1. Dockerfile
# Multi-stage build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY *.csproj ./
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "MyApp.dll"]
Native AOT版
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -r linux-x64 -p:PublishAot=true -o /app
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0
COPY --from=build /app/MyApp /MyApp
ENTRYPOINT ["/MyApp"]
数MBのイメージ・瞬時起動。
45-2. Health checks
builder.Services.AddHealthChecks()
.AddCheck("self", () => HealthCheckResult.Healthy())
.AddDbContextCheck<AppDbContext>()
.AddRedis(redisConnection);
app.MapHealthChecks("/health");
app.MapHealthChecks("/health/ready", new HealthCheckOptions {
Predicate = check => check.Tags.Contains("ready")
});
Kubernetesのreadiness/liveness probeで使う。
45-3. Configuration
// appsettings.json
{
"ConnectionStrings": {
"Default": "Host=...;Database=..."
},
"Logging": { ... }
}
// 強い型で読む
public class AppSettings {
public string ConnectionString { get; set; } = "";
public int MaxRetries { get; set; } = 3;
}
builder.Services.Configure<AppSettings>(builder.Configuration.GetSection("App"));
public class MyService {
public MyService(IOptions<AppSettings> options) {
var settings = options.Value;
}
}
IOptions<T>, IOptionsSnapshot<T>, IOptionsMonitor<T> の3種類があり、それぞれライフサイクルが違う。
45-4. このセクションのまとめ
- Dockerfile + multi-stage
- Native AOTで軽量
- Health checks(Kubernetes連携)
- IOptions<T> で型安全な設定
46. C# 12 / .NET 8新機能ハイライト
46-1. primary constructor for class
// 従来
public class Service {
private readonly ILogger _logger;
public Service(ILogger logger) {
_logger = logger;
}
}
// C# 12
public class Service(ILogger logger) {
public void DoWork() {
logger.LogInformation("working");
}
}
Kotlin / Scalaに近い構文。コンストラクタの引数が クラス全体のスコープで使える。
46-2. collection expressions
int[] arr = [1, 2, 3, 4, 5];
List<int> list = [1, 2, 3];
Span<int> span = [1, 2, 3];
int[] combined = [..arr1, ..arr2, 99]; // スプレッド
JavaScript / Pythonに近い書き方。
46-3. ref readonly parameter
void Process(ref readonly LargeStruct data) { ... }
「変更しない大きい値型」を効率的に渡す。in パラメータの強化版。
46-4. .NET 8主要機能
- AOT安定化(リフレクションフリー化)
- Native AOTで1 MB級バイナリ
- Blazor Server + WASMのハイブリッド
- パフォーマンス全般5〜20% 改善
- C# 12言語機能
- System.Text.JsonのSource Generator強化
47. 用語拡張
- CLR: Common Language Runtime
- CIL: Common Intermediate Language
- JIT: Just-In-Time
- AOT: Ahead-Of-Time
- GC: Garbage Collector
- GAC: Global Assembly Cache
- NuGet: パッケージマネージャ
- EF Core: Entity Framework Core
- MAUI: Multi-platform App UI
- WPF: Windows Presentation Foundation
- MVVM: Model-View-ViewModel
- MVC: Model-View-Controller
- DI: Dependency Injection
- Roslyn: C# コンパイラ
- Source Generator: コンパイル時コード生成
- Span
: メモリビュー - Task: 非同期計算
- ValueTask: 軽量非同期
- Channel
: producer/consumer - Polly: リトライ・サーキットブレーカ
- MediatR: CQRS / メディエータ
- AutoMapper: オブジェクト変換
- Refit: HTTPクライアント生成
- Hangfire: バックグラウンドジョブ
- SignalR: リアルタイム通信
- OpenTelemetry: 分散観測
応用: 分散システムと運用
49. CQRSとMediatorパターン
複雑なアプリでは 「コマンドとクエリを分ける」設計が普及。MediatRライブラリが事実上の標準。
// コマンド(書き込み)
public record CreateUserCommand(string Name, string Email) : IRequest<int>;
public class CreateUserHandler : IRequestHandler<CreateUserCommand, int> {
private readonly AppDbContext _db;
public CreateUserHandler(AppDbContext db) => _db = db;
public async Task<int> Handle(CreateUserCommand cmd, CancellationToken ct) {
var user = new User { Name = cmd.Name, Email = cmd.Email };
_db.Users.Add(user);
await _db.SaveChangesAsync(ct);
return user.Id;
}
}
// クエリ(読み取り)
public record GetUserQuery(int Id) : IRequest<UserDto?>;
public class GetUserHandler : IRequestHandler<GetUserQuery, UserDto?> {
private readonly AppDbContext _db;
public GetUserHandler(AppDbContext db) => _db = db;
public async Task<UserDto?> Handle(GetUserQuery q, CancellationToken ct) {
return await _db.Users
.Where(u => u.Id == q.Id)
.Select(u => new UserDto(u.Id, u.Name, u.Email))
.FirstOrDefaultAsync(ct);
}
}
// Controller
[HttpPost]
public async Task<int> Create([FromBody] CreateUserCommand cmd) =>
await _mediator.Send(cmd);
[HttpGet("{id}")]
public async Task<UserDto?> Get(int id) =>
await _mediator.Send(new GetUserQuery(id));
各ハンドラが 小さな責務を持ち、テストしやすい。
50. Pollyでレジリエンス
外部API呼び出しは失敗する。Pollyでリトライ・サーキットブレーカ・タイムアウトを宣言的に。
var retryPolicy = Policy
.Handle<HttpRequestException>()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); // 指数バックオフ
var circuitBreaker = Policy
.Handle<Exception>()
.CircuitBreakerAsync(5, TimeSpan.FromSeconds(30));
var combined = Policy.WrapAsync(retryPolicy, circuitBreaker);
await combined.ExecuteAsync(async () => {
var resp = await client.GetAsync(url);
resp.EnsureSuccessStatusCode();
});
「ネットワークの不確実性」をコードで扱う標準。
51. SignalRでリアルタイム通信
// サーバ側
public class ChatHub : Hub {
public async Task SendMessage(string user, string message) {
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
}
builder.Services.AddSignalR();
app.MapHub<ChatHub>("/chat");
// クライアント (TypeScript)
const conn = new HubConnectionBuilder().withUrl("/chat").build();
conn.on("ReceiveMessage", (user, msg) => console.log(`${user}: ${msg}`));
await conn.start();
await conn.invoke("SendMessage", "Alice", "Hello");
WebSocket / Server-Sent Events / Long Pollingを抽象化。チャット・通知・ライブダッシュボードで使う。
52. 分散システム向けライブラリ
- gRPC: HTTP/2ベースRPC、protobuf経由(高性能)
- Refit: HTTPクライアント、interface定義から自動生成
- MassTransit: メッセージキュー(RabbitMQ、Azure Service Bus)
- Dapr: 分散アプリのサイドカー
- Orleans: アクターベース分散ランタイム
- YARP: リバースプロキシ
gRPC例
// proto定義
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
// サーバ
public class GreeterService : Greeter.GreeterBase {
public override Task<HelloReply> SayHello(HelloRequest req, ServerCallContext ctx) {
return Task.FromResult(new HelloReply { Message = {{CONTENT}}quot;Hello {req.Name}" });
}
}
builder.Services.AddGrpc();
app.MapGrpcService<GreeterService>();
Refit例
public interface IGitHubApi {
[Get("/users/{user}")]
Task<GitHubUser> GetUser(string user);
}
var api = RestService.For<IGitHubApi>("https://api.github.com");
var user = await api.GetUser("octocat");
53. クラウドネイティブC#
53-1. Azure Functions
[Function("HttpTrigger")]
public HttpResponseData Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req
) {
var resp = req.CreateResponse(HttpStatusCode.OK);
resp.WriteString("Hello, World");
return resp;
}
サーバレス関数。AOTで起動時間が劇的に短縮。
53-2. AWS Lambda
public class Function {
public async Task<APIGatewayProxyResponse> FunctionHandler(
APIGatewayProxyRequest request, ILambdaContext context
) {
return new APIGatewayProxyResponse {
StatusCode = 200,
Body = "Hello from Lambda"
};
}
}
53-3. Container Apps / ECS
ASP.NET CoreをDocker化してKubernetes / ECS / Container Appsへ。Native AOTで イメージ30 MB、起動50ms が現実的。
54. メモリプロファイル
54-1. dotnet-counters
dotnet-counters monitor --process-id <pid>
# 主要メトリクス
- cpu-usage
- working-set
- gc-heap-size
- gen-0/1/2-gc-count
- threadpool-thread-count
- exception-count
- monitor-lock-contention-count
54-2. dotnet-trace
dotnet-trace collect -p <pid> --providers Microsoft-DotNETCore-SampleProfiler
ETW / EventPipeベースの軽量プロファイル。
54-3. dotnet-dump
dotnet-dump collect -p <pid>
dotnet-dump analyze core_xxx
メモリダンプを取って後で分析。本番障害解析に。
54-4. JetBrains dotMemory / dotTrace
Rider統合の有料プロファイラ。最強クラスの可視化。
55. C#実務の補足知識
55-1. ファイルI/O
// 同期
var text = File.ReadAllText("data.txt");
var lines = File.ReadAllLines("data.txt");
File.WriteAllText("out.txt", text);
// 非同期
var text = await File.ReadAllTextAsync("data.txt");
await File.WriteAllTextAsync("out.txt", text);
// ストリーム
using var stream = File.OpenRead("data.bin");
var buf = new byte[4096];
int n;
while ((n = await stream.ReadAsync(buf)) > 0) {
Process(buf.AsSpan(0, n));
}
55-2. JSONシリアライゼーション
using System.Text.Json;
var user = new User { Name = "Alice", Age = 30 };
var json = JsonSerializer.Serialize(user);
var u = JsonSerializer.Deserialize<User>(json);
// オプション
var opts = new JsonSerializerOptions {
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
// 属性
public class User {
[JsonPropertyName("user_name")]
public string Name { get; set; }
[JsonIgnore]
public string InternalField { get; set; }
}
// Source Generator(高速・AOT対応)
[JsonSerializable(typeof(User))]
public partial class MyJsonContext : JsonSerializerContext {}
JsonSerializer.Serialize(user, MyJsonContext.Default.User);
55-3. 正規表現
// クラシック
var re = new Regex(@"^\d{3}-\d{4}-\d{4}{{CONTENT}}quot;);
re.IsMatch("090-1234-5678");
// Source Generator(C# 11+、コンパイル時生成、超高速)
public partial class PhoneRegex {
[GeneratedRegex(@"^\d{3}-\d{4}-\d{4}{{CONTENT}}quot;)]
public static partial Regex Phone();
}
PhoneRegex.Phone().IsMatch("090-1234-5678");
55-4. 並列処理
// CPU並列
Parallel.ForEach(items, item => Process(item));
Parallel.For(0, n, i => Compute(i));
await Parallel.ForEachAsync(items, async (item, ct) => {
await ProcessAsync(item, ct);
});
// LINQの並列
var result = items.AsParallel()
.Where(x => x.Value > 0)
.Select(x => Heavy(x))
.ToList();
AsParallel() で PLINQ。CPUバウンドな処理を自動的に並列化。
55-5. Reflection vs Source Generator
リフレクション:
- 実行時に型情報を解析
- 遅い、AOTに弱い
- コードが書きやすい
Source Generator:
- コンパイル時にコードを生成
- 速い、AOT対応
- 書くのが少し難しい
System.Text.Json、ASP.NET Core MVC、Refitなど、現代のライブラリはSource Generatorに移行しています。
56. 周辺ツール
56-1. Rider / Visual Studio
両者ともトップクラスのIDE。
- Visual Studio: Windows、無料のCommunity版あり
- Rider: クロスプラットフォーム、有料、Mac/Linuxに最適
56-2. dotnet-format
dotnet format
.editorconfig のルールに従って自動整形。
56-3. Roslynator
VSCode / Riderプラグイン。リファクタリング・分析の超強力版。
56-4. NDepend
商用の静的解析ツール。複雑度・依存・コードメトリクスを詳細分析。
56-5. JetBrains ReSharper
VSプラグイン版のRider機能。VS派なら必須。
57. C#補足トピック
57-1. C# vs Java(再考)
C# が優位:
- LINQ(JavaのStreamは劣る)
- async/await(より洗練)
- record / pattern matchingが深い
- structでユーザ値型
- Span<T> / ref struct
- Source Generator
- Native AOT
Javaが優位:
- エコシステムの大きさ
- 多OS・多デバイス対応の歴史
- JVM言語(Kotlin/Scala)の選択肢
- 巨大なエンタープライズコードベース
両者で大差ない:
- パフォーマンス
- 安全性
- GC
- IDE
- 学習しやすさ
57-2. C# vs TypeScript
C#:
サーバ・クライアント(Blazor)両方
CLRランタイム、巨大エコシステム
ジェネリクスが強力
TypeScript:
Node.js/ブラウザ(Vue/React)
V8ランタイム
構造的部分型(Duck Typing風)
両者は 「Anders Hejlsbergが設計した言語」で多くの共通点があります。
57-3. C# vs Python
C#:
静的型、コンパイル、JIT/AOT
業務システム
ゲーム(Unity)
Python:
動的型、インタプリタ
データサイエンス・ML
スクリプト
異なる用途に最適化されているが、双方とも汎用言語として使える。
上級: ランタイムと相互運用
59. 同期プリミティブ詳細
59-1. lock vs Monitor
private readonly object _lock = new();
lock (_lock) {
// クリティカルセクション
}
// 等価
Monitor.Enter(_lock);
try {
// クリティカルセクション
} finally {
Monitor.Exit(_lock);
}
lock は Monitor の糖衣構文。
59-2. Mutex(プロセス間)
using var mutex = new Mutex(false, "Global\\MyAppMutex");
if (mutex.WaitOne(TimeSpan.FromSeconds(5))) {
try {
// クリティカル
} finally {
mutex.ReleaseMutex();
}
}
プロセスをまたぐロック。重いので必要なときだけ。
59-3. SemaphoreSlim(async対応)
private readonly SemaphoreSlim _sem = new(initialCount: 5, maxCount: 5);
await _sem.WaitAsync();
try {
// 同時実行数を5に制限
} finally {
_sem.Release();
}
「最大N並列」を実現する。HTTPクライアントのレートリミットなどに。
59-4. ReaderWriterLockSlim
private readonly ReaderWriterLockSlim _rwLock = new();
// 読み取り(複数並行可)
_rwLock.EnterReadLock();
try { return _data; }
finally { _rwLock.ExitReadLock(); }
// 書き込み(単独)
_rwLock.EnterWriteLock();
try { _data = newValue; }
finally { _rwLock.ExitWriteLock(); }
読み取りが多いシナリオで lock より速い。
59-5. Interlocked(アトミック操作)
int counter = 0;
Interlocked.Increment(ref counter);
Interlocked.Decrement(ref counter);
Interlocked.Add(ref counter, 5);
Interlocked.Exchange(ref counter, 100);
Interlocked.CompareExchange(ref counter, newValue, expected);
CASベースのロックフリー操作。
59-6. ConcurrentDictionary
var dict = new ConcurrentDictionary<string, int>();
dict.TryAdd("a", 1);
dict.TryUpdate("a", newValue: 2, comparisonValue: 1);
dict.AddOrUpdate("a", 1, (key, old) => old + 1);
int value = dict.GetOrAdd("a", k => ComputeDefault(k));
スレッドセーフな辞書。Dictionary<K,V> は スレッドセーフではないので並行アクセスはエラーの元。
59-7. このセクションのまとめ
- lock = Monitorの糖衣
- SemaphoreSlimで並列度制限(async OK)
- ReaderWriterLockSlimで読み多い場面
- Interlockedでアトミック
- ConcurrentDictionaryなどスレッドセーフコレクション
60. dynamicと表現ツリー
60-1. dynamicキーワード
dynamic d = SomeMethod();
d.AnyMethod(); // 実行時に解決
int x = d.SomeProperty;
実行時バインディング。型安全性を捨てる代わりに柔軟性を得る。
ExpandoObject
dynamic obj = new ExpandoObject();
obj.Name = "Alice";
obj.Age = 30;
obj.Greet = (Action)(() => Console.WriteLine({{CONTENT}}quot;Hi, {obj.Name}"));
obj.Greet(); // "Hi, Alice"
obj.NewProperty = 42; // 動的に追加
PythonのdictやJSのobjectのような感覚。
60-2. 表現ツリー(Expression Tree)
Expression<Func<int, int>> expr = x => x + 1;
// expr.BodyはBinaryExpression(x + 1)
// expr.Parametersは [ParameterExpression(x)]
// 動的にコンパイル
var func = expr.Compile();
func(5); // 6
LINQ to SQL / EF Coreが 「LINQ式をSQLに変換」する仕組みの基盤。
// 動的にツリーを組み立てる
var param = Expression.Parameter(typeof(int), "x");
var body = Expression.Add(param, Expression.Constant(1));
var lambda = Expression.Lambda<Func<int, int>>(body, param);
var func = lambda.Compile();
ORM・シリアライザ・動的コード生成で重要。
60-3. このセクションのまとめ
- dynamicで実行時バインディング
- ExpandoObjectで動的プロパティ
- Expression Treeで「コードをデータとして」扱う
- LINQ to SQLの基盤
61. アンマネージドコードとの相互運用
61-1. P/Invoke
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern int MessageBox(IntPtr hWnd, string text, string caption, int type);
MessageBox(IntPtr.Zero, "Hello", "Title", 0);
C/C++ DLLの関数を呼ぶ。Windows API、ネイティブライブラリで多用。
61-2. unsafeコード
unsafe void ProcessArray(int[] arr) {
fixed (int* p = arr) { // GCで動かないように固定
for (int* current = p; current < p + arr.Length; current++) {
*current *= 2;
}
}
}
unsafe ブロック内で ポインタ操作が可能。性能クリティカルやFFIで使う。
61-3. Marshalling
[StructLayout(LayoutKind.Sequential)]
public struct Point {
public int X;
public int Y;
}
[DllImport("native.dll")]
static extern void ProcessPoint(ref Point p);
C構造体とのレイアウト互換。
61-4. NativeAOT制約
AOTで使えない:
- 動的な型生成
- 一部のリフレクション
- 動的アセンブリ読み込み
代替:
- Source Generator
- 静的に型を解決
61-5. このセクションのまとめ
- P/Invokeでネイティブ呼び出し
- unsafe + fixedでポインタ
- StructLayoutでレイアウト指定
- NativeAOTで制約あり
62. 拡張FAQ(追加)
Q21. C# 12のprimary constructorで気をつけることは?
引数が クラス全体のスコープで見える。フィールドにせず引数のままなので、ライフサイクルは「インスタンスが生きる限り」。
Q22. recordとclass、どっち?
record:
データ保持中心、イミュータブル
Equals/GetHashCode/ToString自動
class:
状態を持つ、メソッド中心
値の等価性が要らない
Q23. structで == を使える?
record struct は自動生成。普通の struct は明示的に Equals をオーバーライド。
Q24. nullableとNullable
int? x; // Nullable<int>(値型のnull表現)
string? s; // 参照型のnull表現(NRT機能)
両者は 見た目同じだが内部実装が違う。値型は Nullable<T> 構造体、参照型はコンパイラ警告。
Q25. yield returnとreturnの違い
IEnumerable<int> Naturals() {
int i = 1;
while (true) yield return i++; // 一つずつ生成
}
yield return で 遅延評価のジェネレータ。
Q26. partial classはいつ?
ファイルを分割してクラス定義可能。Source GeneratorやWinFormsデザイナで使われる。
Q27. ref / out / inの違い
void f(int x); // 値渡し
void f(ref int x); // 参照渡し(呼び出し前に初期化必須)
void f(out int x); // 出力(メソッド内で代入必須)
void f(in int x); // 読み取り専用参照渡し(性能)
Q28. async lambdaは?
Func<Task<int>> f = async () => {
await Task.Delay(100);
return 42;
};
async をlambdaに付けてTaskを返せる。
Q29. checked / unchecked
int max = int.MaxValue;
checked { max + 1; } // OverflowException
unchecked { max + 1; } // -2147483648(wrap)
整数オーバーフローの挙動制御。デフォルトはunchecked。
Q30. ファイナライザ(~ClassName)
class Foo {
~Foo() {
// GC時に呼ばれる(タイミング不定)
}
}
ほぼ使わない。リソース解放は IDisposable で。
Q31. virtual / abstract / override / sealed / new
virtual: オーバーライド可能
abstract: 実装必須
override: 親のvirtualを上書き
sealed: これ以上オーバーライド不可
new: 親のメソッドを「隠蔽」(推奨されない)
Q32. isとasの違い
if (obj is User u) { ... } // チェック + 束縛、安全
var u = obj as User; // キャスト、失敗でnull
var u = (User)obj; // キャスト、失敗で例外
Q33. uncheckedにすると速い?
ほぼ変わらない。可読性 / 安全性で選ぶ。
Q34. 大きいstructを返すのは遅い?
in パラメータか ref readonly で参照渡し。またはclassに変える。
Q35. dotnet efの代替
EF Core 7+ では ExecuteUpdateAsync / ExecuteDeleteAsync でrawに近い書き方も可。Dapper(軽量ORM)も人気。
Q36. Source Generatorのデバッグ
Debugger.Launch() を入れると、ビルド時にデバッガがアタッチされる(Visual Studio)。
Q37. .csprojの最小
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
</Project>
これだけで動く。<ImplicitUsings>enable</ImplicitUsings> は using System; 等を暗黙化。
Q38. nameof演算子
public string GetName() => nameof(GetName); // "GetName"
nameof(MyClass.Property); // "Property"
リファクタリング時に文字列が壊れない。
Q39. typeof / GetType / nameof
typeof(string) // System.String、コンパイル時
"hello".GetType() // 同じ、実行時
nameof(string) // "string"、文字列
Q40. C# でゲーム?
Unityが圧倒的シェア。Stride、MonoGame、Godot(C# 公式サポート)も選択肢。
63. 完全な例: Web APIプロジェクト
実プロジェクト相当のコード構成例。
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(o =>
o.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddAuthentication("Bearer").AddJwtBearer();
builder.Services.AddAuthorization();
builder.Services.AddOpenTelemetry()
.WithTracing(t => t.AddAspNetCoreInstrumentation()
.AddEntityFrameworkCoreInstrumentation()
.AddOtlpExporter());
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");
app.Run();
// Domain/User.cs
public class User {
public int Id { get; set; }
public required string Name { get; set; }
public required string Email { get; set; }
public DateTime CreatedAt { get; set; }
}
// Data/AppDbContext.cs
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options) {
public DbSet<User> Users => Set<User>();
protected override void OnModelCreating(ModelBuilder mb) {
mb.Entity<User>(b => {
b.HasKey(u => u.Id);
b.Property(u => u.Email).HasMaxLength(200).IsRequired();
b.HasIndex(u => u.Email).IsUnique();
});
}
}
// Repositories/IUserRepository.cs
public interface IUserRepository {
Task<User?> FindByIdAsync(int id, CancellationToken ct);
Task<User> CreateAsync(User user, CancellationToken ct);
}
public class UserRepository(AppDbContext db) : IUserRepository {
public Task<User?> FindByIdAsync(int id, CancellationToken ct) =>
db.Users.FindAsync(new object[] { id }, ct).AsTask();
public async Task<User> CreateAsync(User user, CancellationToken ct) {
user.CreatedAt = DateTime.UtcNow;
db.Users.Add(user);
await db.SaveChangesAsync(ct);
return user;
}
}
// Services/IUserService.cs
public interface IUserService {
Task<UserDto?> GetUserAsync(int id, CancellationToken ct);
Task<UserDto> CreateUserAsync(CreateUserDto dto, CancellationToken ct);
}
public record UserDto(int Id, string Name, string Email);
public record CreateUserDto(string Name, string Email);
public class UserService(IUserRepository repo, ILogger<UserService> logger) : IUserService {
public async Task<UserDto?> GetUserAsync(int id, CancellationToken ct) {
logger.LogInformation("Fetching user {UserId}", id);
var user = await repo.FindByIdAsync(id, ct);
return user is null ? null : new UserDto(user.Id, user.Name, user.Email);
}
public async Task<UserDto> CreateUserAsync(CreateUserDto dto, CancellationToken ct) {
var user = await repo.CreateAsync(new User { Name = dto.Name, Email = dto.Email }, ct);
return new UserDto(user.Id, user.Name, user.Email);
}
}
// Controllers/UsersController.cs
[ApiController]
[Route("api/users")]
public class UsersController(IUserService svc) : ControllerBase {
[HttpGet("{id}")]
public async Task<ActionResult<UserDto>> Get(int id, CancellationToken ct) {
var user = await svc.GetUserAsync(id, ct);
return user is null ? NotFound() : Ok(user);
}
[HttpPost]
public async Task<ActionResult<UserDto>> Create([FromBody] CreateUserDto dto, CancellationToken ct) {
var user = await svc.CreateUserAsync(dto, ct);
return CreatedAtAction(nameof(Get), new { id = user.Id }, user);
}
}
これだけで本格的なWeb API。Repository → Service → Controllerの 3層アーキテクチャを踏襲。
65. 実践チェックリスト
新規プロジェクトを始めるときに
☐ .NET 8 LTS(または最新LTS)を使う
☐ <Nullable>enable</Nullable>
☐ <ImplicitUsings>enable</ImplicitUsings>
☐ <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
☐ EditorConfigで規約を統一
☐ dotnet formatをCIに組み込み
☐ xUnit + コードカバレッジ
☐ GitHub Actions / Azure DevOpsでCI
☐ .gitignoreは標準テンプレート
☐ README + LICENSE
コード品質チェック
☐ asyncメソッドはAsyncサフィックス
☐ CancellationTokenを引数で伝播
☐ ConfigureAwait(false) はライブラリで
☐ async voidは使わない(イベントハンドラ除く)
☐ .Result / .Wait() は使わない
☐ structは小さく・イミュータブル
☐ recordは値オブジェクトに
☐ NRT(Nullable RT)警告ゼロを維持
☐ LINQの重複列挙を避ける(ToListでキャッシュ)
☐ EF CoreのN+1問題に注意
パフォーマンス改善
☐ BenchmarkDotNetで測定
☐ Span<T> / ArrayPoolで割り当て削減
☐ Source Generatorで静的化
☐ AOT公開を検討
☐ GCログを確認(dotnet-counters)
☐ asyncでブロッキング呼び出しを避ける
☐ ConcurrentDictionaryはオーバーヘッドあり、必要なときだけ
セキュリティ
☐ 入力バリデーション(DataAnnotations / FluentValidation)
☐ HTTPS強制(UseHttpsRedirection)
☐ CSRF / XSS対策
☐ SQL Injection(EF Coreが回避、生SQL注意)
☐ シークレットをappsettings.jsonにハードコードしない
☐ Azure Key Vault / AWS Secrets Manager
☐ 依存ライブラリの脆弱性スキャン(dotnet list package --vulnerable)
66. 学習リソース
書籍
- 『C# in Depth』Jon Skeet
- 『Pro C# 10』Andrew Troelsen
- 『Concurrency in C# Cookbook』Stephen Cleary
- 『Pro ASP.NET Core』Adam Freeman
- 『Programming Entity Framework』Julia Lerman
Web
- learn.microsoft.com/dotnet/csharp
- devblogs.microsoft.com/dotnet
- stackoverflow.com [c#] タグ
- /r/csharp(Reddit)
コミュニティ
- .NET Conf(年次オンラインカンファレンス)
- C# Discord
- Roslyn GitHub
学習サイト
- exercism.io
- Pluralsight
- Codecademy
- Microsoft Learnの無料C# パス
67. C#学習の次のステップ
C# は 6ヶ月ごとに進化する言語。今書いている本もC# 13、14、15と新版が出るでしょう。しかし基礎は変わりません: 静的型・OOP・LINQ・async/await・record/sealed/pattern matching。
新機能を追いかけることも大事ですが、基礎を磨くことの方が長期的に効きます。
// Hello, world.
Console.WriteLine("Hello, C#.");
最後に、Anders Hejlsberg(C#・TypeScriptの設計者)の言葉:
“Programming languages are the user interface of computation.”
C# は、そのUIとして 最高品質の選択肢のひとつです。
C# 12.0 の最新機能と.NET 8アーキテクチャ
C# 12.0 と .NET 8 (2023年11月リリース)
Microsoft Learn(learn.microsoft.com)では、C# 12.0は言語の成熟度を示す安定性と生産性向上に焦点が当たっています。
C# 12.0は .NET 8 にバンドルされ、長期サポート(LTS)版として5年間のサポートが保証されます(learn.microsoft.com)。
C# 12 の主要な新機能
- Primary Constructors - 自動フィールド割り当て
// C# 11 以前
public class User {
private readonly string _name;
public User(string name) => _name = name;
}
// C# 12
public class User(string name) {
public string Name => name;
}
- Collection Expressions - 簡潔な初期化
// 従来
var items = new List<int> { 1, 2, 3, 4, 5 };
// C# 12
int[] items = [1, 2, 3, 4, 5];
Span<int> span = [1, 2, 3];
List<int> list = [..items]; // スプレッド演算子
- Inline Arrays - スタック配列
public struct Buffer {
public Span<byte> Data => buffer;
private byte[10] buffer; // スタック上に10バイト確保
}
- ref readonly parameters - パフォーマンス最適化
public void ProcessData(ref readonly Span<byte> data) {
// data は参照渡し(コピーなし)、読み取り専用
}
CLR(Common Language Runtime)のメモリ管理
.NETアプリケーションは、OSレベルのメモリ管理から隔離されます(learn.microsoft.com)。
ガベージコレクション(GC)の世代別管理
.NET GC は generational GC を採用:
Gen 0: 新しいオブジェクト (すぐ死ぬ傾向)
Gen 1: 1回のGC後も生き残ったもの
Gen 2: 複数のGC後も生き残ったもの (長寿)
毎回の GC:
- Gen 0 をスキャン (頻繁)
- Gen 1, Gen 2 は低頻度
理由: 若いオブジェクトほど死ぬ確率が高い
マネージヒープのレイアウト
[Gen0 領域] [Gen1 領域] [Gen2 領域] [LOH: Large Object Heap]
大きなオブジェクト(85KB以上)は LOH に配置、GC圧力を軽減。
Span とMemory の使い分け
C# 7.2 で導入された Span/Memory により、ゼロコピー操作が可能に:
// 従来: 新しい配列にコピー
byte[] buffer = new byte[1000];
byte[] slice = new byte[100];
Array.Copy(buffer, 0, slice, 0, 100); // コピーコスト!
// Span<T>: ビュー(参照)
Span<byte> view = new Span<byte>(buffer, 0, 100);
// スタック、ヒープ、固定メモリどれも指せる
Span
型消去(Type Erasure)とリフレクション
C#はコンパイル時に型チェック後、実行時は .NET メタデータを保有:
Type t = typeof(List<string>);
Console.WriteLine(t.Name); // "List`1"
var method = t.GetMethod("Add");
method.Invoke(list, new[] { "hello" });
リフレクションは便利ですが、AOT(Ahead-of-Time)コンパイルでは困難(クラウドネイティブで課題)。
NativeAOT
NativeAOTは、C#コードを事前にネイティブコードへコンパイルし、起動時間や配布サイズを抑えるための選択肢です。
// .NET 8 では以下が可能:
// 1. C# コード → ネイティブコンパイル(.exe, .so)
// 2. リフレクション非依存の AOT
// 3. Docker イメージサイズ 98% 削減
NativeAOT は、サーバーレス(AWS Lambda)やコンテナで重要。
まとめ
C#は、静的型付け、オブジェクト指向、非同期処理、LINQ、.NETエコシステムを組み合わせて、大規模な業務アプリケーションからWeb API、デスクトップ、ゲーム開発まで幅広く扱える言語です。構文を覚えるだけでなく、型設計、例外処理、非同期処理、パッケージ管理、テストを一続きで理解すると、保守しやすいC#コードを書きやすくなります。