.NETの進化とともに歩んできたC#は、バージョン13(C# 13)においてさらなる洗練とパフォーマンスの向上を遂げました。

.NET 9と同時にリリースされた今回のアップデートでは、開発者の利便性を高める「構文の柔軟性」と、システムの根幹を支える「実行効率の最適化」の両面に焦点が当てられています。

C# 12までの流れを汲みつつ、より直感的に、そしてより安全にコードを書くための機能が数多く導入されました。

本記事では、C# 13で導入された主要な新機能を網羅し、具体的なコード例を交えながらその魅力と活用シーンを詳しく解説します。

params 引数の柔軟な拡張

C# 13において最も身近で、かつ影響範囲の広い変更の一つが、params 修飾子のサポート対象が大幅に拡大されたことです。

これまでのC#では、可変長引数を扱うための params キーワードは「配列」に対してのみ使用可能でした。

しかし、C# 13からは配列以外のコレクション型に対しても params を適用できるようになりました。

対応するコレクション型の拡大

新しくサポートされた型には、ReadOnlySpan<T>Span<T>、および IEnumerable<T>List<T> を含むすべての「コレクション式」で初期化可能な型が含まれます。

この変更により、パフォーマンスを重視するライブラリ設計において、メモリ割り当て(アロケーション)を最小限に抑えることが可能になりました。

例えば、ReadOnlySpan<int>params として受け取るメソッドを定義すると、呼び出し側で配列を明示的に作成することなく、スタックメモリを活用した効率的なデータ受け渡しが行えます。

C#
using System;

public class ParamsExample
{
    // C# 13からは ReadOnlySpan に対して params を使用可能
    public static void PrintNumbers(params ReadOnlySpan<int> numbers)
    {
        foreach (var number in numbers)
        {
            Console.Write($"{number} ");
        }
        Console.WriteLine();
    }

    public static void Main()
    {
        // 呼び出し側は従来の配列形式と同じように記述できる
        // 内部的には配列のヒープ割り当てを避け、効率的な処理が行われる
        PrintNumbers(10, 20, 30, 40, 50);
    }
}
実行結果
10 20 30 40 50

パフォーマンスへのメリット

従来の配列を用いた params では、メソッドを呼び出すたびに新しい配列オブジェクトがヒープ上に生成されていました。

これに対し、ReadOnlySpan<T> を使用した場合は、コンパイラが呼び出し元の引数をスタックに配置、または静的なデータ領域として最適化するため、ガベージコレクション(GC)の負荷を大幅に軽減できます。

高頻度で呼ばれるユーティリティメソッドなどで特に威力を発揮する機能です。

新しい lock オブジェクトと System.Threading.Lock 型

マルチスレッドプログラミングにおける排他制御は、常に正確さとパフォーマンスのバランスが求められる領域です。

C# 13では、スレッドの同期をより効率的に行うための新しい型 System.Threading.Lock が導入され、lock 文の挙動が強化されました。

従来の lock 文の課題

これまでの lock 文は、任意の参照型(object)を対象としていました。

しかし、汎用的なオブジェクトをロック対象にする場合、ランタイム内部での処理にオーバーヘッドが生じるという課題がありました。

System.Threading.Lock による最適化

C# 13では、.NET 9で追加された System.Threading.Lock クラスのインスタンスを lock 文に渡すと、コンパイラがそれを認識し、より高速でモダンな同期プリミティブへと自動的に変換します。

これにより、従来の Monitor.Enter / Monitor.Exit をベースにしたロックよりも軽量な処理が実現されます。

C#
using System;
using System.Threading;

public class LockExample
{
    // 新しい System.Threading.Lock 型を使用
    private readonly Lock _syncRoot = new();
    private int _counter = 0;

    public void Increment()
    {
        // lock 文が System.Threading.Lock インスタンスを検知し、最適化されたコードを生成する
        lock (_syncRoot)
        {
            _counter++;
            Console.WriteLine($"Counter: {_counter}");
        }
    }

    // scope を利用した手動制御も可能
    public void ManualLock()
    {
        using (_syncRoot.EnterScope())
        {
            // スレッドセーフな処理
        }
    }
}

この新しい Lock 型を使用することで、コードの可読性を維持したまま、実行時のパフォーマンスを底上げできる点が大きなメリットです。

また、EnterScope() メソッドを利用することで、using パターンによる柔軟なロック管理も可能となっています。

オブジェクト初期化子におけるインデックス演算子「^」のサポート

C# 8.0で導入されたインデックス演算子 ^ (末尾からのインデックス指定)は、配列やリストの末尾要素にアクセスする際に非常に便利な機能でした。

