type-design-performance について
C# 開発者向けに、型と API をパフォーマンス重視で設計するスキルです。型の選択、列挙、非同期値、メモリ、コレクションの扱いを解説します。
主な機能
- 01型を既定で sealed にする
- 02readonly 構造体の選択
- 03静的な純粋関数の使用
- 04列挙の遅延
- 05非同期型とメモリ型の選択
- 06読み取り専用コレクションの返却
活用シーン
- 01新しい C# 型と API の設計
- 02コードのパフォーマンス問題の確認
- 03クラス、構造体、レコードの選択
- 04コレクションと enumerable の処理
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ aaronontheweb/ dotnet-skills --skill type-design-performanceインストール後、Claude Code で/type-design-performanceを入力して読み込まれたか確認します。
詳細
パフォーマンスを考慮した型設計
このスキルを使う場面
次の場合にこのスキルを使います:
- 新しい型や API を設計するとき
- パフォーマンス上の問題がないかコードをレビューするとき
- class、struct、record のどれを選ぶか検討するとき
- コレクションや列挙可能なオブジェクトを扱うとき
基本原則
- 型は sealed にする - 継承を明示的に想定して設計する場合を除きます
- readonly struct を優先する - 小さく、イミュータブルな値型に使います
- static な純粋関数を優先する - パフォーマンスとテスト容易性が向上します
- 列挙を遅延する - 必要になるまで実体化しません
- イミュータブルなコレクションを返す - API の境界で使用します
クラスはデフォルトで sealed にする
クラスを sealed にすると、JIT によるデバーチャライズが可能になり、API の意図も明確になります。
// DO: Seal classes not designed for inheritance
public sealed class OrderProcessor
{
public void Process(Order order) { }
}
// DO: Seal records (they're classes)
public sealed record OrderCreated(OrderId Id, CustomerId CustomerId);
// DON'T: Leave unsealed without reason
public class OrderProcessor // Can be subclassed - intentional?
{
public virtual void Process(Order order) { } // Virtual = slower
}
メリット:
- JIT がメソッド呼び出しをデバーチャライズできます
- 「拡張ポイントではない」ことを明示できます
- 意図しない破壊的変更を防げます
値型には readonly struct を使う
struct がイミュータブルな場合は readonly にします。これにより、防御的コピーを防げます。
// DO: Readonly struct for immutable value types
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString();
}
// DO: Readonly struct for small, short-lived data
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
// DON'T: Mutable struct (causes defensive copies)
public struct Point // Not readonly!
{
public int X { get; set; } // Mutable!
public int Y { get; set; }
}
struct を使う場面
| struct を使う場合 | class を使う場合 |
|---|---|
| 小さい(通常 ≤16 bytes) | 大きなオブジェクト |
| 短命 | 長命 |
| 頻繁に割り当てる | 参照を共有する必要がある |
| 値セマンティクスが必要 | ID セマンティクスが必要 |
| イミュータブル | 可変状態 |
static な純粋関数を優先する
副作用のない static メソッドは、より高速でテストしやすくなります。
// DO: Static pure function
public static class OrderCalculator
{
public static Money CalculateTotal(IReadOnlyList<OrderItem> items)
{
var total = items.Sum(i => i.Price * i.Quantity);
return new Money(total, "USD");
}
}
// Usage - predictable, testable
var total = OrderCalculator.CalculateTotal(items);
メリット:
- vtable の参照が不要(高速)
- 隠れた状態がない
- テストしやすい(純粋な入力 → 出力)
- 設計上スレッドセーフ
- 依存関係の明示を促す
// DON'T: Instance method hiding dependencies
public class OrderCalculator
{
private readonly ITaxService _taxService; // Hidden dependency
private readonly IDiscountService _discountService; // Hidden dependency
public Money CalculateTotal(IReadOnlyList<OrderItem> items)
{
// What does this actually depend on?
}
}
// BETTER: Explicit dependencies via parameters
public static class OrderCalculator
{
public static Money CalculateTotal(
IReadOnlyList<OrderItem> items,
decimal taxRate,
decimal discountPercent)
{
// All inputs visible
}
}
やりすぎには注意 - 状態やポリモーフィズムが本当に必要な場合は、インスタンスメソッドを使います。
列挙を遅延する
必要になるまで列挙可能なオブジェクトを実体化しないでください。LINQ チェーンを過剰に使うのも避けます。
// BAD: Premature materialization
public IReadOnlyList<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.ToList() // Materialized!
.OrderBy(o => o.CreatedAt) // Another iteration
.ToList(); // Materialized again!
}
// GOOD: Defer until the end
public IReadOnlyList<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt)
.ToList(); // Single materialization
}
// GOOD: Return IEnumerable if caller might not need all items
public IEnumerable<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt);
// Caller decides when to materialize
}
非同期列挙
非同期処理と IEnumerable の併用には注意してください:
// BAD: Async in LINQ - hidden allocations
var results = orders
.Select(async o => await ProcessOrderAsync(o)) // Task per item!
.ToList();
await Task.WhenAll(results);
// GOOD: Use IAsyncEnumerable for streaming
public async IAsyncEnumerable<OrderResult> ProcessOrdersAsync(
IEnumerable<Order> orders,
[EnumeratorCancellation] CancellationToken ct = default)
{
foreach (var order in orders)
{
ct.ThrowIfCancellationRequested();
yield return await ProcessOrderAsync(order, ct);
}
}
// GOOD: Batch processing for parallelism
var results = await Task.WhenAll(
orders.Select(o => ProcessOrderAsync(o)));
ValueTask と Task
同期的に完了することが多いホットパスには ValueTask を使います。実際の I/O には、通常どおり Task を使います。
// DO: ValueTask for cached/synchronous paths
public ValueTask<User?> GetUserAsync(UserId id)
{
if (_cache.TryGetValue(id, out var user))
{
return ValueTask.FromResult<User?>(user); // No allocation
}
return new ValueTask<User?>(FetchUserAsync(id));
}
// DO: Task for real I/O (simpler, no footguns)
public Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
{
// This always hits the database
return _repository.CreateAsync(cmd);
}
ValueTask のルール:
- ValueTask を複数回 await しないでください
- 完了前に
.Resultや.GetAwaiter().GetResult()を使わないでください - 迷ったら Task を使います
バイト列には Span と Memory を使う
低レベルの操作では、byte[] の代わりに Span<T> と Memory<T> を使います。
// DO: Accept Span for synchronous operations
public static int ParseInt(ReadOnlySpan<char> text)
{
return int.Parse(text);
}
// DO: Accept Memory for async operations
public async Task WriteAsync(ReadOnlyMemory<byte> data)
{
await _stream.WriteAsync(data);
}
// DON'T: Force array allocation
public static int ParseInt(string text) // String allocated
{
return int.Parse(text);
}
よく使われる Span のパターン
// Slice without allocation
ReadOnlySpan<char> span = "Hello, World!".AsSpan();
var hello = span[..5]; // No allocation
// Stack allocation for small buffers
Span<byte> buffer = stackalloc byte[256];
// Use ArrayPool for larger buffers
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try
{
// Use buffer...
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
コレクションの戻り値の型
API からはイミュータブルなコレクションを返す
// DO: Return immutable collection
public IReadOnlyList<Order> GetOrders()
{
return _orders.ToList(); // Caller can't modify internal state
}
// DO: Use frozen collections for static data (.NET 8+)
private static readonly FrozenDictionary<string, Handler> _handlers =
new Dictionary<string, Handler>
{
["create"] = new CreateHandler(),
["update"] = new UpdateHandler(),
}.ToFrozenDictionary();
// DON'T: Return mutable collection
public List<Order> GetOrders()
{
return _orders; // Caller can modify!
}
内部での変更は問題ありません
public IReadOnlyList<OrderItem> BuildOrderItems(Cart cart)
{
var items = new List<OrderItem>(); // Mutable internally
foreach (var cartItem in cart.Items)
{
items.Add(CreateOrderItem(cartItem));
}
return items; // Return as IReadOnlyList
}
コレクションのガイドライン
| シナリオ | 戻り値の型 |
|---|---|
| API の境界 | IReadOnlyList<T>、IReadOnlyCollection<T> |
| 静的な検索データ | FrozenDictionary<K,V>、FrozenSet<T> |
| 内部での構築 | List<T> を使い、その後読み取り専用として返す |
| 単一の項目またはなし | T?(nullable) |
| 0 個以上、遅延評価 | IEnumerable<T> |
クイックリファレンス
| パターン | メリット |
|---|---|
sealed class | デバーチャライズ、明確な API |
readonly record struct | 防御的コピーなし、値セマンティクス |
| static な純粋関数 | vtable なし、テスト可能、スレッドセーフ |
.ToList() を遅延する | 実体化が 1 回で済む |
ホットパスに ValueTask | Task の割り当てを回避 |
バイト列に Span<T> | スタック割り当て、コピーなし |
IReadOnlyList<T> を返す | イミュータブルな API 契約 |
FrozenDictionary | 静的データで最速の検索 |
アンチパターン
// DON'T: Unsealed class without reason
public class OrderService { } // Seal it!
// DON'T: Mutable struct
public struct Point { public int X; public int Y; } // Make readonly
// DON'T: Instance method that could be static
public int Add(int a, int b) => a + b; // Make static
// DON'T: Multiple ToList() calls
items.Where(...).ToList().OrderBy(...).ToList(); // One ToList at end
// DON'T: Return List<T> from public API
public List<Order> GetOrders(); // Return IReadOnlyList<T>
// DON'T: ValueTask for always-async operations
public ValueTask<Order> CreateOrderAsync(); // Just use Task
リソース
コメント
Markdown 対応