配列(array)とは同じデータ型の要素が集まったデータ構造で、固定長の集合ベクトルです。 ここでは配列の宣言・作成方法と、配列を使った基本的な操作について見ていきます。 配列内にある要素の検索や、配列のリサイズといった操作については配列操作で解説します。

なお、ここでは次元のない配列(1次元配列)のみを扱います。 多次元配列については多次元配列・ジャグ配列で個別に解説します。 また可変長のデータ構造としてリストなどのコレクションなども存在しますが、ここでは扱いません。

§1 配列とは

プログラム要素として見た場合の配列とは、複数の同じ種類(=同じ)の値に名前を与えてひとまとめにして扱うものです。 複数の変数の場合は個々の値にそれぞれ名前が与えられますが、配列では値の集まりにひとつの名前(=配列の変数名)が与えられ、配列内の個々の値(要素)はインデックスによって区別します(インデックスは添え字とも呼ばれる)。

複数の変数と配列
using System;

class Sample {
  static void Main()
  {
    // 複数の変数
    string user0 = "Alice";
    string user1 = "Bob";
    string user2 = "Charlie";

    // 配列
    string[] users = new string[3];

    users[0] = "Alice";
    users[1] = "Bob";
    users[2] = "Charlie";
  }
}

上記の例で言えば、user0, user1, user2の各変数は名前から関連性が示唆されるものの独立した値として存在します。 一方usersは、インデックスによって個々の値が区別されると同時に、その値の集合全体にも名前が与えられていて、ひとまとめの値として存在します。

複数の変数
user0 "Alice"
user1 "Bob"
user2 "Charlie"
配列
users
0 "Alice"
1 "Bob"
2 "Charlie"


§2 配列の作成

言語によって構文に多少の違いはありますが、配列を作成するには、次のように格納される要素の型と格納できる要素数(配列の長さ)を指定して行います。 構文は似通っていますが、C#では格納できる要素数、VBではインデックスの最大値を指定するという間違いやすい相違点があるので、両言語を並行して扱う際は注意が必要です。

配列の作成
using System;

class Sample {
  static void Main()
  {
    // 長さが3(格納できる要素数が3)の文字列型配列を作成
    string[] arr1 = new string[3];

    // 長さが12(格納できる要素数が12)のint型配列を作成
    int[] arr2 = new int[12];

    // 長さが0(格納できる要素数が0)のint型配列を作成
    int[] arr3 = new int[0];
  }
}

上記の例のように長さ(要素数)が0の配列(=要素が1つも格納されない配列)も作成することが出来ます。 長さ・要素数を0には出来ますが、当然マイナスにすることは出来ません。

長さが0の配列については後述の§.空の配列でも詳しく解説します。

配列の作成方法のバリエーションについては後述の§.配列の宣言と初期化を参照してください。

§3 要素の参照

.NET Frameworkにおける配列では、要素のインデックス(通番・要素の番号)は0を基準とします(zero-based)。 つまり、最初の要素のインデックスは0、その次の要素のインデックスは1、さらにその次は2…、となります。 以降の解説でも、0番目とは最初の要素を指すものとして説明していきます。 C#では配列の変数に続けてかぎ括弧 [ ]、VBでは丸括弧 ( )を使ってインデックスを指定します。

配列要素への値の設定と参照
using System;

class Sample {
  static void Main()
  {
    // 長さが3(格納できる要素数が3)の文字列型配列を作成
    string[] users = new string[3];

    users[0] = "Alice";     // 配列の0番目(最初)の要素として"Alice"を設定
    users[1] = "Bob";       // 配列の1番目の要素として"Bob"を設定
    users[2] = "Charlie";   // 配列の2番目(最後)の要素として"Charlie"を設定

    // 配列の1番目に格納されているの値を取得して表示
    string user1 = users[1];

    Console.WriteLine(user1);
  }
}
実行結果
Bob

配列は固定長であり、配列を作成するときに格納できる要素数が決定されます。 配列の長さ(格納できる要素数)を超えるインデックスを指定すると実行時に例外IndexOutOfRangeExceptionがスローされます。 配列が自動的に拡張されるといったことは行われません。

配列の長さを超えるインデックスを参照しようとした場合
using System;

