02 7月 2025
この記事では、C# 11 と 12 で導入された新機能の中から、コードを簡素化し、開発をスムーズにするものをいくつか紹介します。これらのアップデートは革命的ではないかもしれませんが、実際的であり、不必要な複雑さを減らすことで時間を節約するように設計されています。日常のコーディングタスクにおいて、小さな変更がどのようにしてよりクリーンで効率的なソリューションにつながるかを見てみましょう。
C# では、複雑な内容を含む文字列を構築することは歴史的に課題でした。開発者は、特殊文字、改行、引用符をエスケープする必要があり、冗長で読みにくいコードになることがよくありました。このプロセスは、JSON、XML、または正規表現といった形式をソースファイルに直接埋め込む場合に特に煩雑になります。
C# 11 では、この問題を直接解決するために生文字列リテラルが導入されました。この機能により、文字列を複数行にわたって記述でき、埋め込まれた引用符やバックスラッシュを含むほぼすべての文字をエスケープシーケンスなしで含めることができます。生文字列リテラルは、少なくとも3つの二重引用符("""
)で始まり、終了します。
C# 11 以前:
string oldJson = "{\r\n \"name\": \"Alice\",\r\n \"age\": 30\r\n}";
Console.WriteLine(oldJson);
C# 11 以降:
string newJson = """
{
"name": "Alice",
"age": 30
}
""";
Console.WriteLine(newJson);
終了引用符の前の任意の空白は、文字列の最小インデントを定義し、コンパイラが最終出力からこれを削除します。生文字列リテラルは文字列の可読性を劇的に向上させ、構文エラーの可能性を減らします。
C# におけるパターンマッチングは大きく進化し、C# 11 では配列やリスト内でのシーケンスマッチングを可能にするリストパターンが導入されました。この強化により、開発者はコレクションの構造と内容を簡潔かつ表現力豊かに検査できます。
以前は、コレクションの構造を検証するには、長さや個々のインデックスを手動でチェックする必要があり、冗長でメンテナンスが難しいコードになっていました。リストパターンでは、定数、型、プロパティ、関係パターンなどのサブパターンをサポートすることでこれに対処しています。主な機能には、任意の単一要素にマッチする破棄パターン(_
)や、ゼロ以上の要素のシーケンスにマッチする範囲パターン(..
)が含まれます。
C# 11 以前:
int[] numbers = { 1, 2, 3 };
if (numbers != null && numbers.Length == 3 &&
numbers[0] == 1 && numbers[1] == 2 && numbers[2] == 3)
{
Console.WriteLine("配列は正確に 1, 2, 3 を含んでいます。");
}
if (numbers != null && numbers.Length >= 2 && numbers[1] == 2)
{
Console.WriteLine("配列の2番目の要素が 2 です。");
}
C# 11 以降:
int[] numbers = { 1, 2, 3 };
if (numbers is [1, 2, 3])
{
Console.WriteLine("配列は正確に 1, 2, 3 を含んでいます。");
}
if (numbers is [_, 2, ..])
{
Console.WriteLine("配列の2番目の要素が 2 です。");
}
リストパターンはシーケンスの検証をコンパクトで読みやすい形に簡略化し、こうした操作に必要なコード行を大幅に削減します。
オブジェクトの初期化は、重要なプロパティやフィールドが未割り当てのままになる望ましくない状態を引き起こすことがあります。従来、開発者はすべての必要なパラメータを受け取るコンストラクタや、メソッド内に防御的なチェックを追加することで必須の初期化を強制していました。
C# 11 では、プロパティとフィールドに対して required
修飾子が導入され、コンパイル時に強制する仕組みが提供されました。メンバーが required
とマークされている場合、コンパイラはオブジェクト作成時にコンストラクタまたはオブジェクト初期化子を介して値が割り当てられることを保証します。これにより、型のインスタンスが常に有効で完全に初期化された状態になることが保証され、データ欠損に関連する一般的なバグが防止されます。
C# 11 以前:
public class OldPerson
{
public string FirstName { get; set; }
public string LastName { get; set; }
public void DisplayName() => Console.WriteLine($"名前: {FirstName} {LastName}");
}
// 使用例:
var person = new OldPerson(); // コンパイル時エラーなしだが、潜在的に無効なオブジェクトを作成
person.DisplayName();
C# 11 以降:
public class NewPerson
{
public required string FirstName { get; init; }
public required string LastName { get; init; }
public void DisplayName() => Console.WriteLine($"名前: {FirstName} {LastName}");
}
// 使用例:
// var person = new NewPerson(); // コンパイルエラー - 必須プロパティが欠けている
// var person = new NewPerson { FirstName = "John" }; // コンパイルエラー - LastName が欠けている
var person = new NewPerson { FirstName = "Jane", LastName = "Doe" }; // OK
person.DisplayName();
必須メンバーは、コンパイル時に初期化を強制することで実行時の予期せぬ事態を排除し、手動チェックの必要性を減らします。この機能は、防御的なコーディングを減らし、コードの信頼性を高め、開発者が検証よりも機能に集中できるようにします。
C# 12 では、すべてのクラスと構造体に対してプライマリコンストラクタが導入され、以前はレコード型に限定されていた機能が拡張されました。これにより、コンストラクタのパラメータを型定義内で直接宣言でき、クラス全体にわたってフィールドやプロパティとして自動的にスコープが設定されます。従来のコンストラクタとは異なり、このアプローチでは明示的なフィールド宣言や手動割り当てが不要です。
ここで解決される主な問題は、オブジェクト初期化における繰り返しコードです。以前は、開発者はプライベートフィールドを定義し、コンストラクタの引数を明示的にマッピングする必要があり、コードサイズが不必要に膨らんでいました。プライマリコンストラクタはこれを簡略化し、初期化ロジックを型シグネチャに直接埋め込みます。
C# 12 以前:
public class OldProduct
{
private readonly int _productId;
private readonly string _productName;
public OldProduct(int productId, string productName)
{
_productId = productId;
_productName = productName;
}
public string PrintDetails() => $"製品ID: {_productId}, 名前: {_productName}";
}
// 使用例:
OldProduct oldProd = new OldProduct(101, "ノートパソコン");
oldProd.PrintDetails();
C# 12 以降:
public class NewProduct(int productId, string productName)
{
public string PrintDetails() => $"製品ID: {productId}, 名前: {productName}";
}
// 使用例:
NewProduct newProd = new NewProduct(102, "キーボード");
newProd.PrintDetails();
プライマリコンストラクタは、データ中心の型の定義を非常に簡潔にします。型の名前と並べて必須の構築パラメータを配置することで可読性を向上させ、クラスや構造体の依存関係を一目で明確にします。
C# でのコレクションの初期化は、従来、コレクションの種類に応じてさまざまな構文を使用する必要がありました。たとえば、リストでは new List<T> { ... }
、配列では new T[] { ... }
などです。既存のコレクションを新しいコレクションに結合またはマージするには、反復ループや Concat()
のような LINQ メソッドが必要で、オーバーヘッドと冗長性が増していました。
C# 12 では、幅広いコレクション型を作成および初期化するための統一的で簡潔な構文であるコレクション式が導入されました。シンプルな [...]
構文を使用することで、開発者は配列、リスト、Span<T>
、およびその他のコレクション類似型を作成できます。新しいスプレッド要素(..
)を使用すると、既存のコレクションの要素を新しいコレクション式に直接インライン化でき、手動での連結が不要になります。
C# 12 以前:
// 異なるコレクションの初期化
int[] initialNumbers = new int[] { 1, 2, 3 };
List<int> moreNumbers = new List<int> { 4, 5 };
// コレクションの結合
List<int> allNumbers = new List<int>();
allNumbers.AddRange(initialNumbers);
allNumbers.AddRange(moreNumbers);
allNumbers.Add(6);
allNumbers.Add(7);
Console.WriteLine(string.Join(", ", allNumbers));
C# 12 以降:
// 異なるコレクションの初期化
int[] initialNumbers = [1, 2, 3];
List<int> moreNumbers = [4, 5];
// スプレッド演算子を使用したコレクションの結合
List<int> allNumbers = [..initialNumbers, ..moreNumbers, 6, 7];
Console.WriteLine(string.Join(", ", allNumbers));
コレクション式は、コレクションの初期化と結合の冗長性を削減し、よりクリーンで直感的な構文を提供します。この効率性はコーディングを加速させ、可読性を向上させ、少ないコード行で大きな効果を達成するという原則をサポートします。
C# での関数型プログラミングの基盤であるラムダ式は、従来、パラメータのデフォルト値を定義する機能がありませんでした。ラムダがオプションの引数を処理したり、フォールバック値を提供したりする必要がある場合、開発者はラムダ本体内の条件ロジックに頼るか、複数のオーバーロードを定義する必要がありましたが、ラムダは直接オーバーロードをサポートしていません。
C# 12 では、ラムダ式のパラメータにデフォルト値を指定できるようになり、このギャップが埋められました。構文と動作は、メソッドやローカル関数のパラメータと同様であり、柔軟なラムダ関数をより流れるように簡潔に定義する方法を提供します。
C# 12 以前:
// デフォルトパラメータのないラムダ。
// 'y' にデフォルト値が必要な場合、しばしばラッパーや条件ロジックが必要でした:
Func<int, int, int> addOld = (x, y) => x + y;
Func<int, int> addWithDefaultOld = x => addOld(x, 10); // 一般的な回避策
Console.WriteLine(addOld(5, 3));
Console.WriteLine(addWithDefaultOld(5));
C# 12 以降:
// デフォルトパラメータを持つラムダ
Func<int, int, int> addNew = (x, y = 10) => x + y;
Console.WriteLine(addNew(5, 3)); // y は 3
Console.WriteLine(addNew(5)); // y はデフォルトの 10
ラムダに対するデフォルトパラメータの導入は、その柔軟性と表現力を大幅に向上させます。冗長なラムダ定義や内部の条件ロジックの必要性を減らします。
C# 11 と 12 は、「少ないコードで多くのことを実現する」という約束を果たす魅力的な機能セットを提供します。C# 11 の生文字列リテラルやリストパターンから、C# 12 のプライマリコンストラクタやコレクション式に至るまで、これらの進歩は日常のコーディングにおける実際のフラストレーションに対処します。不必要な構文を取り除き、可読性を高め、より安全なパターンを強制することで、ソフトウェア開発やコード変換プロジェクトのワークフローを直接向上させます。必須メンバーの強制やコレクション設定の簡略化など、それぞれの革新は、キーストロークを減らしながら、明確さを最大化し、エラーリスクを最小化します。