C# 13では、この便利な構文がオブジェクト初期化子の中でも利用可能になりました。

末尾からの要素指定

これまでは、オブジェクトの初期化フローの中で「最後からn番目の要素」に値を代入するには、一度インスタンスを変数に格納してから個別に代入操作を行う必要がありました。

C# 13からは、以下のように初期化子の中で直接記述できます。

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

public class IndexerExample
{
    public static void Main()
    {
        // オブジェクト初期化子内で ^ 演算子を使用
        var buffer = new int[5]
        {
            [0] = 1,
            [1] = 2,
            [^2] = 4, // 後ろから2番目
            [^1] = 5  // 最後
        };

        foreach (var item in buffer)
        {
            Console.Write($"{item} ");
        }
    }
}
実行結果
1 2 0 4 5

この例では、サイズ5の配列に対して、インデックス0, 1と、末尾からのインデックス ^2 (3番目) および ^1 (4番目) を指定して初期化しています。

コレクションのサイズが固定されている場合や、末尾に近い要素を特定のルールで初期化したい場合に、コードの意図をより明確に表現できるようになりました。

partial プロパティと partial インデクサー

C# 2.0から存在する partial (部分)定義の概念が、クラスやメソッドだけでなく、プロパティとインデクサーにも拡張されました。

これは特に、ソースジェネレーター(Source Generators)を利用する開発パターンにおいて強力な武器となります。

定義と実装の分離

partial プロパティを使用すると、一方のファイルでプロパティのシグネチャ(定義)を記述し、もう一方のファイルでその具体的な実装(getter/setterの内容)を記述することができます。

C#
// ファイル1: 手書きのコード
public partial class UserProfile
{
    // プロパティの定義
    public partial string DisplayName { get; }
}

// ファイル2: ソースジェネレーターなどによって自動生成されるコード
public partial class UserProfile
{
    private string _displayName = "Guest";

    // プロパティの実装
    public partial string DisplayName => _displayName;
}

ソースジェネレーターとの親和性

従来のソースジェネレーターでは、手書きのプロパティに対して追加の処理を差し込むことが難しく、属性を付与したフィールドからプロパティを丸ごと生成する手法が一般的でした。

C# 13の partial プロパティを活用すれば、開発者がプロパティの存在を明示しつつ、その内部ロジックをツール側に委ねるという、よりクリーンな設計が可能になります。

これにより、IntelliSenseなどの開発支援機能との親和性も向上します。

エスケープシーケンス \e の導入

非常に小さな変更に見えるかもしれませんが、コマンドラインツールやコンソールアプリケーションを開発するユーザーにとって、\e エスケープシーケンスの導入は大きな喜びです。

ANSI エスケープコードの簡略化

ターミナルの文字色を変更したり、カーソル位置を制御したりするために使用される ANSI エスケープコードは、ESC 文字(16進数で 0x1B)から始まります。

これまでは C# でこれを表現するために \u001b\x1b と記述する必要がありました。

C# 13からは、標準的なエスケープシーケンスとして \e がサポートされました。

C#
using System;

public class EscapeExample
{
    public static void Main()
    {
        // \e は ESC (U+001B) を表す
        // 以下はコンソールの文字色を緑色に変更する ANSI コードの例
        Console.WriteLine("\e[32mこのテキストは緑色で表示されます\e[0m");
    }
}

この修正により、コードが読みやすくなるだけでなく、他のプログラミング言語(C++やPHPなど)で既に採用されている慣習との一貫性が保たれるようになります。

メソッドグループの自然な型の改善

C#では、メソッド名をデリゲートや変数に代入する際、その「メソッドグループ」がどの型であるかを推論する仕組みがあります。

C# 13では、この推論ロジックが強化され、オーバーロードが存在する場合でも、より適切な型を「自然な型」として選択できるようになりました。

以前のバージョンでは、複数のオーバーロードがあるメソッドグループを型推論(var など)に渡すと、コンパイルエラーになるケースがありました。

C# 13では、コンパイラが候補を絞り込む際の精度が向上し、より広範なシナリオでスムーズな記述が可能になっています。

ref および unsafe の制限緩和(async メソッド等)

パフォーマンス最適化の文脈において、ref 構造体(ReadOnlySpan<T> など)は非常に重要ですが、これまで async メソッド内での利用には厳しい制限がありました。

C# 13では、一定の条件下において、async メソッド内での ref 変数の利用や、反復子(iterator)における unsafe コンテキストの利用制限が緩和されました。

async メソッドでの ReadOnlySpan 利用

具体的には、await を跨がない範囲であれば、async メソッド内でも ReadOnlySpan<T> などの ref struct をローカル変数として宣言・使用できるようになりました。