class Sample {
  static void Main()
  {
    // 長さが3(格納できる要素数が3)の文字列型配列を作成
    string[] users = new string[3];

    users[2] = "Charlie";   // 配列の2番目(最後)の要素として"Charlie"を設定 (問題なく動作する)
    users[3] = "Dave";      // 配列の3番目(最後の一つ後)の要素として"Dave"を設定 (インデックスが範囲外のため例外がスローされる)
  }
}
実行結果
ハンドルされていない例外: System.IndexOutOfRangeException: インデックスが配列の境界外です。
   場所 Sample.Main()

つまり、JavaScript等の言語では認められている次のような操作は、.NET Frameworkにおける配列ではサポートされません。

JavaScript等では認められているこのような操作を行うことはできない
// 配列を作成
var users = new Array();

users[2] = "Charlie"; // 配列の2番目に"Charlie"を設定 (ここでarr.length = 3となる)
users[3] = "Dave";    // 配列の3番目に"Dave"を設定 (ここでarr.length = 4となる)

可変長の配列が必要な場合は、ジェネリックコレクションクラスの一つであるListクラスを使うことができます。 ただし、Listクラスでも上記のように途中のインデックスに要素を追加するようなことはできません。

既存の配列を拡張する必要がある場合はArray.Resizeメソッドを使うことができます。

§4 長さの取得

実行時に配列の長さ(配列の要素数・サイズ)を取得するには、Lengthプロパティを参照します。

配列の長さを取得する
using System;

class Sample {
  static void Main()
  {
    // 長さが3の配列
    string[] arr1 = new string[3];

    // 長さが12の配列
    int[] arr2 = new int[12];

    // 長さが0の配列
    double[] arr3 = new double[0];

    // それぞれの配列の長さを取得して表示
    Console.WriteLine("arr1.Length = {0}", arr1.Length);
    Console.WriteLine("arr2.Length = {0}", arr2.Length);
    Console.WriteLine("arr3.Length = {0}", arr3.Length);
  }
}
実行結果
arr1.Length = 3
arr2.Length = 12
arr3.Length = 0

多次元配列の場合、Lengthプロパティは多次元配列内の全要素数を返します。 (多次元配列・ジャグ配列 §.長さ・次元数の取得)

配列のバイト数を取得する場合は、Buffer.ByteLengthメソッドを使うことができます。 詳しくはバイト列操作 §.配列のバイト数の取得 (ByteLengthメソッド)を参照してください。

§5 配列の宣言と初期化

ここまでで紹介した例では、配列変数の宣言と、配列の作成・代入を同時に行っていました。 ここでは配列の宣言と、配列の作成・初期化の方法を分けて見ていきます。

§5.1 配列の宣言

まず、配列の宣言について見ていきます。 配列を格納する変数を宣言するには、次のようにします。

配列変数の宣言
using System;

class Sample {
  static void Main()
  {
    string[] arr1;  // string型の配列を格納する変数
    int[] arr2;     // int型の配列を格納する変数
    double[] arr3;  // double型の配列を格納する変数
  }
}

VBでは、丸括弧( )を変数名の後ろに付けた場合も、型名の後ろに付けた場合も、同じ配列変数として扱われます。 どちらも意味は同じです。

このようにして宣言した変数は、配列が格納できる変数が用意されるだけで、実際にはどの配列も参照していない(配列の実体を参照していない)状態となっています。 C#では未初期化の変数、VBではヌル参照の変数となります。 この状態から、配列を作成して変数に代入したり、他の変数が参照している配列やメソッドの戻り値として返される配列などを変数に代入したりする必要があります。

§5.2 配列の初期化

続いて配列の初期化について見ていきます。 次のようにすることで、配列変数の宣言と同時に配列を作成して変数に代入することが出来ます。

初期値を与えて配列を作成する (配列初期化子)
using System;

class Sample {
  static void Main()
  {
    // 長さが3の配列を作成し、各要素にそれぞれ文字列"Alice", "Bob", "Charlie"を設定する
    string[] arr1 = new string[3] {"Alice", "Bob", "Charlie"};
    string[] arr2 = new string[] {"Alice", "Bob", "Charlie"};
    string[] arr3 = {"Alice", "Bob", "Charlie"};

    // 長さが5の配列を作成し、各要素にそれぞれ数値0, 1, 2, 3, 4を設定する
    int[] arr4 = new int[5] {0, 1, 2, 3, 4};
    int[] arr5 = new int[] {0, 1, 2, 3, 4};
    int[] arr6 = {0, 1, 2, 3, 4};
  }
}

