C#は、.NETプラットフォームの進化と共に、開発者の生産性を高め、コードの可読性と保守性を向上させるためのアップデートを継続的に行っています。

C# 12では、ボイラープレートコード(定型的な記述)を削減し、より直感的かつ簡潔にロジックを記述できる新機能が多数導入されました。

特に、プライマリコンストラクターの拡張やコレクション式の導入は、日常的なコーディングスタイルを劇的に変える可能性を秘めています。

本記事では、C# 12の主要な新機能を網羅し、具体的なサンプルコードを交えながら、実務でどのように活用すべきかを詳しく解説します。

プライマリコンストラクター(Primary Constructors)の拡張

C# 12において最も注目すべき変更点の一つが、プライマリコンストラクターがすべてのクラスおよび構造体で利用可能になったことです。

これまでプライマリコンストラクターは、C# 9で導入されたレコード型(record)に限定された機能でしたが、C# 12からは通常のクラス(class)や構造体(struct)でも定義できるようになりました。

従来の記述との比較

従来のクラス定義では、コンストラクターの引数を受け取り、それをプライベートなフィールドやプロパティに手動で代入する必要がありました。

しかし、プライマリコンストラクターを使用すると、クラス名の直後に引数を記述するだけで、クラス全体でその引数を利用できるようになります。

C#
using System;

// 従来の書き方
public class TraditionalUser
{
    private readonly string _name;
    private readonly int _age;

    public TraditionalUser(string name, int age)
    {
        _name = name;
        _age = age;
    }

    public void PrintInfo() => Console.WriteLine($"{_name} ({_age})");
}

// C# 12 のプライマリコンストラクターを使用した書き方
public class ModernUser(string name, int age)
{
    // フィールド宣言なしで直接引数を使用可能
    public void PrintInfo() => Console.WriteLine($"{name} ({age})");
}

class Program
{
    static void Main()
    {
        var user = new ModernUser("Alice", 25);
        user.PrintInfo();
    }
}
実行結果
Alice (25)

プライマリコンストラクターの挙動と注意点

プライマリコンストラクターの引数は、クラスの全範囲(メソッド、プロパティ、フィールドの初期化子)でスコープ内に入ります。

ただし、レコード型とは異なり、自動的にパブリックなプロパティが生成されるわけではありません。

カプセル化を維持したまま、初期化プロセスを簡略化できるのが利点です。

また、依存性の注入(Dependency Injection)を利用する際、従来は冗長なコンストラクターとフィールド定義が必要でしたが、C# 12ではこれを極めてシンプルに記述できます。

C#
// DIを利用したサービスの定義例
public class OrderService(IDatabase database, ILogger logger)
{
    public void PlaceOrder(Order order)
    {
        logger.Log("Processing order...");
        database.Save(order);
    }
}

このように、冗長なコードが排除され、本来のロジックに集中できる構造になります。

ただし、プライマリコンストラクターの引数は「変更可能(mutable)」な状態であるため、イミュータブル(不変)にしたい場合は、明示的に readonly フィールドに代入するなどの対応が必要です。

コレクション式(Collection Expressions)

C# 12で導入されたコレクション式は、配列やリスト、スパンなどのコレクションを初期化するための統一的な構文を提供します。

これまでは、初期化対象の型に応じて new int[] { ... }new List<int> { ... }、あるいは stackalloc など、異なる書き方を使い分ける必要がありました。

統一された新しい構文

新しい構文では、角括弧 [] を使用して、直感的にコレクションを定義できます。

C#
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 配列の初期化
        int[] array = [1, 2, 3, 4, 5];

        // リストの初期化
        List<string> list = ["C#", "VB", "F#"];

        // Span/ReadOnlySpan の初期化
        ReadOnlySpan<char> span = ['a', 'b', 'c'];

        // ジャグ配列(配列の配列)
        int[][] jagged = [[1, 2], [3, 4, 5]];

        Console.WriteLine($"Array length: {array.Length}");
        Console.WriteLine($"List count: {list.Count}");
    }
}
実行結果
Array length: 5
List count: 3

スプレッド演算子(Spread Operator)

コレクション式とともに導入された強力な機能が、スプレッド演算子 .. です。

