C Sharpの基礎 - 可変長引数

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

C#では、paramsキーワードを使用することで、メソッドの引数の数を可変にすることができる。

 // 定義側の例
 int Sum(params int[] args)
 {
    // 処理
 }

 // 利用側の例
 Sum(1, 2, 3, 4, 5);  // Sum(new int[] { 1, 2, 3, 4, 5 }); と同じ意味



paramsキーワード

以下の2つの例は、可変個の整数のうち最大の整数を求めるメソッドである。

可変長引数を使用しない場合

可変長引数を使用せずにメソッドを実装すると、以下のようになる。

以下の方法では、値を配列に格納してからMaxメソッドを呼び出すという操作が必要になる。
Maxメソッドを呼び出すたびに一時的に配列を作成して、値を格納するという作業を行うのは面倒である。

 
 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);
       
       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;
   }
 }


可変長引数を使用する場合

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;
    }
 }


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


サンプルコード

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キーワードを使用できるものは配列だけである。

IEnuemrable<T>インターフェイス(System.Collections.Generic名前空間)を使用した可変長引数を記述したいという要望は多いが、
現在まで実装に至っていない。

これは、具体的にどのような型を作成して渡すことがよいかで揉めている。
配列の場合は配列が具象型であるため、配列以外を作成する選択肢は無いが、IEnumerable<T>の場合は型の種類が選択できる。
現在、配列よりも都合がよい型を採用できないか(値型にしてヒープ確保を避ける、immutableな型にして安全性を上げる等)が検討されている。

 // コンパイル可能
 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;
    }
 }



可変長引数を引数無しで呼ぶ

可変長引数があるメソッドは、引数無しで呼ぶこともできる。
この場合、呼び出されたメソッドには、空配列(長さ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.5以前では、new T[0]で空配列が生成される。

つまり、上記のサンプルコードにおいて、var x = Sum()は、.NET Framework 4.5以前であれば、以下のように解釈される。

 // .NET Framework 4.5以前
 var x = Sum(new int[0]);
 
 // .NET Framework 4.6以降
 var x = Sum(Array.Empty<int>());


2つの方法が存在する理由は、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 Framework 4.5以前の場合、毎回違う配列をnewして渡す
       // .NET Framework 4.6以降の場合、毎回同じインスタンスを使いまわす
       if (prev == source)
       {
          return true;
       }
 
       prev = source;
       return false;
    }
 }