このコードにおいて、arr1, arr2, arr3はすべて同じ長さで同じ内容が格納された配列となります。 arr4, arr5, arr6も同様に同じ内容が格納されています。 このコードのnew以降の部分は配列作成式、中括弧 { } の部分は配列初期化子と呼ばれます。 配列作成式で配列の長さを指定する場合、配列初期化子で記述されている要素数と合っていないとコンパイルエラーとなります。

配列の長さを記述する場合は配列初期化子の要素数と一致していなければならない
using System;

class Sample {
  static void Main()
  {
    // 配列の長さと、配列初期化子の要素数が一致していないのでコンパイルエラーとなる
    string[] arr = new string[2] {"Alice", "Bob", "Charlie"};
    // error CS0847: An array initializer of length `2' was expected
  }
}

配列初期化子を記述しない場合は、指定された長さの配列が作成されますが、作成される配列内の各要素にはデフォルト値が設定されます。

デフォルト値が設定された配列を作成する
using System;

class Sample {
  static void Main()
  {
    string[] arr1 = new string[3];  // 長さが3のstring型配列
    int[] arr2 = new int[5];        // 長さが5のint型配列
    double[] arr3 = new double[12]; // 長さが12のdouble型配列
  }
}

デフォルト値は型によって異なりますが、数値などの値型では00に相当する値、文字列型やクラスなどの参照型ではヌル参照(null/Nothing)となります。

型とデフォルト値の詳細については型の種類・サイズ・精度・値域 §.型のデフォルト値、型の種類については値型と参照型を参照してください。


配列初期化子を記述しない場合は、次のように変数に代入されている値を長さとして配列を作成することもできます。 この場合も、各要素にデフォルト値が設定された状態の配列が作成されます。

変数で指定された長さの配列を作成する
using System;

class Sample {
  static void Main()
  {
    int length = 3;

    // 長さに変数lengthの値を指定して文字列配列を作成する
    string[] arr = new string[length];

    // 作成された配列の長さを取得して表示する
    Console.WriteLine(arr.Length);
  }
}
実行結果
3

§5.3 長さ0の配列

長さ0の配列(空の配列)の作成については後述の§.空の配列で解説します。

§6 配列フィールドの宣言

ローカル変数の場合と同様、クラスや構造体のフィールドとしても配列を宣言することもできます。

クラス・構造体のフィールドとして配列を宣言する
using System;

class C {
  // クラス内の配列フィールド
  int[] ArrayField;
}

struct S {
  // 構造体内の配列フィールド
  int[] ArrayField;
}

ただし、構造体内の配列フィールドには初期化子を指定することはできません。 一方、クラスの場合は配列フィールドに初期化子を指定することができます。

配列フィールドに初期化子を指定する
using System;

class C {
  // クラス内では配列フィールドに初期化子を指定することができる
  int[] ArrayField = new int[5];
}

struct S {
  // 構造体内では配列フィールドに初期化子を指定することはできない
  int[] ArrayField = new int[5]; // error CS0573: 'S.ArrayField': 構造体にインスタンスフィールド初期化子を指定することはできません。
}

構造体内の配列フィールドと初期化子については構造体と配列フィールドで詳しく解説しています。

§7 配列の代入

配列は参照型です。 そのため、配列の変数に代入を行う場合、配列がまるごとコピーされて代入されるのではなく、配列への参照が代入されるだけとなる点に注意が必要です。

次の例のように、配列の変数に代入を行っても配列の実体は一つであるため、異なる変数を経由した変更でも実際に行われる変更は同一の配列に対するものとなります。

配列の代入
using System;

class Sample {
  static void Main()
  {
    // 配列を作成してarr1に代入
    string[] arr1 = {"Alice", "Bob", "Charlie"};

    // arr1の各要素を表示
    Console.WriteLine("arr1[0] = {0}", arr1[0]);
    Console.WriteLine("arr1[1] = {0}", arr1[1]);
    Console.WriteLine("arr1[2] = {0}", arr1[2]);
    Console.WriteLine();

    // arr1に代入されている配列への参照をarr2に代入
    string[] arr2 = arr1;

    arr2[1] = "Dave"; // arr2の1番目の値を変更

    // arr1の各要素を表示
    Console.WriteLine("arr1[0] = {0}", arr1[0]);
    Console.WriteLine("arr1[1] = {0}", arr1[1]);
    Console.WriteLine("arr1[2] = {0}", arr1[2]);
  }
}
実行結果
arr1[0] = Alice
arr1[1] = Bob
arr1[2] = Charlie

