パフォーマンスのためのタイプ設計
このスキルを使用するタイミング
以下の場合にこのスキルを使用します:
- 新しいタイプとAPIを設計するとき
- コードのパフォーマンス問題をレビューするとき
- class、struct、recordを選択するとき
- コレクションと列挙可能なオブジェクトを扱うとき
核となる原則
- タイプをシールする - 明示的に継承を目的としていない限り
- readonly structを優先する - 小さく不変な値型に対して
- 静的純粋関数を優先する - パフォーマンスとテスト容易性が向上
- 列挙を遅延する - 必要になるまで具体化しない
- 不変コレクションを返す - API境界から
デフォルトでクラスをシールする
クラスをシールすることで、JITのデバーチャリゼーションが可能になり、APIの意図が伝わります。
// 推奨:継承を目的としないクラスはシールする
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) { } // Virtual = 低速
}
利点:
- JITがメソッド呼び出しをデバーチャライズできる
- 「これは拡張ポイントではない」ことを伝える
- 意図しない破壊的変更を防ぐ
値型に対するReadonly Struct
structは不変の場合にreadonlyにする必要があります。これにより防御的コピーを防ぎます。
// 推奨:不変な値型にはreadonly struct
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
public override string ToString() => Value.ToString();
}
// 推奨:小さく短命なデータにはreadonly struct
public readonly struct Money
{
public decimal Amount { get; }
public string Currency { get; }
public Money(decimal amount, string currency)
{
Amount = amount;
Currency = currency;
}
}
// 避ける:可変struct(防御的コピーの原因)
public struct Point // readonlyではない!
{
public int X { get; set; } // 可変!
public int Y { get; set; }
}
Structを使用する場合
| Structを使用する場合 | Classを使用する場合 |
|---|---|
| 小さい(通常16バイト以下) | より大きなオブジェクト |
| 短命 | 長命 |
| 頻繁に割り当てられる | 共有参照が必要 |
| 値セマンティクスが必要 | 同一性セマンティクスが必要 |
| 不変 | 可変状態 |
静的純粋関数を優先する
副作用のない静的メソッドは、より高速でテストしやすくなります。
// 推奨:静的純粋関数
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");
}
}
// 使用法 - 予測可能でテスト可能
var total = OrderCalculator.CalculateTotal(items);
利点:
- vtableルックアップなし(高速)
- 隠れた状態なし
- テストが容易(純粋な入力→出力)
- 設計上スレッドセーフ
- 依存関係を明示的に強制
// 避ける:依存関係を隠すインスタンスメソッド
public class OrderCalculator
{
private readonly ITaxService _taxService; // 隠れた依存関係
private readonly IDiscountService _discountService; // 隠れた依存関係
public Money CalculateTotal(IReadOnlyList<OrderItem> items)
{
// 実際に何に依存しているのか?
}
}
// より良い:パラメーターで依存関係を明示
public static class OrderCalculator
{
public static Money CalculateTotal(
IReadOnlyList<OrderItem> items,
decimal taxRate,
decimal discountPercent)
{
// すべての入力が可視
}
}
やりすぎに注意 - 状態やポリモーフィズムが本当に必要な場合はインスタンスメソッドを使用します。
列挙を遅延する
必要な時まで列挙可能なオブジェクトを具体化しない。過剰なLINQチェーンを避ける。
// 悪い:早期の具体化
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(); // 1回の具体化
}
// 良い:呼び出し側がすべてのアイテムを必要としないかもしれない場合、IEnumerableを返す
public IEnumerable<Order> GetActiveOrders()
{
return _orders
.Where(o => o.IsActive)
.OrderBy(o => o.CreatedAt);
// 呼び出し側が具体化するタイミングを決定
}
非同期列挙
asyncとIEnumerableでは注意が必要:
// 悪い:LINQでの非同期 - 隠れた割り当て
var results = orders
.Select(async o => await ProcessOrderAsync(o)) // アイテムごとにTask!
.ToList();
await Task.WhenAll(results);
// 良い:ストリーミングにIAsyncEnumerableを使用
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);
}
}
// 良い:並列処理のためのバッチ処理
var results = await Task.WhenAll(
orders.Select(o => ProcessOrderAsync(o)));
ValueTask vs Task
同期的に完了することが多いホットパスにはValueTaskを使用します。実際のI/Oには単にTaskを使用します。
// 推奨:キャッシュ/同期的パスにはValueTask
public ValueTask<User?> GetUserAsync(UserId id)
{
if (_cache.TryGetValue(id, out var user))
{
return ValueTask.FromResult<User?>(user); // 割り当てなし
}
return new ValueTask<User?>(FetchUserAsync(id));
}
// 推奨:実際のI/OにはTask(よりシンプル、落とし穴なし)
public Task<Order> CreateOrderAsync(CreateOrderCommand cmd)
{
// これは常にデータベースにアクセスする
return _repository.CreateAsync(cmd);
}
ValueTaskのルール:
- ValueTaskを2回以上awaitしない
- 完了前に
.Resultや.GetAwaiter().GetResult()を使用しない - 迷ったらTaskを使用する
バイト操作用のSpanとMemory
低レベル操作にはbyte[]の代わりにSpan<T>とMemory<T>を使用します。
// 推奨:同期操作にはSpanを受け入れる
public static int ParseInt(ReadOnlySpan<char> text)
{
return int.Parse(text);
}
// 推奨:非同期操作にはMemoryを受け入れる
public async Task WriteAsync(ReadOnlyMemory<byte> data)
{
await _stream.WriteAsync(data);
}
// 避ける:配列の割り当てを強制
public static int ParseInt(string text) // 文字列が割り当てられる
{
return int.Parse(text);
}
一般的なSpanパターン
// 割り当てなしでスライス
ReadOnlySpan<char> span = "Hello, World!".AsSpan();
var hello = span[..5]; // 割り当てなし
// 小さなバッファ用のスタック割り当て
Span<byte> buffer = stackalloc byte[256];
// より大きなバッファにはArrayPoolを使用
var buffer = ArrayPool<byte>.Shared.Rent(4096);
try
{
// バッファを使用...
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
コレクションの戻り値の型
APIからは不変コレクションを返す
// 推奨:不変コレクションを返す
public IReadOnlyList<Order> GetOrders()
{
return _orders.ToList(); // 呼び出し側は内部状態を変更できない
}
// 推奨:静的データには凍結コレクションを使用(.NET 8以降)
private static readonly FrozenDictionary<string, Handler> _handlers =
new Dictionary<string, Handler>
{
["create"] = new CreateHandler(),
["update"] = new UpdateHandler(),
}.ToFrozenDictionary();
// 避ける:可変コレクションを返す
public List<Order> GetOrders()
{
return _orders; // 呼び出し側が変更可能!
}
内部での可変性は問題ない
public IReadOnlyList<OrderItem> BuildOrderItems(Cart cart)
{
var items = new List<OrderItem>(); // 内部的には可変
foreach (var cartItem in cart.Items)
{
items.Add(CreateOrderItem(cartItem));
}
return items; // IReadOnlyListとして返す
}
コレクションのガイドライン
| シナリオ | 戻り値の型 |
|---|---|
| API境界 | IReadOnlyList<T>, IReadOnlyCollection<T> |
| 静的ルックアップデータ | FrozenDictionary<K,V>, FrozenSet<T> |
| 内部構築 | List<T>、その後読み取り専用として返す |
| 単一アイテムまたはなし | T? (nullable) |
| 0個以上、遅延 | IEnumerable<T> |
クイックリファレンス
| パターン | 利点 |
|---|---|
sealed class | デバーチャリゼーション、明確なAPI |
readonly record struct | 防御的コピーなし、値セマンティクス |
| 静的純粋関数 | vtableなし、テスト可能、スレッドセーフ |
.ToList()の遅延 | 1回の具体化 |
ホットパスにValueTask | Taskの割り当て回避 |
バイト操作用のSpan<T> | スタック割り当て、コピーなし |
IReadOnlyList<T>の戻り値 | 不変のAPIコントラクト |
FrozenDictionary | 静的データの最速ルックアップ |
アンチパターン
// 避ける:理由なくシールされていないクラス
public class OrderService { } // シールしてください!
// 避ける:可変struct
public struct Point { public int X; public int Y; } // readonlyにしてください
// 避ける:静的でよいインスタンスメソッド
public int Add(int a, int b) => a + b; // 静的にしてください
// 避ける:複数のToList()呼び出し
items.Where(...).ToList().OrderBy(...).ToList(); // 末尾に1つのToList
// 避ける:パブリックAPIからList<T>を返す
public List<Order> GetOrders(); // IReadOnlyList<T>を返す
// 避ける:常に非同期の操作にValueTask
public ValueTask<Order> CreateOrderAsync(); // 単にTaskを使用