これは、既存のコレクションを別のコレクションの中に展開して結合するための機能です。

C#
int[] first = [1, 2, 3];
int[] second = [4, 5, 6];

// 複数のコレクションを結合
int[] combined = [.. first, .. second, 7, 8, 9];

foreach (var item in combined)
{
    Console.Write($"{item} ");
}
実行結果
1 2 3 4 5 6 7 8 9

スプレッド演算子を使用することで、従来の Concat メソッドや AddRange を使用するよりも、視覚的に分かりやすく効率的な結合処理が可能になります。

コンパイラは、ターゲットとなる型に合わせて最適なコードを生成するため、パフォーマンス面での懸念も最小限に抑えられています。

ラムダ式のデフォルト引数(Default Lambda Parameters)

C# 12では、ラムダ式にデフォルトの引数値を設定できるようになりました。

これにより、メソッドと同様の柔軟性をラムダ式にも持たせることが可能になりました。

実装例

これまでは、特定の引数が省略された場合にデフォルト値を使用するラムダ式を作成するには、複数のデリゲートを用意するか、三項演算子で対応する必要がありました。

C# 12からは、パラメーターリスト内で直接デフォルト値を指定できます。

C#
using System;

class Program
{
    static void Main()
    {
        // デフォルト値を持つラムダ式
        var increment = (int value, int step = 1) => value + step;

        Console.WriteLine(increment(10));      // stepにデフォルト値1が使用される
        Console.WriteLine(increment(10, 5));   // stepに5が使用される
    }
}
実行結果
11
15

活用のメリット

この機能は、特にLINQのカスタム述語や、高階関数(関数を引数に取る関数)を扱う際に威力を発揮します。

関数のシグネチャを複雑にすることなく、オプションの動作を定義できるため、APIのデザインがより洗練されます。

ただし、動的な変数をデフォルト値にすることはできず、コンパイル時に決定可能な定数である必要がある点は、通常のメソッド引数のルールと同様です。

任意の型のエイリアス(Alias Any Type)

C# 12では、using エイリアスディレクティブが拡張され、ほぼすべての型に対して別名(エイリアス)を付けられるようになりました。

これには、タプル型、ポインター型、配列型、ジェネリック型などが含まれます。

複雑な型へのエイリアス設定

特に、名前付きタプルを使用する場合、コードの各所で長い型名を記述するのは保守性を下げる要因となります。

エイリアスを使用することで、意味のある名前を型に付与できます。

C#
using System;

// タプル型にエイリアスを設定
using Point = (int X, int Y);
// 配列型にエイリアスを設定
using GradeList = int[];

class Program
{
    static void Main()
    {
        Point p = (10, 20);
        GradeList grades = [90, 85, 70];

        Console.WriteLine($"Point: {p.X}, {p.Y}");
        Console.WriteLine($"Average: {GetAverage(grades)}");
    }

    static double GetAverage(GradeList grades)
    {
        double sum = 0;
        foreach (var g in grades) sum += g;
        return sum / grades.Length;
    }
}

型エイリアスの有用性

この機能の主な目的は、複雑なジェネリック型のシグネチャを簡略化することにあります。

例えば、Dictionary<string, List<(int Id, string Name)>> のような深い階層を持つ型に CustomerRegistry といった名前を付けることで、コードの意図が明確になります。

ただし、エイリアスはそのファイル内(またはグローバルな using の場合、プロジェクト全体)でのみ有効であり、新しい型自体を定義しているわけではない(既存の型への参照である)という点に注意が必要です。

インライン配列(Inline Arrays)

パフォーマンスを重視するシナリオ、特にランタイムやライブラリ開発において、スタック上に固定サイズのバッファを効率的に配置する機能が求められます。

C# 12では、InlineArray 属性を使用することで、構造体内に固定長の配列を定義できる「インライン配列」がサポートされました。

インライン配列の定義と利用

インライン配列は、System.Runtime.CompilerServices 名前空間にある InlineArray 属性を構造体に付与することで作成します。

C#
using System;
using System.Runtime.CompilerServices;

[InlineArray(5)] // 5要素の配列として扱う
public struct MyBuffer<T>
{
    private T _element0; // 内部的なバッファ
}