arr1[0] = Alice
arr1[1] = Dave
arr1[2] = Charlie

配列のコピーを行いたい場合はArray.CloneメソッドまたはArray.Copyメソッドを使います。

§8 要素の列挙

配列内の各要素を列挙するには、for文とforeach文が使えます。 for文では配列のインデックスを使って各要素を参照するのに対し、foreach文では個別の変数によって列挙された各要素を参照します。

forとforeachによる配列内の要素の列挙
using System;

class Sample {
  static void Main()
  {
    // 配列を作成
    string[] arr = {"Alice", "Bob", "Charlie"};

    // for文を使って配列内の各要素を列挙
    for (int i = 0; i < arr.Length; i++) {
      Console.WriteLine("arr[{0}] = {1}", i, arr[i]);
    }

    Console.WriteLine();

    // foreach文を使って配列内の各要素を列挙
    // (列挙した各要素は変数elemに代入される)
    foreach (string elem in arr) {
      Console.WriteLine(elem);
    }
  }
}
実行結果
arr[0] = Alice
arr[1] = Bob
arr[2] = Charlie

Alice
Bob
Charlie

foreach文では常に配列の先頭から一つずつ列挙されるため、配列の末尾から先頭に向けて列挙したり、一つ飛ばしで列挙したりといったことは出来ません。 そういった列挙が必要な場合はforeach文ではなくfor文を使って記述する必要があります。

for文とforeach文の使い分けは概ね以下のようになると言えます。

for文
列挙する際に要素の位置(インデックス)が重要となる場合
インデックスによって各要素に対して行う処理を振り分けたい場合(インデックスn以上の要素だけ参照したい、インデックスがnのときだけ処理を行いたいなど)
foreach文
列挙する際に要素の位置(インデックス)が重要でない場合
インデックスに関わらずすべての要素に同じ処理を行いたい場合

配列内から条件に合う要素を検索・取得したりするにはArrayクラスのFindメソッドやExistsメソッドなどを使うことができます。 また、配列内の要素の順番を逆にして列挙したい場合はReverseメソッドを使うことができます。

for文とforeach文による配列の列挙では、パフォーマンス上の差はほとんどありません。

foreach文での列挙は配列に実装されているIEnumerableインターフェイスから提供される機能によって実現しています。 配列とIEnumerableインターフェイスについては後述の§.配列のインターフェイス、IEnumerableインターフェイスと列挙についてはIEnumerable・IEnumeratorで詳しく解説しています。

§9 要素の検索・ソート、配列全体への変更

配列内にある要素を検索する、ソートを行う、また配列のサイズを変更する、コピーする、配列全体の型変換を行うといった操作は、Arrayクラスに用意されているユーティリティメソッドを使うことで行うことができます。 Arrayクラスを使った配列の操作については配列操作で解説しています。

また、ソートに関しては個別に基本型のソートと昇順・降順でのソートでも詳しく解説しています。

§10 空の配列

.NET Frameworkでは空の配列を作成することができます。 空の配列とは、長さが0要素が1つも格納されていないあるいは1つも格納できない配列です。

§10.1 構文を用いた作成

空の配列(長さ0の配列)を作成したい場合は次のように記述することができます。

空の配列(長さが0の配列)を作成する
using System;

class Sample {
  static void Main()
  {
    // 以下のいずれも空の配列となる
    string[] arr1 = new string[0];
    string[] arr2 = new string[] {};
  }
}

空の配列はヌル参照の状態とは明確に異なります。 配列は参照しているが、その配列にはなにも格納されていない状態になります。

空の配列が代入された配列変数とヌル参照の配列変数の違い
using System;

class Sample {
  static void Main()
  {
    string[] arrEmpty = new string[0];

    // 空の配列ではプロパティの参照などを行うことができる
    Console.WriteLine(arrEmpty.Length);

    string[] arrNull = null;

    // ヌル参照の配列変数ではプロパティの参照などを行うことはできない
    Console.WriteLine(arrNull.Length); // NullReferenceExceptionがスローされる
  }
}

§10.2 Array.Emptyメソッドを用いた作成

.NET Framework 4.6以降ではArray.Empty<T>メソッドを使うことでも空の配列を取得することができます。 それより前のバージョンでは、Emumerable.Emptyメソッドの戻り値に対してToArrayメソッドを呼び出すことで空の配列を作成することができます。

Array.Emptyメソッド・Enumerable.Emptyメソッドを使って空の配列を作成する
using System;
using System.Linq;

