現代的なC#コーディング規約 は何をしますか?
このスキルを使用するタイミング
以下の場合にこのスキルを使用します。
- 新しいC#コードを作成する、または既存のコードをリファクタリングする場合
- ライブラリやサービスのパブリックAPIを設計する場合
- パフォーマンスが重要なコードパスを最適化する場合
- 強い型付けを持つドメインモデルを実装する場合
- 非同期/待機(async/await)を多用するアプリケーションを構築する場合
- バイナリデータ、バッファ、または高スループットのシナリオを扱う場合
参照ファイル
- value-objects-and-patterns.md: 値オブジェクトの完全な例とパターンマッチングコード
- performance-and-api-design.md: Span/Memoryの例とAPI設計の原則
- composition-and-error-handling.md: 継承より合成、Result型、テストパターン
- anti-patterns-and-reflection.md: リフレクションの回避と一般的なアンチパターン
中核原則
- デフォルトで不変 -
record型とinit専用プロパティを使用 - 型安全性 - null許容参照型と値オブジェクトを活用
- モダンなパターンマッチング -
switch式とパターンを広範囲に使用 - あらゆる場面で非同期 - 適切なキャンセルサポート付きの非同期APIを優先
- ゼロアロケーションパターン - パフォーマンスが重要なコードでは
Span<T>とMemory<T>を使用 - API設計 - 抽象化を受け入れ、適切に特化した型を返す
- 継承より合成 - 抽象基底クラスを避け、合成を優先
- 値オブジェクトとしての構造体 - 値オブジェクトには
readonly record structを使用
言語パターン
不変データのためのレコード(C# 9以降)
DTO、メッセージ、イベント、ドメインエンティティにはrecord型を使用します。
// 単純な不変DTO
public record CustomerDto(string Id, string Name, string Email);
// コンストラクタで検証を行うレコード
public record EmailAddress
{
public string Value { get; init; }
public EmailAddress(string value)
{
if (string.IsNullOrWhiteSpace(value) || !value.Contains('@'))
throw new ArgumentException("無効なメールアドレスです", nameof(value));
Value = value;
}
}
// コレクションを持つレコード - IReadOnlyListを使用
public record ShoppingCart(
string CartId,
string CustomerId,
IReadOnlyList<CartItem> Items
)
{
public decimal Total => Items.Sum(item => item.Price * item.Quantity);
}
record classとrecord structの使い分け:
record class(デフォルト):参照型。エンティティ、集約、複数のプロパティを持つDTOに使用record struct:値型。値オブジェクトに使用(次のセクション参照)
読み取り専用レコード構造体としての値オブジェクト
値オブジェクトは、パフォーマンスと値のセマンティクスのために**常にreadonly record struct**にします。明示的な変換を使用し、暗黙の演算子は使用しないでください。
public readonly record struct OrderId(string Value)
{
public OrderId(string value) : this(
!string.IsNullOrWhiteSpace(value)
? value
: throw new ArgumentException("OrderIdは空にできません", nameof(value)))
{ }
public override string ToString() => Value;
}
public readonly record struct Money(decimal Amount, string Currency);
public readonly record struct CustomerId(Guid Value)
{
public static CustomerId New() => new(Guid.NewGuid());
}
完全な例(複数値オブジェクト、ファクトリパターン、暗黙変換禁止ルールを含む)については、value-objects-and-patterns.mdを参照してください。
パターンマッチング(C# 8-12)
switch式、プロパティパターン、関係パターン、リストパターンを使用して、よりクリーンなコードを実現します。
public decimal CalculateDiscount(Order order) => order switch
{
{ Total: > 1000m } => order.Total * 0.15m,
{ Total: > 500m } => order.Total * 0.10m,
{ Total: > 100m } => order.Total * 0.05m,
_ => 0m
};
パターンマッチングの完全な例については、value-objects-and-patterns.mdを参照してください。
null許容参照型(C# 8以降)
プロジェクトでnull許容参照型を有効にし、nullを明示的に処理します。
// .csproj内
<PropertyGroup>
<Nullable>enable</Nullable>
</PropertyGroup>
// 明示的なnull許容性
public string? FindUserName(string userId)
{
var user = _repository.Find(userId);
return user?.Name;
}
// nullチェックとのパターンマッチング
public decimal GetDiscount(Customer? customer) => customer switch
{
null => 0m,
{ IsVip: true } => 0.20m,
{ OrderCount: > 10 } => 0.10m,
_ => 0.05m
};
// ArgumentNullException.ThrowIfNullを使用したガード節(C# 11以降)
public void ProcessOrder(Order? order)
{
ArgumentNullException.ThrowIfNull(order);
// このスコープではorderは非nullになります
Console.WriteLine(order.Id);
}
継承より合成
抽象基底クラスを避けます。 インターフェース+合成を使用します。共有ロジックには静的ヘルパーを使用します。バリアントにはファクトリメソッドを持つレコードを使用します。
完全な例については、composition-and-error-handling.mdを参照してください。
パフォーマンスパターン
非同期/待機(Async/Await)のベストプラクティス
// 非同期を徹底する - 常にCancellationTokenを受け入れる
public async Task<Order> GetOrderAsync(string orderId, CancellationToken cancellationToken)
{
var order = await _repository.GetAsync(orderId, cancellationToken);
return order;
}
// 頻繁に呼び出され、しばしば同期的なメソッドにはValueTask
public ValueTask<Order?> GetCachedOrderAsync(string orderId, CancellationToken cancellationToken)
{
if (_cache.TryGetValue(orderId, out var order))
return ValueTask.FromResult<Order?>(order);
return GetFromDatabaseAsync(orderId, cancellationToken);
}
// ストリーミングにはIAsyncEnumerable
public async IAsyncEnumerable<Order> StreamOrdersAsync(
string customerId,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
await foreach (var order in _repository.StreamAllAsync(cancellationToken))
{
if (order.CustomerId == customerId)
yield return order;
}
}
重要なルール:
- 常に
= default付きでCancellationTokenを受け入れる - ライブラリコードでは
ConfigureAwait(false)を使用する - 非同期コードをブロックしない(
.Resultや.Wait()は使わない) - タイムアウトにはリンクされたCancellationTokenSourceを使用する
SpanとMemory
同期的なゼロアロケーション操作にはSpan<T>を、非同期操作にはMemory<T>を、大きな一時バッファにはArrayPool<T>を使用します。
完全なSpan/Memoryの例とAPI設計セクションについては、performance-and-api-design.mdを参照してください。
エラーハンドリング:Result型
予期されるエラーには、例外の代わりにResult<T, TError>を使用します。例外は予期しない/システムエラーの場合にのみ使用します。
Result型の実装と使用例の完全な説明については、composition-and-error-handling.mdを参照してください。
リフレクションベースのメタプログラミングを避ける
禁止: AutoMapper、Mapster、ExpressMapper。代わりに明示的なマッピング拡張メソッドを使用します。プライベートメンバーへのアクセスが本当に必要な場合は、UnsafeAccessorAttribute(.NET 8以降)を使用します。
完全なガイダンスについては、anti-patterns-and-reflection.mdを参照してください。
コード構成
// ファイル: Domain/Orders/Order.cs
namespace MyApp.Domain.Orders;
// 1. プライマリドメイン型
public record Order(
OrderId Id,
CustomerId CustomerId,
Money Total,
OrderStatus Status,
IReadOnlyList<OrderItem> Items
)
{
public bool IsCompleted => Status is OrderStatus.Completed;
public Result<Order, OrderError> AddItem(OrderItem item)
{
if (Status is not OrderStatus.Draft)
return Result<Order, OrderError>.Failure(
new OrderError("ORDER_NOT_DRAFT", "下書き注文にのみアイテムを追加できます"));
var newItems = Items.Append(item).ToList();
var newTotal = new Money(
Items.Sum(i => i.Total.Amount) + item.Total.Amount,
Total.Currency);
return Result<Order, OrderError>.Success(
this with { Items = newItems, Total = newTotal });
}
}
// 2. 状態を表す列挙型
public enum OrderStatus { Draft, Submitted, Processing, Completed, Cancelled }
// 3. 関連する型
public record OrderItem(ProductId ProductId, Quantity Quantity, Money UnitPrice)
{
public Money Total => new(UnitPrice.Amount * Quantity.Value, UnitPrice.Currency);
}
// 4. 値オブジェクト
public readonly record struct OrderId(Guid Value)
{
public static OrderId New() => new(Guid.NewGuid());
}
// 5. エラー
public readonly record struct OrderError(string Code, string Message);
ベストプラクティスのまとめ
推奨事項
- DTO、メッセージ、ドメインエンティティには
recordを使用する - 値オブジェクトには
readonly record structを使用する switch式でパターンマッチングを活用する- null許容参照型を有効にし、尊重する
- すべてのI/O操作に非同期/待機を使用する
- すべての非同期メソッドで
CancellationTokenを受け入れる - 高パフォーマンスシナリオには
Span<T>とMemory<T>を使用する - 抽象化(
IEnumerable<T>、IReadOnlyList<T>)を受け入れる - 予期されるエラーには
Result<T, TError>を使用する - 大きなアロケーションには
ArrayPool<T>でバッファをプールする - 継承より合成を優先する
禁止事項
- レコードで十分な場合に変更可能なクラスを使用しない
- 値オブジェクトにクラスを使用しない(
readonly record structを使用する) - 深い継承階層を作成しない
- null許容参照型の警告を無視しない
- 非同期コードをブロックしない(
.Result、.Wait()) Span<byte>で十分な場合にbyte[]を使用しないCancellationTokenパラメータを忘れない- APIから変更可能なコレクションを返さない
- 予期されるビジネスエラーに対して例外をスローしない
- 大きな配列を繰り返し割り当てない(
ArrayPoolを使用する)
詳細なアンチパターンの例については、anti-patterns-and-reflection.mdを参照してください。
追加リソース
- C#言語仕様: https://learn.microsoft.com/ja-jp/dotnet/csharp/
- パターンマッチング: https://learn.microsoft.com/ja-jp/dotnet/csharp/fundamentals/functional/pattern-matching
- SpanとMemory: https://learn.microsoft.com/ja-jp/dotnet/standard/memory-and-spans/
- 非同期のベストプラクティス: https://learn.microsoft.com/ja-jp/archive/msdn-magazine/2013/march/async-await-best-practices-in-asynchronous-programming
- .NETパフォーマンスのヒント: https://learn.microsoft.com/ja-jp/dotnet/framework/performance/