modern-csharp について
C#を使う開発者向けに、C# 14と.NET 10の言語機能をまとめたリファレンススキルです。可読性、値型、不変性の指針と、よくあるアンチパターンを扱います。
主な機能
- 01C# 14の言語機能を解説
- 02コレクション式とパターンマッチングを紹介
- 03fieldキーワードと拡張メンバーを説明
- 04値型と不変性を重視
- 05アンチパターンと代替案を提示
活用シーン
- 01C#プロジェクトのコーディング規約を更新
- 02DTOや値オブジェクトの型を選択
- 03文字列スライス時の割り当てを削減
- 04プロパティ検証と遅延初期化を改善
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ codewithmukesh/ dotnet-claude-kit --skill modern-csharpインストール後、Claude Code で/modern-csharpを入力して読み込まれたか確認します。
詳細
モダンな C#(C# 14 / .NET 10)
基本原則
- 最新の安定機能を使う — 目標は C# 14 です。ライブラリで代替するより、言語機能を優先します。
- 巧妙さより読みやすさを優先する — パターンマッチングや式形式のメンバーは、適切に使えば読みやすさを向上させますが、深くネストしたパターンはそうではありません。
- 可能な限り値型を使う — GC の負荷を軽減するため、
record struct、Span<T>、スタック割り当てを優先します。 - デフォルトで不変にする —
record、readonly、init、requiredを使い、不正な状態を表現できないようにします。
パターン
よく使われる機能のクイックリファレンス
| 機能 | 用途 | 例 |
|---|---|---|
| プライマリコンストラクター | DI 注入、フィールドへの代入の削減 | public class OrderService(IOrderRepo repo, TimeProvider clock) { } |
| コレクション式 | すべてのコレクション型で [] を使用 + スプレッド | List<string> names = ["Alice", "Bob"]; / int[] all = [..a, ..b, 99]; |
| レコード | DTO、値オブジェクト、不変データ | public record CreateOrderRequest(string CustomerId, List<OrderItem> Items); |
readonly record struct | スタックに割り当てる小さな値型 | public readonly record struct Money(decimal Amount, string Currency); |
| パターンマッチング | switch 式、リスト/プロパティパターン | order switch { { Total: > 1000 } => "Premium", _ => "Standard" }; |
| リストパターン | 配列/リストの分解 | items switch { [] => "Empty", [var x] => $"One: {x}", [var f, .., var l] => $"{f}..{l}" }; |
Span<T> | 割り当てなしのスライス | ReadOnlySpan<char> trimmed = input.Trim(); int.TryParse(trimmed[4..], out id); |
| 生文字列リテラル | 複数行の SQL、JSON、XML | var sql = """ SELECT ... """; / 補間あり:$$""" {"id": "{{id}}"} """; |
required メンバー | 初期化を必須にする | public required string ConnectionString { get; init; } |
is パターン + 抽出 | null/型/プロパティのチェック | if (result is { IsSuccess: true, Value: var order }) { ... } |
field キーワード(C# 14)
手動で宣言せずに、プロパティアクセサー内から自動生成されたバッキングフィールドにアクセスします。
// GOOD — field keyword for validation in auto-property
public class Product
{
public string Name
{
get => field;
set => field = value?.Trim() ?? throw new ArgumentNullException(nameof(value));
}
public decimal Price
{
get => field;
set => field = value >= 0 ? value : throw new ArgumentOutOfRangeException(nameof(value));
}
}
field による遅延初期化
public class ProductCatalog
{
// Lazy-load on first access — no manual Lazy<T> or backing field
public IReadOnlyList<Product> Products
{
get => field ??= LoadProducts();
}
private static List<Product> LoadProducts() => /* expensive load */;
}
field による変更通知
// INotifyPropertyChanged without manual backing fields
public class OrderViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
public string CustomerName
{
get => field;
set
{
if (field == value) return;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CustomerName)));
}
} = "";
public decimal Total
{
get => field;
set
{
if (field == value) return;
field = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Total)));
}
}
}
拡張メンバー(C# 14)
C# 14 では、静的クラス内で extension ブロックを使えるようになりました。従来の拡張メソッドとは異なり、拡張 プロパティ と 静的 拡張メンバーに対応しており、レシーバーはブロック全体に対して一度だけ宣言します。
// GOOD — extension block (shipped C# 14 syntax)
public static class OrderExtensions
{
extension(Order order)
{
public decimal TotalWithTax => order.Total * 1.2m;
public bool IsHighValue => order.Total > 1000m;
public string ToSummary() =>
$"Order #{order.Id}: {order.Total:C} ({order.Items.Count} items)";
}
// Static extension members use the type (no receiver instance)
extension(Order)
{
public static Order Empty => Order.Create("none", [], DateTimeOffset.MinValue);
}
}
// Callers see them as if declared on Order
if (order.IsHighValue) { /* ... */ }
従来の this パラメーターを使った拡張メソッドも引き続き使用でき、併用できます。プロパティや同じレシーバーに対する複数のメンバーが必要な場合は、拡張ブロックを使います。
アンチパターン
モダンな代替手段がある場合は、古いパターンを使わない
// BAD — manual backing field when field keyword works
private string _name;
public string Name
{
get => _name;
set => _name = value ?? throw new ArgumentNullException();
}
// BAD — old-style collection initialization
var list = new List<int>() { 1, 2, 3 };
// BAD — Tuple instead of record for domain types
(string Name, decimal Price) product = ("Widget", 9.99m);
// GOOD — record
public record Product(string Name, decimal Price);
パターンマッチングを過度に使わない
// BAD — deeply nested pattern that's hard to read
if (order is { Customer: { Address: { Country: { Code: "US" } } } })
// GOOD — extract to a clear method or use sequential checks
if (order.Customer.Address.Country.Code == "US")
型が明らかでない場合は var を使わない
// BAD — what type is this?
var result = Process(order);
// GOOD — explicit type when not obvious
Result<Order> result = Process(order);
// Also GOOD — var is fine when type is apparent
var orders = new List<Order>();
判断ガイド
| シナリオ | 推奨 |
|---|---|
| DTO/API コントラクト | record(参照型) |
| 小さな値オブジェクト(2~3 フィールド) | readonly record struct |
| DI を使うサービス | プライマリコンストラクター |
| コレクションの作成 | コレクション式 [] |
| 検証を伴うプロパティ | field キーワード |
| 複数行文字列(SQL、JSON) | 生文字列リテラル """ |
| 文字列/配列のスライス | Span<T> |
| 型チェック + 抽出 | is/switch によるパターンマッチング |
| 初期化の強制 | required 修飾子 |
| 外部型へのメソッド追加 | 拡張メンバー |
コメント
Markdown 対応