class Sample {
  static void Main()
  {
    // Array.Emptyメソッドを使って空の配列を取得する(.NET Framework 4.6以降)
    string[] arr1 = Array.Empty<string>();
    int[] arr2 = Array.Empty<int>();

    // Enumerable.Emptyメソッドを使って空の配列を作成する(.NET Framework 3.5以降)
    string[] arr3 = Enumerable.Empty<string>().ToArray();
    int[] arr4 = Enumerable.Empty<int>().ToArray();
  }
}

呼び出される回数が膨大で配列の生成コストが無視できないようなメソッドや、使用可能なメモリが極端にシビアな実行環境などでは、空の配列を都度作成・取得するよりも、あらかじめキャッシュしておいたインスタンスを使用するようにするか、Type.EmptyTypesのようにあらかじめ用意された空の配列を使用することでパフォーマンスを向上させることができます。

Array.Emptyメソッドが常にキャッシュされたインスタンスを返すかどうか、呼び出しの度に空の配列のインスタンスを作成するのかどうかについてはドキュメントには明記されていません。

一方、Emumerable.Emptyメソッドでは以下のように記載されているため常にキャッシュされた空のシーケンスを返すことが保証されています。

Empty<TResult>() メソッドは、TResult 型の空のシーケンスをキャッシュします。

Enumerable.Empty<TResult> メソッド ()

しかし、Enumerable.Empty<T>().ToArray()の呼び出しでは、ToArrayメソッドが空のシーケンスから配列の作成を行うため呼び出しの度に空の配列のインスタンスが作成されます。

§10.3 ヌル参照と空の配列・戻り値や初期値としての空の配列

配列を返すメソッドでは、null/Nothingよりも空の配列を返したほうがよい場合があります。 配列を返すメソッドがnullを返す場合、呼び出し側は戻り値がnullかどうかの判定を行った上で列挙などの操作を行う必要がありますが、空の配列を返すようにすれば呼び出し側はnull判定を行わずに戻り値の配列に操作を行うことができます。

空の配列を返すメソッド
using System;
using System.IO;

class Sample {
  // ファイルから複数の値を読み込むメソッド
  static int[] ReadValuesFromFile(string file)
  {
    if (File.Exists(file)) {
      int[] values;

      // ファイルを開いて読み込んだが、読み込み対象の値が無かった場合を想定する
      // 値が無かったことを表すため、nullではなく空の配列を返す
      values = new int[0];

      return values;
    }
    else {
      throw new FileNotFoundException("ファイルがありません", file);
    }
  }

  static void Main()
  {
    // ファイルから読み込んだ値を列挙する
    // (ファイルに目的の値がなかった場合でもメソッドは空の配列を返すため、
    // null判定を行うことなく列挙を行うことができる)
    foreach (int val in ReadValuesFromFile("data.txt")) {
      Console.WriteLine(val);
    }
  }
}

上記の例では、ファイルが無かった場合にもnullや空の配列を返すようにすることもできますが、推奨できません。 例えば、上記のメソッドにエラー処理を追加してファイルの内容が異常だった場合にもnullを返すような実装にした場合、メソッドの呼び出し側は戻り値がnullとなった原因が何なのかを知ることができません。

このため、なんらかのエラーがあった場合には、nullや空の配列を返すよりも、ファイルが無かった場合はFileNotFoundException、ファイルの内容やフォーマットが異常だった場合はInvalidDataExceptionをスローするというように、状況にあった例外をスローして呼び出し側に原因を通知すべきです。


配列フィールドでも、初期値としてnull/Nothingの代わりに空の配列を指定しておくことができます。 メソッドの場合と同様、初期値として空の配列を指定しておくことでnull判定をせずに配列フィールドの参照ができるようになります。

初期値として空の配列を設定したフィールド
class Account {
  public string Name;
  public string[] Address = new string[0]; // 初期値として空の配列を代入しておく
}

一方この場合、空の配列よりもnull/Nothingの方が初期値として妥当な場合もあります。 例えば、このフィールドがデータベースからの参照やユーザー入力によって設定されるものであれば、まだ設定が行われていないことを表す目的でnullを初期値としておいたほうが妥当と考えることもできます。 (もちろん、未入力を表すフラグは別に用意し、初期値は空の配列にしておく、という選択もありえます。)


このように、空の配列を用いることでヌル判定やヌル参照の状況をさけることができる一方で、空の配列とヌル参照では意味が全く異なるため、空の配列とヌル参照を混同したり、単純に置き換えることはできません。 どちらがより妥当か十分検討する必要があります。

