目次

概要

C# では params キーワードを用いることでメソッドの引数の数を可変にすることが出来ます。

ポイント
  • 定義側の例:int Sum(params int[] args) { ... }

  • 利用側の例:Sum(1, 2, 3, 4, 5);… これで、Sum(new int[] { 1, 2, 3, 4, 5 });と同じ意味。

params キーワード

例えば、可変個の整数のうち最大の整数を求めるメソッドを作りたいとします。 可変長引数を使わずにメソッドを実装すると以下のようになるでしょう。

using System;

class ParamsTest
{
  static void Main()
  {
    int a = 314, b = 159, c = 265, d = 358, e  = 979;
    // ↑こいつらの最大値を探したいとき、

    int[] tmp = new int[]{a, b, c, d, e};
    // ↑こんな風に一度配列に格納してから

    int max = Max(tmp);
    // ↑Max メソッドを呼び出す必要がある。

    Console.Write("{0}\n", max);
  }

  static int Max(int[] a)
  {
    int max = a[0];
    for(int i=1; i<a.Length; ++i)
    {
      if(max < a[i])
        max = a[i];
    }
    return max;
  }
}

この方法では、1度値を配列に格納してからメソッドを呼び出すという操作が必要になります。 このメソッドを呼び出すたびに1時的に配列を作成して、 値を格納してという作業を行うのは面倒です。 そこで、この作業を自動化しようというのが C# の可変長引数の考え方です。

C# では params というキーワードを使って可変個の引数を取るメソッドを定義することが出来ます。 例えば、上の例を params キーワードを使って書き直すと以下のようになります。

using System;

class ParamsTest
{
  static void Main()
  {
    int a = 314, b = 159, c = 265, d = 358, e  = 979;
    // ↑こいつらの最大値を探したいとき、

    int max = Max(a, b, c, d, e);
    // ↑こうすると、自動的に配列を作って値を格納してくれる。

    Console.Write("{0}\n", max);
  }

  static int Max(params int[] a)
  {
    int max = a[0];
    for(int i=1; i<a.Length; ++i)
    {
      if(max < a[i])
        max = a[i];
    }
    return max;
  }
}

メソッド定義側の変更点は引数 int[] a の前に params キーワードが付いただけです。 呼び出し側では、手動で配列を用意して値を格納しなくても、 可変個の引数を与えてメソッドを呼び出すことが出来ます。

サンプル

今まで何気なく Console.Write("(x, y) = ({0}, {1})\n", x, y) というような書き方をしていましたが、この Console.Write メソッドは可変長引数の機構を使っています。

ここでは、params の例として、 かなり簡略化したものですが、Console.Write もどきを作ってみます。

using System;

class TestParams
{
  static void Main(string[] args)
  {
    double x = 3.14;
    int    n = 99;
    string s = "test string";
    bool   b = true;

    Write("x = {0}, n = {1}, s = {2}, b = {3}\n", x, n, s, b);
  }

  /// <summary>
  /// Console.Write もどき。
  /// {0:d5} のような書式指定は出来ません。
  /// </summary>
  /// <param name="format">書式指定文字列</param>
  /// <param name="args">format を使用して書き込むオブジェクトの配列</param>
  static void Write(string format, params object[] args)
  {
    for(int i=0; i<args.Length; ++i)
    {
      format = format.Replace("{" + i.ToString() + "}", args[i].ToString());
    }
    Console.Write(format);
  }
}
x = 3.14, n = 99, s = test string, b = True

余談: params IEnumerable

現状の C# では、params 修飾子を付けれるのは配列だけです。

    // OK
    static int Sum(params int[] source)
    {
        var sum = 0;
        foreach (var x in source) sum += x;
        return sum;
    }

    // コンパイル エラー
    static int Sum(params IEnumerable<int> source)
    {
        var sum = 0;
        foreach (var x in source) sum += x;
        return sum;
    }

IEnuemrable<T> インターフェイス(System.Collections.Generic 名前空間)を使った可変長引数を書きたいという要望は多いんですが、 今のところ実装に至っていません。

何が問題かというと、具体的にどういう型を作って渡すのがいいかでもめます。 配列の場合は、配列が具象型なので、配列以外を作る選択肢がありませんが、IEnumerable の場合は何を作るかを選べます。 せっかくなので、配列よりも都合がいい型を採用できないか(値型にしてヒープ確保を避けるとか、immutable な型にして安全性を上げるとか)ということが検討されています。

余談: 可変長引数を引数なしで呼ぶ

可変長引数にしたメソッドは、引数なしで呼ぶこともできます。 この場合、呼び出された側のメソッドには、空配列(長さ0の配列)が渡ります。

using System;

class Program
{
    static void Main(string[] args)
    {
        var x = Sum();
        Console.WriteLine(x); // 0
    }

    static int Sum(params int[] source)
    {
        // 引数なしで呼ばれた場合、source には空配列が入る
        // source が null にはならない
        var sum = 0;
        foreach (var x in source) sum += x;
        return sum;
    }
}

ちなみに、空配列の作られ方ですが、 .NET Frameworkのバージョンによって変化します。 .NET Framework 4.6以降/.NET Coreでは、Array.Emptyという空配列を作るためのメソッドが用意されています。 これがある(つまり、.NET Framework 4.6以降か、.NET Coreがターゲットになっている)場合、このメソッドが呼ばれます。 なければ、new T[0]で空配列を作ります。

つまり、上記のvar x = Sum()は、.NET Framework 4.5以前であれば以下のように解釈されます。

// .NET Framework 4.5 以前はこういう扱い
var x = Sum(new int[0]);

一方、.NET Framework 4.6以降であれば以下のように解釈されます。

// .NET Framework 4.6 以降はこういう扱い
var x = Sum(Array.Empty<int>());

これらの差・変更の理由は単純で、Array.Emptyを使う方がパフォーマンスが良いです。 new int[0]だと、メソッド呼び出しのために新しい配列のインスタンスが作られますが、 Array.Emptyは最初に作った1つのインスタンスをキャッシュしてずっと使いまわします。

一応、昔からあるプログラムの挙動が変わる可能性がある破壊的変更なので注意してください。 狙ってやらないと起こせないような珍しい問題ですが、 例えば以下のようなコードの挙動は、.NET Framework のバージョンによって変化します。

using System;

class Program
{
    static void Main(string[] args)
    {
        var x = IsCached();
        Console.WriteLine(x);
        var y = IsCached();
        Console.WriteLine(y); // ターゲットによって結果が変わる
    }

    static int[] prev;

    static bool IsCached(params int[] source)
    {
        // .NET 4.5 以前だと、毎回違う配列がnewされて渡ってくる
        // .NET 4.6 以降だと、毎回同じインスタンスが使いまわされる
        if (prev == source) return true;

        prev = source;
        return false;
    }
}

__arglist

ちなみに、仕様書にない隠し機能ではあるんですが、 マイクロソフト製や、Mono 製の C# コンパイラーには、可変長引数のための構文として、もう1つ、__arglist というものがあります。 詳しくは「型付き参照」で説明します。

この隠し機能は主に C# 以外のプログラミング言語との相互運用にあるためのものです。 実際のところ、あまり性能はよくない(params を使ったものと比べると1桁は余裕で遅い)ので、わざわざ使うものではないでしょう。

更新履歴

ブログ