| name | dotnet-type-design-performance |
| description | .NETの型をパフォーマンス重視で設計する。sealed クラス、readonly 構造体、 静的関数、遅延列挙、不変コレクションを活用。 新しい型を設計するとき、性能をレビューするとき、または class / struct / record の使い分けを判断するとき。
|
このドキュメントは dotnet-type-design-performance の日本語版です。
.NETにおけるパフォーマンスのための型設計
JIT最適化を最大化し、アロケーションを最小化し、明確なAPI意図を伝える.NET型設計の実践パターン。シーリング、値型、純粋関数、遅延列挙、コレクション返却型をカバーします。
ゴール駆動で使うため、最初に達成したいゴール、成功条件、確認手段を短く固定します。
When to Use This Skill
以下の場合にこのスキルを使用してください:
- 新しい型を設計し、class / struct / record の選択を行うとき
- 既存コードのパフォーマンス改善機会をレビューするとき
- ホットパスでコレクションやenumerableを扱うとき
- .NETプロジェクトの型設計規約を確立するとき
- 非同期戻り値型(
Task vs ValueTask)を選択するとき
前提条件:
- .NET 6+ プロジェクト
- C# 型システム(class, struct, record)の基本的な理解
Related Skills
dotnet — .NET family の入口
git-commit — パフォーマンス重要な変更をアトミックに分けて記録
Core Principles
- デフォルトでシール — 継承を意図して設計していない限り、型をsealしてJITのdevirtualizationを有効化(基礎と型)
- 小さなデータには値セマンティクス — 小さな不変データにはreadonly構造体を使い、ヒープアロケーションを回避(基礎と型)
- 依存の明示 — すべての入力を可視化する静的純粋関数を優先(ニュートラル)
- マテリアライズの遅延 — 必要になるまで列挙しない。呼び出し側にマテリアライズのタイミングを委ねる(余白の設計)
- 不変なAPI境界 — パブリックAPIからは
IReadOnlyList<T>を返し、意図しない変更を防止(成長の複利)
Why — なぜこれらの原則が重要か:
.NETのJITコンパイラは「型が何であるか」を知ることで最適化を行います。sealedクラスはvtableルックアップを排除し、readonly構造体は防御コピーを排除します。これは「基礎と型」の哲学そのものです — 正しい型を選ぶことが、すべての応用(パフォーマンス最適化)の基盤になります。
また、APIから不変コレクションを返すことは「成長の複利」です。最初は面倒に感じても、長期的にバグを防ぎ、信頼性を積み上げます。
Workflow: Design Types for Performance
Step 1 — デフォルトでクラスをシールする
継承を意図していない新しいクラスやレコードを作成するときに使用します。
シーリングによりJITのdevirtualizationが有効になります — コンパイラがvtableルックアップの代わりにメソッド呼び出しをインライン化できます。
public sealed class OrderProcessor
{
public void Process(Order order) { }
}
public sealed record OrderCreated(OrderId Id, CustomerId CustomerId);
public class OrderProcessor
{
public virtual void Process(Order order) { }
}
Why: sealedは「このクラスは拡張ポイントではない」と明示的に宣言します。JITはこの情報を使ってメソッド呼び出しを最適化します。型を明示する行為そのものが、基礎と型の実践です。
Values: 基礎と型(型を明示することで意図が伝わり、JITが最適化できる)
Step 2 — 値型にはReadonly構造体を使用する
小さな不変データ型を値セマンティクスで定義するときに使用します。
構造体をreadonlyにマークすることで、in参照渡しやreadonlyフィールドに格納された際の防御コピーを防ぎます。
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString();
}
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
}
Why: C#コンパイラは、readonlyではない構造体がreadonly文脈で使用されると、自動的に防御コピーを作成します。これはコードからは見えませんが、メモリ使用量を2倍にし、パフォーマンスを低下させます。正しい型選択(readonly)がこの問題の根本的な解決策です。
| 構造体を使う場面 | クラスを使う場面 |
|---|
| 小さい(≤ 16バイト) | 大きなオブジェクト |
| 短命 | 長期間・共有参照 |
| 値セマンティクス | アイデンティティセマンティクス |
| 不変 | ミュータブルな状態が必要 |
Values: 基礎と型(正しい型選択が防御コピーを排除し、性能の基盤となる)
Step 3 — 静的純粋関数を優先する
副作用なしで入力を変換するロジックを書くときに使用します。
隠れた状態を持たない静的メソッドは高速(vtableルックアップなし)で、本質的にスレッドセーフで、テストが容易です。
public static class OrderCalculator
{
public static Money CalculateTotal(
IReadOnlyList<OrderItem> items,
decimal taxRate,
decimal discountPercent)
{
var subtotal = items.Sum(i => i.Price * i.Quantity);
var discounted = subtotal * (1 - discountPercent / 100);
var total = discounted * (1 + taxRate / 100);
return new Money(total, "USD");
}
}
public class OrderCalculator
{
private readonly ITaxService _taxService;
private readonly IDiscountService _discountService;
public Money CalculateTotal(IReadOnlyList<OrderItem> items)
{
}
}
Why: 依存を隠さず明示することは「ニュートラルな視点」の実践です。誰がコードを読んでも、そのメソッドが何を必要とし、何を返すかが一目で分かります。これは個人の好みではなく、普遍的に理解しやすい設計です。
Values: ニュートラル(依存を隠さず明示することで、誰でも理解できる普遍的な設計になる)
Step 4 — 列挙を遅延させる
LINQクエリやコレクションパイプラインを扱うときに使用します。
必要になるまでenumerableをマテリアライズしないでください。まず操作をチェーンし、最後に.ToList()を一度だけ呼びます。
public IReadOnlyList<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.ToList()
.OrderBy(o => o.CreatedAt)
.ToList();
}
public IReadOnlyList<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt)
.ToList();
}
public IEnumerable<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt);
}
Why: 列挙を遅延させることは「余白の設計」です。呼び出し側に「いつデータを確定するか」という判断の余白を残します。すべてを先に確定してしまうと、柔軟性が失われます。
Values: 余白の設計(列挙を遅延させることで、呼び出し側に判断の余白を残す)
Step 5 — コレクション返却型を選択する
パブリックAPIのコレクション返却型を定義するときに使用します。
パブリックAPIからは不変なコレクションインターフェースを返します。構築中は内部的にミュータブルな型を使用して構いません。
public IReadOnlyList<Order> GetOrders()
{
return _orders.ToList();
}
private static readonly FrozenDictionary<string, Handler> _handlers =
new Dictionary<string, Handler>
{
["create"] = new CreateHandler(),
["update"] = new UpdateHandler(),
}.ToFrozenDictionary();
public List<Order> GetOrders()
{
return _orders;
}
Why: 不変なAPIを積み上げることは「成長の複利」です。最初の実装では少し手間がかかりますが、プロジェクトが成長するにつれて、バグの防止効果が複利的に増加します。
| シナリオ | 返却型 |
|---|
| API境界 | IReadOnlyList<T>, IReadOnlyCollection<T> |
| 静的ルックアップ | FrozenDictionary<K,V>, FrozenSet<T> |
| 内部構築 | List<T> → readonlyとして返却 |
| 単一またはなし | T?(nullable) |
| 0個以上、遅延 | IEnumerable<T> |
Values: 成長の複利(不変なAPIは変更に強く、長期的な信頼性を積み上げる)
Good Practices
1. 新しいクラスはデフォルトでシール
✅ すべての新しいクラスにsealedを付けることから始めます。継承の具体的な必要性が生じたときだけ外します。「拡張に開く」という判断を偶然ではなく明示的にします。
2. IDにはreadonly record structを優先
✅ ドメイン識別子(OrderId、CustomerId)にはreadonly record structを使用します。値の等価性、ToString()、ゼロヒープアロケーションを一つの宣言で得られます。
3. パイプライン末尾でSingle ToList
✅ すべてのLINQ操作(Where、OrderBy、Select)をチェーンしてから、.ToList()を一度だけ呼びます。中間の.ToList()はそれぞれ新しいリストを割り当て、シーケンス全体を反復します。
4. キャッシュされたホットパスにはValueTaskを使用
✅ メソッドが同期的に返すことが多い場合(例:キャッシュヒット)、ValueTask<T>を使ってTaskのアロケーションを回避します。常に非同期のI/O操作にはTask<T>を使用します。詳細は応用パターンを参照。
Common Pitfalls
1. ミュータブルな構造体による防御コピー
readonlyではない構造体がreadonlyフィールドに格納されたり、inで渡されたりすると、コンパイラは変更を防ぐために隠れたコピーを作成します。これはメモリ使用量を無言で2倍にします。
解決策: 変更が意図的に必要な場合を除き、常に構造体をreadonlyにマークします。
2. パブリックAPIからList<T>を返す
List<T>を公開すると、呼び出し側がAdd()、Remove()、Clear()で内部コレクションを操作でき、カプセル化が壊れます。
解決策: IReadOnlyList<T>またはIReadOnlyCollection<T>を返します。
3. 非同期パイプラインでの早期列挙
.Select(async o => await ProcessAsync(o)).ToList()はアイテムごとに1つのTaskを作成し、即座にマテリアライズします。
解決策: ストリーミングにはIAsyncEnumerable<T>、バッチ並列処理にはTask.WhenAllを使用します。詳細は応用パターンを参照。
Anti-Patterns
public class OrderService { }
public struct Point { public int X; public int Y; }
public int Add(int a, int b) => a + b;
items.Where(...).ToList().OrderBy(...).ToList();
public List<Order> GetOrders();
public ValueTask<Order> CreateOrderAsync();
Quick Reference
| パターン | 効果 |
|---|
sealed class | Devirtualization、明確なAPI意図 |
readonly record struct | 防御コピーなし、値セマンティクス |
| 静的純粋関数 | vtableなし、テスト可能、スレッドセーフ |
.ToList()の遅延 | 一度のマテリアライズ、アロケーション削減 |
ホットパスにValueTask | 同期時のTaskアロケーション回避 |
バイトにSpan<T> | スタックアロケーション、コピーなし |
IReadOnlyList<T>返却 | 不変なAPI契約 |
FrozenDictionary | 静的データ最速ルックアップ(.NET 8+) |
Resources