§11 部分配列

Pythonなどの言語において用いることができる配列内の区間を指定して部分配列を取り出す構文(arr[from..to]arr[from:until]など)は、C#・VBには用意されていません。 また、JavaScriptなどの言語におけるsliceのようなメソッドも用意されていません。

部分配列の切り出し(Python)
lst = [0, 1, 2, 3, 4, 5]
segment = lst[2:5] # from 2 until 5

print lst
print segment
実行結果
[0, 1, 2, 3, 4, 5]
[2, 3, 4]
部分配列の切り出し(Ruby)
arr = [0, 1, 2, 3, 4, 5]
segment = arr[2..4] # from 2 to 4

p array
p segment
実行結果
[0, 1, 2, 3, 4, 5]
[2, 3, 4]
部分配列の切り出し(JavaScript)
var arr = [0, 1, 2, 3, 4, 5];
var subarr = arr.slice(2, 5); // from 2 until 5

console.log(arr);
console.log(subarr);
実行結果
Array [ 0, 1, 2, 3, 4, 5 ]
Array [ 2, 3, 4 ]

このような部分配列を切り出すような構文やメソッドは用意されていませんが、ArraySegment構造体を用いるとこれに似た操作を行うことができます。

ArraySegment構造体を使って部分配列を構成する
using System;

class Sample {
  static void Main()
  {
    var arr = new int[] {0, 1, 2, 3, 4, 5};

    // C#では以下のような構文は用意されていない
    //var segment = arr[2..4];

    // 配列にはこのようなメソッドは用意されていない
    //var segment = arr.Slice(2, 4);

    // ArraySegmentを用いて、配列arrのインデックス2から3つ分の要素を参照する部分配列を作成する
    var segment = new ArraySegment<int>(arr, 2, 3); // from 2 length 3

    Console.WriteLine(string.Join(", ", arr));
    Console.WriteLine(string.Join(", ", segment));
  }
}
実行結果
0, 1, 2, 3, 4, 5
2, 3, 4

部分配列を取得する方法や、配列の一部分をコピーして部分配列を作成する方法については部分配列を参照してください。

§12 配列のインターフェイス

配列(厳密には配列の基底クラスであるArrayクラス)は、IEnumerable, ICollection, IListなどのインターフェイスを実装しています。 これにより、配列も一種のコレクションとして、他のコレクションと同等に扱うことができるようになっています。

例えば、IEnumerableまたはIEnumerable<T>を引数にとるメソッドでは配列を指定することができます。 String.Concatメソッド(.NET Framework 4以降)では、任意の型TのIEnumerable<T>を引数にとることができます。 このメソッドに配列を指定することができ、配列内の各要素の値を結合した文字列を取得することができます。

String.Concatメソッドを使って配列内の値を結合した文字列を作成する
using System;

class Sample {
  static void Main()
  {
    int[] arr = {0, 1, 2, 3, 4};

    string s = string.Concat(arr);

    Console.WriteLine(s);
  }
}
実行結果
01234

またWhereなどのLINQの各メソッドもIEnumerable<T>を引数にとりますが、配列はIEnumerable<T>を実装しているため配列に対してもLINQのメソッドを使用することができます。

LINQのメソッドを使って配列を操作する
using System;
using System.Linq;

class Sample {
  static void Main()
  {
    int[] arr = {0, 1, 2, 3, 4};

    // 配列の中にある奇数の要素のみを抽出し、そのうち最大のものを取得して表示する
    Console.WriteLine(arr.Where(e => e % 2 != 0).Max());
  }
}
実行結果
3

配列がICollectionなどのインターフェイスを実装しているといっても、その機能のすべてがサポートされるわけではありません。 たとえば、配列をICollectionインターフェイスにキャストして操作することは可能ですが、ICollection.AddICollection.Removeなどのメソッドを呼び出して配列のサイズを変更するような操作を行うことはできません。 このような操作を行おうとした場合にはNotSupportedExceptionがスローされます。

ICollectionインターフェイスを介して配列ではサポートされないメソッドを呼び出す場合
using System;
using System.Collections.Generic;

class Sample {
  static void Main()
  {
    int[] arr = {0, 1, 2, 3, 4};

    // 配列をICollection<T>インターフェイスにキャスト
    ICollection<int> coll = arr;

    // ICollectionインターフェイスを介して要素を追加しようとしても、
    // 配列は固定長であるためNotSupportedExceptionがスローされる
    coll.Add(5);
  }
}
実行結果
System.NotSupportedException: コレクションは固定サイズです。
   at System.SZArrayHelper.Add[T](T value)
   at Sample.Main(String[] args)

