C#を用いた開発において、メソッドに渡す引数の数が状況によって変動する場合、その柔軟性を確保するために非常に便利な機能が「可変長引数(paramsキーワード)」です。

従来、C#のparamsは配列のみに限定されていましたが、近年のアップデートにより、その適用範囲とパフォーマンスが劇的に向上しました。

特に、最新のC# 13では配列以外のコレクション型やSpan型でもparamsを利用可能になり、メモリ効率を極限まで追求するプログラミングにおいても、可変長引数の恩恵を受けられるようになっています。

本記事では、paramsキーワードの基本的な使い方から、C# 13で導入された画期的な進化、さらには実務で役立つ設計上の注意点まで、テクニカルな視点で詳しく解説します。

paramsキーワードの基本概念

C#のparamsキーワードは、メソッドの定義において特定の型の引数を「個数不定」で受け取るための仕組みです。

通常、複数の値を渡すためには配列を明示的に作成して渡す必要がありますが、paramsを使用することで、呼び出し側は値をカンマ区切りで列挙するだけで済みます。

基本的な構文と動作

まずは、最も標準的な配列を用いたparamsの使い方を確認しましょう。

以下のコードは、渡された複数の整数を合計するシンプルなメソッドの例です。

C#
using System;
using System.Linq;

class Program
{
    static void Main()
    {
        // 1. カンマ区切りで引数を渡す
        int result1 = Sum(1, 2, 3, 4, 5);
        Console.WriteLine($"引数5つの合計: {result1}");

        // 2. 引数なしで呼び出すことも可能(空の配列として扱われる)
        int result2 = Sum();
        Console.WriteLine($"引数なしの合計: {result2}");

        // 3. 配列を直接渡すこともできる
        int[] numbers = { 10, 20, 30 };
        int result3 = Sum(numbers);
        Console.WriteLine($"配列を渡した合計: {result3}");
    }

    // paramsキーワードを使用したメソッド定義
    static int Sum(params int[] values)
    {
        // 内部的にはvaluesはint[]型の配列として扱われる
        if (values == null || values.Length == 0)
        {
            return 0;
        }
        return values.Sum();
    }
}
実行結果
引数5つの合計: 15
引数なしの合計: 0
配列を渡した合計: 60

このように、呼び出し側はnew int[] { ... }といった記述を省略でき、コードの可読性が大幅に向上します。

paramsを使用する際の制約事項

paramsキーワードを使用するには、いくつかの重要なルールを守る必要があります。

これに違反するとコンパイルエラーとなります。

params 引数は最後に配置

メソッドに他の引数がある場合、params 修飾子付き引数は必ずパラメータリストの最後に配置しなければなりません。

例えば、Method(params int[] nums, string name) のような定義は許可されません。

params は1つだけ

1つのメソッドで定義できる params 引数は1つだけ です。

複数の可変長引数を定義することはできません。

例:Method(params int[] a, params string[] b) は無効です。

