連載
» 2017年11月01日 05時00分 公開

.NET TIPS:可変長引数を持つメソッドを作成するには?[C#/VB]

可変長引数を受け取るメソッドを定義する方法と、位置指定引数/オプション引数との組み合わせ、可変長引数を持つメソッドを呼び出す際の注意点を取り上げる。

[山本康彦,BluewaterSoft/Microsoft MVP for Windows Development]
「.NET TIPS」のインデックス

連載目次

 引数の個数が可変なメソッドがある。Stringクラス(System名前空間)のFormatメソッドなどだ。そのようなメソッドを自分で作るにはどうしたらよいだろうか? 本稿ではその方法を解説する。

 特定のトピックをすぐに知りたいという方は以下のリンクを活用してほしい。

 なお、可変長引数は.NET Frameworkバージョン1.0から利用できるが、本稿に掲載したサンプルコードをそのまま試すにはVisual Studio 2017以降が必要である。また、サンプルコードはコンソールアプリの一部であり、コードの冒頭に以下の宣言が必要となる。

using System;
using static System.Console;

Imports System.Console

本稿のサンプルコードに必要な宣言(上:C#、下:VB)

可変長引数を持つメソッドを作成するには?

 個数を可変にしたい引数を配列にして、ParamArray属性(System名前空間)を付ければよい。ただし、ParamArray属性の代わりに使えるキーワードparams(C#)/ParamArray(Visual Basic)が用意されているので、通常はそちらを使う(C#ではParamArray属性を直接書くとコンパイルエラーになる)。

 例えば、任意の数の整数を引数に取るメソッドのシグネチャは次のコードのようになる。

int GetFirstEven(params int[] args)

Function GetFirstEven(ParamArray args As Integer()) As Integer

整数の可変長引数を持つメソッドシグネチャの例(上:C#、下:VB)

 このメソッドに引数として3つの整数をカンマで区切って渡したとすると、args配列に3つの整数が格納された状態でメソッドが呼び出される。

 このメソッドが引数なしで呼び出された場合は、args配列は(nullにはならず)長さ0となる。引数としてnullが渡されたときにはargsもnullになるので、注意が必要だ(次のコード)。

// 引数リストに登場する最初の偶数を返す
//(見つからなければ-1、引数が空のときは-99、引数がnullのときは-999)
static int GetFirstEven(params int[] args)
{
  // 引数としてnullが渡されることもある
  if (args is null)
    return -999;

  // 引数が1つもないときは、nullではなく、空の配列
  if (args.Length == 0)
    return -99;

  // 最初の偶数を探して返す
  foreach (int n in args)
    if (n % 2 == 0)
      return n;

  // 見つからなかったら-1
  return -1;
}

' 引数リストに登場する最初の偶数を返す
'(見つからなければ-1、引数が空のときは-99、引数がNothingのときは-999)
Function GetFirstEven(ParamArray args As Integer()) As Integer
  ' 引数としてNothingが渡されることもある
  If (args Is Nothing) Then
    Return -999
  End If

  ' 引数が1つもないときは、Nothingではなく、空の配列
  If (args.Length = 0) Then
    Return -99
  End If

  ' 最初の偶数を探して返す
  For Each n As Integer In args
    If (n Mod 2 = 0) Then
      Return n
    End If
  Next

  ' 見つからなかったら-1
  Return -1
End Function

整数の可変長引数を持つメソッドの例(上:C#、下:VB)
このメソッドを呼び出した結果は、すぐ後に示す。

なお、C#の「args is null」という書き方は、C# 7で導入された「is演算子の定数パターン」と呼ばれるものだ。詳細は「特集:C# 7の新機能詳説:第3回 型による分岐の改良(p.2)」をご覧いただきたい。


 上のメソッドを呼び出した例を示す(次のコード)。

int firstEven = GetFirstEven(1, 2, 3);
WriteLine($"1, 2, 3 → {firstEven}");
// 出力:1, 2, 3 → 2

firstEven = GetFirstEven(1, 3, 5);
WriteLine($"1, 3, 5 → {firstEven}");
// 出力:1, 3, 5 → -1

firstEven = GetFirstEven();
WriteLine($"引数なし → {firstEven}");
// 出力:引数なし → -99

firstEven = GetFirstEven(null);
WriteLine($"null → {firstEven}");
// 出力:null → -999

Dim firstEven As Integer = GetFirstEven(1, 2, 3)
WriteLine($"1, 2, 3 → {firstEven}")
' 出力:1, 2, 3 → 2

firstEven = GetFirstEven(1, 3, 5)
WriteLine($"1, 3, 5 → {firstEven}")
' 出力:1, 3, 5 → -1

firstEven = GetFirstEven()
WriteLine($"引数なし → {firstEven}")
' 出力:引数なし → -99

firstEven = GetFirstEven(Nothing)
WriteLine($"Nothing → {firstEven}")
' 出力:Nothing → -999

整数の可変長引数を持つメソッドを呼び出す例(上:C#、下:VB)
最初の2例は引数が3個、3つ目は引数なし、最後は引数としてnullを渡している。引数なしと引数がnullとではメソッドの挙動が異なっている。

他の引数と組み合わせるには?

 通常の引数と組み合わせるときは、可変長引数を引数リストの最後に置かねばならない。C#ではオプション引数とも組み合わせられるが、そのときは通常の引数→オプション引数→可変長引数の順に並べる(次のコード)。

// 可変長引数は、引数リストの最後に1回だけ
static void SampleMethod1a(int n, params object[] args) { }
// 次はコンパイルエラー
// static void SampleMethod1b(int n, params object[] args1, params object[] args2) { }
// 次はコンパイルエラー
// static void SampleMethod1c(params object[] args, int n) { }

// オプション引数との組み合わせ
// C#では、オプション引数の後ろに可変長引数を置ける
static void SampleMethod2a(int n, int m = 0, params object[] args) { }
// 次はコンパイルエラー
// static void SampleMethod2b(int n, params object[] args, int m = 0) { }

' 可変長引数は、引数リストの最後に1回だけ
Sub SampleMethod1a(n As Integer, ParamArray args As Object())
End Sub
' 次はコンパイルエラー
' Sub SampleMethod1b(n As Integer, ParamArray args1 As Object(), ParamArray args2 As Object())
' End Sub
' 次はコンパイルエラー
' Sub SampleMethod1c(ParamArray args As Object(), n As Integer)
' End Sub

' オプション引数との組み合わせ
' VBは不可(コンパイルエラー)
' Sub SampleMethod2a(n As Integer, Optional m As Integer = 0, ParamArray args As Object())
' End Sub

他の引数と可変長引数を組み合わせる例(上:C#、下:VB)

引数として配列を渡すときの注意点は?

 object型の可変長引数を受け取るメソッドでは、引数の1つとして渡したつもりの配列が分解されて複数の引数として扱われてしまう場合がある。そのようなときには、配列を1つのobject型にキャストしてから渡せばよい。

 言葉だけでは分かりにくいと思うので、実際に試してみよう。

 まず、次のコードのような可変長引数を受け取るメソッドを用意する。object型の引数を任意の個数だけ引数リストに指定できるというメソッドだ。渡されたargs配列の要素が整数なのか配列なのかを判別してコンソールに出力するようになっている。

static void SampleMethod3(params object[] args)
{
  for (int i = 0; i < args.Length; i++)
    switch (args[i])
    {
      case int n:
        WriteLine($"{i + 1}番目はintで、値は{n}です"); break;
      case Array a:
        WriteLine($"{i + 1}番目は配列です"); break;
      default:
        WriteLine($"{i + 1}番目は想定外の型です"); break;
    }
}

Sub SampleMethod3(ParamArray args As Object())
  For i As Integer = 0 To args.Length - 1
    If (TypeOf args(i) Is Integer) Then
      WriteLine($"{i + 1}番目はintで、値は{args(i)}です")
    ElseIf (TypeOf args(i) Is Array) Then
      WriteLine($"{i + 1}番目は配列です")
    Else
      WriteLine($"{i + 1}番目は想定外の型です")
    End If
  Next
End Sub

配列を渡されたときの挙動を見るための可変長引数を持つメソッド(上:C#、下:VB)
渡された引数がそれぞれ整数なのか配列なのかを判別して、コンソールに出力する。

なお、C#のこのswitch文の書き方は、C# 7で導入された「switch文の型パターン」と呼ばれるものだ。詳細は「特集:C# 7の新機能詳説:第3回 型による分岐の改良(p.2)」をご覧いただきたい。


 上のメソッドに対して、object配列をそのまま渡すと分解される(次のコード)。渡した配列がそのままargs配列として扱われるのだ。

var array = new object[] { 1, 2 };

// 配列のまま渡すと、分解される
SampleMethod3(array);
// 出力:1番目はintで、値は1です
// 出力:2番目はintで、値は2です

Dim array = New Object() {1, 2}

' 配列のまま渡すと、分解される
SampleMethod3(array)
' 出力:1番目はintで、値は1です
' 出力:2番目はintで、値は2です

前述のメソッドに対して、object配列をそのまま渡した場合(上:C#、下:VB)

 上の結果が想定通りならよい。そうではなくて、渡した配列はargs配列の1つの要素になってほしいときもある。その場合は、配列を1つのobject型のインスタンスにキャストしてから渡せばよい(次のコード)。

var array = new object[] { 1, 2 };

// 分解を回避するには、1つのobjectにキャストして渡す
SampleMethod3((object)array);
// 出力:1番目は配列です

Dim array = New Object() {1, 2}

' 分解を回避するには、1つのObjectにキャストして渡す
SampleMethod3(CType(array, Object))
' 出力:1番目は配列です

前述のメソッドに対して、配列を1つのobjectにキャストして渡した場合(上:C#、下:VB)

まとめ

 メソッドの引数リストの最後を配列にしてparams(C#)/ParamArray(Visual Basic)キーワードを付けると、その部分が可変長引数になる。可変長引数を受け取る配列がnullになり得ることには注意しよう。

利用可能バージョン:.NET Framework 1.0以降
カテゴリ:C# 処理対象:言語構文
カテゴリ:Visual Basic .NET 処理対象:言語構文
使用ライブラリ:ParamArray属性(System名前空間)
関連TIPS:オプション引数が使えるメソッドを作るには?[C#/VB]
関連TIPS:メソッドを呼び出すときに名前付き引数を使うには?[C#/VB]
関連TIPS:構文:クラス名を書かずに静的メソッドを呼び出すには?[C# 6.0]
関連TIPS:VB.NETでクラス名を省略してメソッドや定数を利用するには?
関連TIPS:数値を右詰めや0埋めで文字列化するには?[C#、VB]


「.NET TIPS」のインデックス

.NET TIPS

Copyright© 1999-2017 Digital Advantage Corp. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。