配列が実装するインターフェイスや他のコレクションとの特徴の違いについてはコレクションの種類と特徴 §.コレクションクラスの特徴を参照してください。

§13 配列の使用と代替手段

ここでは配列が適用できる場合と、それが不適切となる場合、またその場合の代替手段やより適切な実装方法などについて解説します。

§13.1 object配列

object型の配列では任意の型の値を格納することができますが、object配列が必要になる場面はそう多くなく、また必要になった場合でもそれはデータ構造を定めていないことによる不適切な実装である可能性があります。

object配列を使って複数のデータ型の値を格納する
using System;

class Sample {
  static void Main()
  {
    object[] data = new object[4];

    data[0] = "Alice";
    data[1] = 24;
    data[2] = "Bob";
    data[3] = 16;
  }
}

こういった場合、格納するデータの構造をクラスや構造体で適切に定め、それに対応した配列型を使用すべきです。 安易にobject型配列を用いる前に、データ構造が適切に定められているか、既存の型を使ってデータを扱えないか検討してください。

独自に定義したデータ型を使って値を配列に格納する
using System;

// 配列に格納するデータの構造を表すクラス
class Account {
  public string Name;
  public int Age;
}

class Sample {
  static void Main()
  {
    Account[] accounts = new Account[2];

    accounts[0] = new Account() {Name = "Alice", Age = 24};
    accounts[1] = new Account() {Name = "Bob", Age = 16};
  }
}

§13.2 可変長の引数

引数で配列を指定することにより、任意個の値をメソッドに渡すことができます。 この場合、あらかじめ引数に渡す値を配列としてまとめておく必要があります。

配列を使って引数に任意個の値を渡す
using System;

class Sample {
  // 引数で与えられた配列の合計を求めるメソッド
  static long Sum(int[] arr)
  {
    long sum = 0L;

    foreach (int val in arr) {
      sum += val;
    }

    return sum;
  }

  static void Main()
  {
    int[] arr = new int[] {2, 5, 3, 8, 4};

    Console.WriteLine("合計 = {0}", Sum(arr));
  }
}