params は1次元配列(C# 12以前)

C# 12 より前のバージョンでは、params に指定できる配列は 1次元配列のみ です。

多次元配列(例:int[,])を params に指定することはできません。

C# 13におけるparamsの進化:配列からの解放

C# 13(.NET 9)における最も大きな変更の一つが、paramsキーワードの拡張です。

これまで可変長引数は「配列(Array)」に限定されていましたが、C# 13からは以下のような多様な型でparamsが利用可能になりました。

  • System.Span<T>
  • System.ReadOnlySpan<T>
  • System.Collections.Generic.IEnumerable<T>
  • System.Collections.Generic.IList<T>
  • その他、コレクション式(Collection Expressions)が適用可能な型

なぜこの進化が重要なのか

従来の配列ベースのparamsには、「呼び出しのたびにヒープメモリへの割り当て(アロケーション)が発生する」というパフォーマンス上の課題がありました。

たとえ数個の数値を渡すだけでも、コンパイラは裏側で配列オブジェクトを生成し、それをガベージコレクション(GC)の対象として管理しなければなりませんでした。

C# 13でReadOnlySpan<T>がサポートされたことにより、スタックメモリを活用したゼロアロケーションな可変長引数の実現が可能になりました。

これは、高頻度で呼び出されるロギングや数学ライブラリにおいて、劇的なパフォーマンス向上をもたらします。

ReadOnlySpan<T> を用いた実装例

C# 13の新機能を活用した、効率的な数値処理メソッドの例を見てみましょう。

C#
using System;

class PerformanceDemo
{
    static void Main()
    {
        // C# 13以降では、ReadOnlySpanをparamsとして受け取れる
        PrintNumbers(10, 20, 30);
    }

    // ReadOnlySpanを使用することで、配列のヒープ割当を回避できる可能性がある
    static void PrintNumbers(params ReadOnlySpan<int> numbers)
    {
        Console.WriteLine($"要素数: {numbers.Length}");
        foreach (var num in numbers)
        {
            Console.Write($"{num} ");
        }
    }
}

この実装により、コンパイラは可能な限りスタック上にデータを配置し、ヒープへの配列生成を回避する最適化を行います。

これにより、GCの負荷を軽減し、アプリケーション全体の応答性を向上させることができます。

paramsの内部動作とオーバーロードの解決

paramsを使用する際、開発者が意識すべきなのは「どのメソッドが優先的に呼び出されるか」というオーバーロード解決のルールです。

配列 vs 個別引数

特定の引数の数に対して最適化されたオーバーロードが存在する場合、コンパイラはparams版よりも具体的なメソッドを優先します。

C#
class OverloadTest
{
    static void Display(int a) => Console.WriteLine("単一引数版");
    static void Display(params int[] list) => Console.WriteLine("params版");

    static void Main()
    {
        Display(10);      // 「単一引数版」が呼ばれる
        Display(10, 20);  // 「params版」が呼ばれる
    }
}

これは、パフォーマンス最適化のための常套手段です。

頻繁に使われる「引数1個」や「引数2個」のパターンを個別のメソッドとして定義しておくことで、paramsによる配列生成のオーバーヘッドを避けることができます。

C# 13での型優先順位

C# 13で複数のコレクション型がサポートされたことで、優先順位のルールも整理されました。

一般的に、ReadOnlySpan<T>を受け取るオーバーロードは、T[]を受け取るものよりも優先される傾向にあります。

これは、言語設計者がよりパフォーマンスの高い選択肢をデフォルトで選ぶように配慮した結果です。

実践的な活用シーン

paramsキーワードは、APIの利便性を高めるために多くの場面で活用されています。

1. 文字列のフォーマットやロギング

最も身近な例は string.FormatConsole.WriteLine です。

C#
string message = string.Format("ID: {0}, Name: {1}, Score: {2}", 101, "Alice", 95);

このメソッドのシグネチャは string.Format(string format, params object[] args) となっており、任意の数のオブジェクトを渡せるようになっています。

2. LINQやコレクションのユーティリティ

特定の複数の要素を元にリストを初期化したり、結合したりするヘルパーメソッドで役立ちます。

C#
public static List<T> CreateList<T>(params T[] items)
{
    return new List<T>(items);
}

// 利用例
var list = CreateList("C#", "Java", "Python", "Go");

3. 条件の動的な構築

SQLのIN句のような条件を構築する際、可変長引数は非常に強力です。

C#
public void FindProducts(params int[] categoryIds)
{
    string ids = string.Join(",", categoryIds);
    Console.WriteLine($"SELECT * FROM Products WHERE CategoryID IN ({ids})");
}

パフォーマンスとメモリの考慮事項

paramsは便利ですが、濫用するとシステムに悪影響を及ぼす可能性があります。

特にC# 12以前の環境や、C# 13でも配列(T[])を型に指定している場合は注意が必要です。

アロケーションのコスト

以下のコードをループ内で実行するとどうなるでしょうか。

C#
for (int i = 0; i < 1000000; i++)
{
    Process(1, 2, 3);
}

static void Process(params int[] values) { /* 何らかの処理 */ }

このループでは、100万個の配列オブジェクトがヒープに生成されます。

たとえメソッド内で何もしていなくても、メモリの確保と解放のコストが発生し、GC(ガベージコレクタ)が頻繁に作動する原因となります。

回避策

C# 13のReadOnlySpanを活用する

配列のアロケーションを避けるために params ReadOnlySpan<int> values のように定義します。

多くのケースで アロケーションがゼロ になり、パフォーマンス向上が期待できます。

よく使う引数の数に対してオーバーロードを用意する

頻繁に使われる引数の組み合わせ向けに Process(int a), Process(int a, int b) などのオーバーロードを用意し、これらから共通の内部メソッドを呼び出して実装の重複や不要な配列生成を減らします。

空の呼び出しを避ける

引数なしの呼び出し Process() はサイズ0の配列を生成する場合があるため、頻繁に発生するなら引数なし専用メソッドを用意するか、オーバーロードで特別扱いして配列割り当てを防ぎます。

paramsと他の引数機能の比較

C#には、引数の柔軟性を高めるための機能が他にもあります。

それぞれの違いを理解し、適切に使い分けることが重要です。

機能特徴適したシーン
params同型の引数を任意の数だけ渡せる。統計処理、フォーマット、リスト作成など。
オプション引数デフォルト値を設定し、引数の省略を許可する。設定値の指定など、大部分がデフォルトで良い場合。
メソッドオーバーロード異なる型や数の引数セットを定義する。型が異なる場合や、特定の数で高速化したい場合。

params vs オプション引数

オプション引数(例:void M(int x = 0))は、引数の「存在」を任意にしますが、引数の「数」を無限に増やすことはできません。

一方、paramsは同じ型のデータがいくつ来るかわからない場合に適しています。

デザインガイドライン:いつparamsを使うべきか

プロのテクニカルライターの視点から、ライブラリ設計時におけるparamsの採用基準をまとめます。

呼び出し側の簡潔さを優先する場合

ユーザーがそのメソッドを頻繁に利用し、かつ渡す要素数が通常少ない(1〜5個程度)場合、paramsは非常に喜ばれます。

引数が論理的に「コレクション」である場合

渡される値が一つの集合として意味をなす場合(例:平均値を出すための数値群)に限定すべきです。

全く異なる意味を持つ引数をparams object[]で受け取るのは、型安全性の観点から避けるべきです。

パフォーマンスがクリティカルなパスでない場合

もし超低遅延が求められるホットパス(極めて実行頻度が高い箇所)であれば、C# 13のReadOnlySpanを使うか、あるいはparams自体を避けて明示的なバッファ管理を検討してください。

まとめ

C#のparamsキーワードは、単なる「便利なシンタックスシュガー」から、C# 13を経て「パフォーマンスと利便性を両立する強力なツール」へと進化しました。

  • 基本:配列を型として指定し、最後の引数に配置することで、可変長の引数を受け取れる。
  • 進化:C# 13からは Span<T>IEnumerable<T> に対応し、メモリ効率が飛躍的に向上した。
  • 注意点:従来の配列ベースではヒープアロケーションが発生するため、ループ内での使用には注意が必要。
  • 解決策:高パフォーマンスが求められる場合は、ReadOnlySpan<T> を使用した params 定義を積極的に採用する。

C# 13以降の環境では、paramsの型としてReadOnlySpan<T>を選択することが、モダンなC#プログラミングのスタンダードとなっていくでしょう。

この記事を参考に、用途に応じた最適な可変長引数の実装を取り入れてみてください。