C#
using System;
using System.Threading.Tasks;

public class AsyncRefExample
{
    public async Task ProcessDataAsync(byte[] data)
    {
        // C# 13からは async メソッド内でも、await を跨がなければ ref struct を使用可能
        ReadOnlySpan<byte> span = data.AsSpan();
        if (span.Length > 0)
        {
            Console.WriteLine($"Processing {span.Length} bytes...");
        }

        // await の前後で ref struct を保持することは依然として禁止されているが、
        // await を呼び出す前にスコープが終わっていれば問題ない
        await Task.Delay(100);

        Console.WriteLine("Done.");
    }
}

この改善により、非同期処理が中心となる現代的なアプリケーションにおいても、ホットパス(頻繁に実行される箇所)での Span 活用が容易になり、さらなる高速化が期待できます

オーバーロード解決の優先順位(OverloadResolutionPriority)

ライブラリの作者にとって、APIの進化と互換性の維持は常に難しい問題です。

C# 13では、新しい属性 [OverloadResolutionPriority] を導入することで、コンパイラがどのオーバーロードを優先的に選択すべきかを明示的に指示できるようになりました。

優先順位の指定方法

例えば、従来の配列を受け取るメソッドに加え、パフォーマンス向上のために ReadOnlySpan を受け取る新しいオーバーロードを追加したとします。

通常、コンパイラは最も適合する型を選ぼうとしますが、曖昧さが生じる場合もあります。

この属性を使えば、特定のメソッドの「優先度」を高く設定し、優先的に選ばせることが可能です。

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

public class PriorityExample
{
    // 通常のメソッド
    public void Process(int[] data)
    {
        Console.WriteLine("Array overload called");
    }

    // 優先順位を高く設定したメソッド
    [OverloadResolutionPriority(1)]
    public void Process(ReadOnlySpan<int> data)
    {
        Console.WriteLine("Span overload called");
    }

    public static void Main()
    {
        var example = new PriorityExample();
        int[] numbers = { 1, 2, 3 };
        
        // 属性による指定があるため、Span版が優先して選ばれる
        example.Process(numbers);
    }
}
実行結果
Span overload called

この機能は、既存のコードの挙動を壊すことなく、より最適な新しい実装へとユーザーを誘導したい場合に非常に有用です。

標準ライブラリの改善などでも活用されており、.NETプラットフォーム全体の進化を支える重要なパーツとなっています。

C# 13 がもたらす開発体験の変化

ここまで紹介してきた新機能は、一見すると地味な改善に見えるかもしれません。

しかし、これらが組み合わさることで、実際の開発体験(DX)とアプリケーションの品質は大きく向上します。

コードの簡潔化と意図の明確化

params の拡張やオブジェクト初期化子のインデックス指定は、冗長なコードを排除し、開発者が「何をしたいか」という意図をよりストレートに表現することを助けます。

ボイラープレート(定型文)が減ることは、バグの混入を防ぐだけでなく、長期的なメンテナンスコストの削減にも直結します。

「デフォルトで高速」な世界へ

System.Threading.LockReadOnlySpan の活用範囲の拡大は、特別なチューニングを意識せずとも、言語の標準機能を使うだけで効率的なコードが生成される方向にC#を導いています。

「書きやすさ」と「速さ」をトレードオフにしない進化こそが、C# 13の真髄と言えるでしょう。

エコシステムの成熟

partial プロパティやオーバーロードの優先順位指定といった機能は、個々の開発者だけでなく、ライブラリ作者やツール開発者にとっても強力な武器となります。

これにより、私たちが利用するNuGetパッケージやフレームワークが、より高性能で使いやすいものへと洗練されていく好循環が生まれます。

まとめ

C# 13は、.NET 9時代の幕開けにふさわしい、着実かつ強力なアップデートとなりました。

  • params 引数の拡張により、アロケーションを抑えた柔軟なメソッド設計が可能になりました。
  • 新しい Lock 型の導入で、スレッド同期のパフォーマンスが最適化されました。
  • オブジェクト初期化子のインデックス指定\e エスケープシーケンスといった細かな改善が、日々のコーディングを快適にします。
  • partial プロパティは、ソースジェネレーターを活用したモダンな開発スタイルを後押しします。
  • OverloadResolutionPriority により、ライブラリの進化と互換性の両立が容易になりました。

これらの機能は、個別に利用しても十分な恩恵がありますが、組み合わせて活用することで、C#という言語が持つポテンシャルを最大限に引き出すことができます。

.NET 9への移行とともに、C# 13の新しい構文を積極的に取り入れ、より洗練されたソフトウェア開発を目指しましょう。