配列型の引数とは別に、paramsキーワード(C#)・ParamArrayキーワード(VB)を使うことにより、引数を可変長とすることができます。 可変長の引数では、引数に渡す値は必ずしも配列にする必要はありません(配列を直接渡すこともできます)。 メソッド側では、可変長引数は通常の配列と同様に扱うことができます。

可変長引数を使って任意個の値を渡す
using System;

class Sample {
  // 引数で与えられた複数の値の合計を求めるメソッド
  static long Sum(params int[] arr)
  {
    long sum = 0L;

    foreach (int val in arr) {
      sum += val;
    }

    return sum;
  }

  static void Main()
  {
    Console.WriteLine("合計 = {0}", Sum(2, 5, 3));
    Console.WriteLine("合計 = {0}", Sum(2, 5, 3, 8, 4));
    Console.WriteLine("合計 = {0}", Sum(new int[] {2, 5, 3, 8, 4})); // 配列を渡すこともできる
  }
}

可変長引数は、必ずメソッドの一番最後の引数でなければなりません。 例えば、可変長引数のあとに通常の引数をとるようなメソッドを作成することはできません。

可変長引数を定義できる位置
// このように通常の引数のあとに可変長引数をとるようなメソッドは宣言できる
void ParamsMethodOK(int firstValue, params int[] values) {}

// このように可変長引数のあとに通常の引数をとるようなメソッドは宣言できない
void ParamsMethodNG(params int[] values, int lastValue) {}

可変長引数の型をobjectにすることにより、任意の型の値を任意個とることができるようになります。 例えば、Console.WriteLineメソッドString.Joinメソッドではobject型の可変長引数をとることができるようになっています。

§13.3 複数の戻り値

メソッドの戻り値を配列にすることでメソッドから複数の値を返すことができます。 しかし、例えば意味の異なる値を配列でひとまとめにして返すようにすると、格納されている値の意味があいまいになり、また呼び出し側では戻り値を配列から展開する必要があります。

商と剰余を配列に格納して返すメソッド
using System;

class Sample {
  // xとyの商と剰余を求めるメソッド
  // 商は戻り値のインデックス0、剰余はインデックス1に格納する
  static int[] DivRem(int x, int y)
  {
    int[] ret = new int[2];

    ret[0] = x / y;
    ret[1] = x % y;

    return ret;
  }

  static void Main()
  {
    // 7÷3の商と剰余を求めたい
    int[] ret = DivRem(7, 3);

    // 戻り値から商と剰余に展開しなければならない
    // また戻り値の配列に格納されている値の意味があいまいになる
    int div = ret[0];
    int rem = ret[1];
  }
}

このような場合は、配列よりもoutパラメータ(VBではByRef引数)を使ったほうがより適切です。

商と剰余をoutパラメータ・ByRef引数に格納して返すメソッド
using System;

class Sample {
  // xとyの商と剰余を求めるメソッド
  // 商は第3引数のoutパラメータ、剰余は第4引数のoutパラメータに格納する
  static void DivRem(int x, int y, out int div, out int rem)
  {
    div = x / y;
    rem = x % y;
  }

  static void Main()
  {
    // 7÷3の商と剰余を求めたい
    int div, rem;

    DivRem(7, 3, out div, out rem);

    // メソッドの呼び出し後、変数divとremに結果が格納される
  }
}

outパラメータ・ByRef引数はメソッドの戻り値と併用することもできます。 実際、.NET Frameworkで用意されている商と剰余を求めるMath.DivRemメソッドでは、商は戻り値として、剰余はoutパラメータで返されるようになっています。

Math.DivRemメソッドについては数学関数 §.積・商と剰余 (BigMul, DivRem)を参照してください。


これとは異なる方法で複数の値を返す手段として、Tuple型(タプル)を利用することもできます。 Tuple型は複数の値をひとまとめにするという点は配列と似ていますが、異なる型同士でもまとめることができる点で配列とは異なります。 これにより、配列では値を複数返す場合でも同じ型に限られるのに対し、Tuple型を用いれば型の異なる複数の値を返すことができます。

Tuple型を使って合計と平均を返すメソッド
using System;

class Sample {
  // 引数で与えられた配列の合計と平均を求めるメソッド
  // 合計はTuple.Item1, 平均はTuple.Item2に格納する
  static Tuple<long, double> SumAvr(int[] arr)
  {
    long sum = 0L;

    foreach (int val in arr) {
      sum += val;
    }

    double avr = sum / (double)arr.Length;

    // Item1にsum, Item2にavrの値を格納したTupleを作成して返す
    return Tuple.Create(sum, avr);
  }

  static void Main()
  {
    // この配列内の要素の合計と平均を求めたい
    int[] arr = new int[] {2, 5, 3, 8, 4};

    Tuple<long, double> ret = SumAvr(arr);

    Console.WriteLine("合計 = {0}", ret.Item1);
    Console.WriteLine("平均 = {0}", ret.Item2);
  }
}

このように、配列と違ってTuple型では型の異なる値をひとまとめにできる利点がある一方、Tuple型では格納されている値を参照するのにItem1, Item2といったメンバ名で参照する必要があるため、配列同様に格納されている値の意味があいまいになるという欠点もあります。


なお、複数のスカラー値をタプルや配列を返したり、タプルや配列で返される戻り値を変数(スカラー値)に展開するための以下のような構文は、C# 6.0、VB14(Visual Basic 2015)時点では用意されていません。 (このような機能の将来的な導入については検討がされています)

複数の戻り値をスカラー値に展開する構文(擬似コード、実際にはコンパイルできない)
using System;

class Sample {
  // 引数で与えられた配列の合計と平均を同時に返すメソッド
  static (long, double) SumAvr(int[] arr)
  {
    long sum = 0L;

    foreach (int val in arr) {
      sum += val;
    }

    double avr = sum / (double)arr.Length;

    // 複数の値をひとまとめにして返したい
    return (sum, avr);
  }

  static void Main()
  {
    // 呼び出し側で戻り値を展開したい
    (long sum, double avr) = SumAvr(new int[] {2, 5, 3, 8, 4});
  }
}

比較用の参考として、JavaScriptなどいくつかのスクリプト言語では以下のようなコードを記述することができます。

function SumAvr(arr) {
  var sum = 0;

  for (var i = 0; i < arr.length; i++) {
    sum += arr[i];
  }

  var avr = sum / arr.length;

  return [sum, avr];
}

var sum, avr;

[sum, avr] = SumAvr([2, 5, 3, 8, 4]);