dotnet-csharp-modern-patterns について
プロジェクトのターゲットフレームワークに合わせて、モダンな C# 言語機能を解説するガイドです。TFM と C# のバージョン対応、機能ごとの制約、マルチターゲット時の互換性を扱います。
主な機能
- 01TFM と C# バージョンの対応表
- 02レコードと主コンストラクターの解説
- 03コレクション式とパターンの例
- 04言語機能のバージョン制約
- 05マルチターゲット互換性の指針
活用シーン
- 01対象フレームワークで使える C# 構文を確認
- 02適切なデータ型と言語機能を選択
- 03マルチターゲットプロジェクトの互換性に対応
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-csharp-modern-patternsインストール後、Claude Code で/dotnet-csharp-modern-patternsを入力して読み込まれたか確認します。
詳細
dotnet-csharp-modern-patterns
プロジェクトのターゲットフレームワークに合わせた、モダンな C# 言語機能のガイダンスです。TFM と C# のバージョンを特定するため、必ず最初に [skill:dotnet-version-detection] を実行してください。
関連項目:命名規則とスタイル規則については [skill:dotnet-csharp-coding-standards]、非同期処理固有のパターンについては [skill:dotnet-csharp-async-patterns] を参照してください。
クイックリファレンス:TFM と C# のバージョン
| TFM | C# | 主な言語機能 |
|---|---|---|
| net8.0 | 12 | プライマリコンストラクター、コレクション式、任意の型のエイリアス |
| net9.0 | 13 | params コレクション、Lock 型、部分プロパティ |
| net10.0 | 14 | field キーワード、拡張ブロック、非バインドジェネリックの nameof |
| net11.0 | 15 (preview) | with() 引数を使ったコレクション式 |
レコード
等価性がオブジェクトの同一性ではなく値に基づく、不変のデータ転送オブジェクト、値セマンティクス、ドメインモデリングにはレコードを使用します。
レコードクラス(参照型)
// Positional record: concise, immutable, value equality
public record OrderSummary(int OrderId, decimal Total, DateOnly OrderDate);
// With additional members
public record Customer(string Name, string Email)
{
public string DisplayName => $"{Name} <{Email}>";
}
レコード構造体(値型、C# 10 以降)
// Positional record struct: value type with value semantics
public readonly record struct Point(double X, double Y);
// Mutable record struct (rare -- prefer readonly)
public record struct MutablePoint(double X, double Y);
レコードとクラスの使い分け
| 用途 | 推奨 |
|---|---|
| DTO、API レスポンス | record |
| ドメイン値オブジェクト(Money、Email) | readonly record struct |
| 同一性を持つエンティティ(User、Order) | class |
| 高スループットで小さなデータ | readonly record struct |
| 継承が必要 | record(クラスベース) |
非破壊的な変更
var updated = order with { Total = order.Total + tax };
プライマリコンストラクター(C# 12 以降、net8.0 以降)
コンストラクターパラメーターをクラス/構造体の本体内で直接キャプチャします。パラメーターは型の全体で使用できますが、フィールドやプロパティではなく、キャプチャされた状態です。
サービス向け(DI 注入)
public class OrderService(IOrderRepository repo, ILogger<OrderService> logger)
{
public async Task<Order> GetAsync(int id)
{
logger.LogInformation("Fetching order {OrderId}", id);
return await repo.GetByIdAsync(id);
}
}
注意点
- プライマリコンストラクターのパラメーターは
readonlyフィールドではなく、変更可能なキャプチャです。不変性が重要な場合は、本体内でreadonlyフィールドに代入してください。 - コンストラクター実行時にパラメーターを検証する必要がある場合は、プライマリコンストラクターを使わないでください。代わりに、ガード句を備えた従来のコンストラクターを使用します。
- レコードでは、位置指定パラメーターが自動的にパブリックプロパティになります。クラス/構造体では、プライベートキャプチャのままです。
// Explicit readonly field when immutability matters
public class Config(string connectionString)
{
private readonly string _connectionString = connectionString
?? throw new ArgumentNullException(nameof(connectionString));
}
コレクション式(C# 12 以降、net8.0 以降)
[...] を使ってコレクションを作成するための統一された構文です。
// Array
int[] numbers = [1, 2, 3];
// List
List<string> names = ["Alice", "Bob"];
// Span
ReadOnlySpan<byte> bytes = [0x00, 0xFF];
// Spread operator
int[] combined = [..first, ..second, 99];
// Empty collection
List<int> empty = [];
引数を指定したコレクション式(C# 15 プレビュー、net11.0 以降)
容量、比較子、その他のコンストラクター引数を指定します。
// Capacity hint
List<int> nums = [with(capacity: 1000), ..Generate()];
// Custom comparer
HashSet<string> set = [with(comparer: StringComparer.OrdinalIgnoreCase), "Alice", "bob"];
// Dictionary with comparer
Dictionary<string, int> map = [with(comparer: StringComparer.OrdinalIgnoreCase),
new("key1", 1), new("key2", 2)];
net11.0 以降のみ。
<LangVersion>preview</LangVersion>が必要です。それ以前の TFM では使用しないでください。
パターンマッチング
switch 式(C# 8 以降)
string GetDiscount(Customer customer) => customer switch
{
{ Tier: "Gold", YearsActive: > 5 } => "30%",
{ Tier: "Gold" } => "20%",
{ Tier: "Silver" } => "10%",
_ => "0%"
};
リストパターン(C# 11 以降)
bool IsValid(int[] data) => data is [> 0, .., > 0]; // first and last positive
string Describe(int[] values) => values switch
{
[] => "empty",
[var single] => $"single: {single}",
[var first, .., var last] => $"range: {first}..{last}"
};
型パターンとプロパティパターン
decimal CalculateShipping(object package) => package switch
{
Letter { Weight: < 50 } => 0.50m,
Parcel { Weight: var w } when w < 1000 => 5.00m + w * 0.01m,
Parcel { IsOversized: true } => 25.00m,
_ => 10.00m
};
required メンバー(C# 11 以降)
オブジェクト初期化子を使って、呼び出し側に構築時のプロパティ初期化を必須にします。
public class UserDto
{
public required string Name { get; init; }
public required string Email { get; init; }
public string? Phone { get; init; }
}
// Compiler enforces Name and Email
var user = new UserDto { Name = "Alice", Email = "[email protected]" };
デシリアライズが必要な DTO に便利です(System.Text.Json は .NET 8 以降で required を尊重します)。
field キーワード(C# 14、net10.0 以降)
プロパティアクセサー内で、コンパイラーが生成するバッキングフィールドに直接アクセスします。
public class TemperatureSensor
{
public double Reading
{
get => field;
set => field = value >= -273.15
? value
: throw new ArgumentOutOfRangeException(nameof(value));
}
}
プライベートフィールドとカスタムロジックを持つプロパティを宣言する従来のパターンに代わるものです。別個のバッキングフィールドを用意せずに、セッターで検証や変換を行う必要がある場合に使用します。
net10.0 以降のみ。 それ以前の TFM では、従来のプライベートフィールドを使用してください。
拡張ブロック(C# 14、net10.0 以降)
1 つのブロック内に、ある型の拡張メンバーをまとめます。
public static class EnumerableExtensions
{
extension<T>(IEnumerable<T> source) where T : class
{
public IEnumerable<T> WhereNotNull()
=> source.Where(x => x is not null);
public bool IsEmpty()
=> !source.Any();
}
}
net10.0 以降のみ。 それ以前の TFM では、従来の
static拡張メソッドを使用してください。
任意の型のエイリアス(using、C# 12 以降、net8.0 以降)
using Point = (double X, double Y);
using UserId = System.Guid;
Point origin = (0, 0);
UserId id = UserId.NewGuid();
タプルのエイリアスやドメイン型のエイリアスを、完全な型を作成せずに定義するのに便利です。
params コレクション(C# 13、net9.0 以降)
params は配列以外の追加のコレクション型もサポートするようになりました。これには Span<T>、ReadOnlySpan<T>、および特定のコレクションインターフェイスを実装する型が含まれます。
public void Log(params ReadOnlySpan<string> messages)
{
foreach (var msg in messages)
Console.WriteLine(msg);
}
// Callers: compiler may avoid heap allocation with span-based params
Log("hello", "world");
net9.0 以降のみ。 net8.0 では、
paramsがサポートするのは配列のみです。
Lock 型(C# 13、net9.0+)
ロックには object ではなく System.Threading.Lock を使用します。
private readonly Lock _lock = new();
public void DoWork()
{
lock (_lock)
{
// thread-safe operation
}
}
Lock は高度なシナリオ向けに Scope ベースの API を提供し、lock (object) よりも表現力に優れています。
net9.0+ のみ。 net8.0 では、
private readonly object _gate = new();とlock (_gate)を使用してください。
部分プロパティ(C# 13、net9.0+)
部分プロパティを使うと、ソースジェネレーターがプロパティのシグネチャを定義し、それをユーザーが実装する、またはその逆を行えます。
// In generated file
public partial class ViewModel
{
public partial string Name { get; set; }
}
// In user file
public partial class ViewModel
{
private string _name = "";
public partial string Name
{
get => _name;
set => SetProperty(ref _name, value);
}
}
net9.0+ のみ。 ジェネレーターパターンについては、[skill:dotnet-csharp-source-generators] を参照してください。
非バインドジェネリック型での nameof(C# 14、net10.0+)
string name = nameof(List<>); // "List"
string name2 = nameof(Dictionary<,>); // "Dictionary"
ログ記録、診断、リフレクションのシナリオで役立ちます。
net10.0+ のみ。
マルチターゲット時のポリフィルに関するガイダンス
複数の TFM をターゲットにする場合、新しい言語機能は古いターゲットではコンパイルできないことがあります。次の方法を使用してください。
- PolySharp -- コンパイラーが必要とする型(
IsExternalInit、RequiredMemberAttributeなど)をポリフィルし、init、required、recordなどの言語機能を古い TFM でも使用できるようにします。 - Polyfill -- ランタイム API をポリフィルします(例:netstandard2.0 向けの
string.Contains(char))。 - 条件付きコンパイル -- ポリフィルできない機能には
#ifを使用します。
#if NET10_0_OR_GREATER
// Use field keyword
public double Value { get => field; set => field = Math.Max(0, value); }
#else
private double _value;
public double Value { get => _value; set => _value = Math.Max(0, value); }
#endif
ポリフィルに関する包括的なガイダンスについては、[skill:dotnet-multi-targeting] を参照してください。
知識ソース
このスキルの機能に関するガイダンスは、一般公開されている以下の言語設計の根拠に基づいています。
- C# 言語設計ノート(Mads Torgersen ほか) -- 各 C# バージョンの機能に関する設計上の決定事項です。このスキルに関連する主な根拠は、プライマリコンストラクター(DI を多用するサービスの定型コードを減らす)、コレクション式(コレクションの初期化構文を統一する)、
fieldキーワード(バッキングフィールドに関する定型コードをなくす)、拡張ブロック(対象の型ごとに拡張をまとめる)です。各機能は表現力と安全性のバランスを取っています。たとえば、プライマリコンストラクターのパラメーターは、機能をシンプルに保つため、意図的に(読み取り専用ではなく)変更可能なキャプチャとして扱われます。不変性が必要な場合は、明示的な readonly フィールドを使用してください。ソース:https://github.com/dotnet/csharplang/tree/main/meetings - C# 言語提案リポジトリ -- 採用済みおよび提案中の機能に関する詳細な仕様と設計上の根拠です。ソース:https://github.com/dotnet/csharplang/tree/main/proposals
注: このスキルは、一般公開されている設計上の根拠を適用しています。記載されたソースを代表するものではなく、それらの見解を代弁するものでもありません。