class Program
{
    static void Main()
    {
        var buffer = new MyBuffer<int>();

        // 配列のようにインデックスでアクセス可能
        for (int i = 0; i < 5; i++)
        {
            buffer[i] = i * 10;
        }

        foreach (var item in buffer)
        {
            Console.WriteLine(item);
        }
    }
}
実行結果
0
10
20
30
40

パフォーマンス上の利点

従来の配列はヒープに割り当てられますが、インライン配列は構造体の一部としてスタック上(または包含先のオブジェクト内)に配置されるため、ガベージコレクション(GC)の負荷を軽減できます。

また、Span<T>ReadOnlySpan<T> との親和性が高く、安全かつ高速なメモリ操作を実現するために設計されています。

一般的なアプリケーション開発で頻繁に使う機能ではありませんが、低レイテンシが要求される処理では極めて重要なツールとなります。

その他の注目すべき変更点

C# 12には、上記以外にもコードの品質や柔軟性を高める細かな改善が含まれています。

インターセプター(Interceptors)

※本機能は実験的な機能(Preview)として導入されました。

インターセプターは、ソースジェネレーターが特定のメソッド呼び出しを別のコードに「横取り(インターセプト)」して置き換えることができる機能です。

例えば、AOT(Ahead-of-Time)コンパイル時に、リフレクションを使用している箇所を静的な最適化コードに置き換えるといった用途が想定されています。

コンパイラレベルでの強力な最適化を可能にする技術です。

ref readonly パラメーター

メソッドの引数において、ref readonly を使用できるようになりました。

これは、引数を参照渡し(コピーを避ける)しつつ、呼び出し先でその値を変更できないように制限するものです。

in パラメーターと似ていますが、ref readonly は呼び出し側で変数が変更可能である必要がないなど、より厳密な制御が必要なライブラリ開発向けの機能です。

Experimental 属性

新しく導入された [Experimental] 属性を型、メソッド、またはアセンブリに付与することで、その機能が実験的であることを明示できます。

この属性が付いた機能を使用すると、コンパイラは警告を出力し、開発者に将来的な破壊的変更の可能性を通知します。

実務で役立つC# 12の活用シーン

C# 12の新機能をどのようにプロジェクトに導入すべきか、具体的なユースケースをまとめました。

機能主な活用シーンメリット
プライマリコンストラクターDI(依存性注入)やDTO(データ転送オブジェクト)の定義ボイラープレートの削減、コードの可視性向上
コレクション式テストデータの作成、リストの初期化、配列の結合記法の統一、読みやすさの向上
任意の型のエイリアス複雑なタプルやジェネリック型の利用型名の短縮、ドメイン知識の明文化
ラムダ式のデフォルト引数小規模なユーティリティ関数の定義、LINQの拡張オーバーロード作成の回避、柔軟な関数定義

特にプライマリコンストラクターとコレクション式は、既存のコードベースに対しても適用しやすく、すぐに効果を実感できる機能です。

コードレビューの基準にこれらを取り入れることで、プロジェクト全体のコード量を削減し、意図が伝わりやすい洗練されたソースコードへと進化させることができるでしょう。

まとめ

C# 12は、言語としての堅牢さを維持しつつ、「より少ない記述で、より多くのことを実現する」という方向性を明確に打ち出したアップデートとなっています。

プライマリコンストラクターによるクラス定義の簡略化、コレクション式による直感的な初期化、そしてエイリアスの拡張による表現力の向上など、いずれの新機能も開発者の日常的な「書く苦労」を軽減することに焦点を当てています。

また、インライン配列やインターセプターのような機能は、高度なパフォーマンスチューニングを必要とするエンジニアにとって強力な武器となります。

C# 12の機能を活用することで、冗長なコードに埋もれていたビジネスロジックが表面化し、保守性の高いソフトウェア開発が可能になります。

まずはコレクション式やプライマリコンストラクターといった身近な機能から導入し、その恩恵を体感してみてください。

新しい言語機能をマスターすることは、単に短く書くためだけではなく、モダンなC#のベストプラクティスに従った、安全で効率的なコードを書くための第一歩です。