C#の「配列」は、同じデータ型の値をまとめて扱うための機能です。配列を使うと大量の値を一元管理できるため、ソースコードが簡潔になります。さらに「ループ(繰り返し)」や「ソート(並び替え)」など、高度な処理が行いやすくなることもメリットです。
一方で、配列は通常のデータ型とは異なり、配列変数の宣言方法や使い方などわかりづらい部分もあります。そこで今回は、C#の配列の使い方や便利なメソッド、応用テクニックなどを解説します。わかりやすいサンプルコードも紹介するので、ぜひご参考ください。
目次
C#の「配列」とは、同じデータ型の値をまとめて管理するための機能です。関連する数値や文字列を、一元管理したいシーンはよくあります。たとえば、5つの整数値に対して同じ演算を行いたいとき、配列を使わない場合は5つの変数を個別に用意して、演算もひとつずつ行わないといけません。以下のサンプルコードで確認してみましょう。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 5つのint型変数を宣言する int a = 1; int b = 2; int c = 3; int d = 4; int e = 5; // それぞれの値を2乗した結果を求める int squareA = a * a; int squareB = b * b; int squareC = c * c; int squareD = d * d; int squareE = e * e; // 計算結果を画面に表示する System.Console.WriteLine(squareA); System.Console.WriteLine(squareB); System.Console.WriteLine(squareC); System.Console.WriteLine(squareD); System.Console.WriteLine(squareE); } } }
//実行結果
上記のサンプルプログラムでは、5つの整数値を格納する変数を用意して、それぞれ2乗した値を表示しています。しかし、演算や表示を個別に行う必要があるため、冗長なソースコードになってしまいました。一方で、C#の配列を活用すると以下のように、すっきりしたソースコードになります。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数5のint型配列を生成する int[] nums = new int[] { 1, 2, 3, 4, 5 }; // それぞれの値を二乗した結果を画面に表示する foreach (int i in nums) { System.Console.WriteLine(i * i); } } } }
このように、演算や表示をまとめて行えるので、ソースコードが簡潔化されて読みやすくなります。冗長なソースコードが原因となるバグやエラーが減り、効率が向上することがC#の配列の大きなメリットです。
上記のサンプルプログラムでは、「IndexOutOfRangeException」という例外が出ています。これは「配列の範囲外にアクセスしました」という危険なエラーなので、配列のインデックスの扱い方にはご注意ください。
前述した方法でC#の配列が使えるようになりますが、「あらかじめ値を設定しておきたい」こともあるでしょう。C#の配列は以下2つの方法で初期化できます。
C#の配列を初期化する最も基本的な方法は、配列変数の生成後にひとつずつ値を設定していく方法です。「データ型[] 配列変数名 = new データ型[要素数];」の構文で配列変数を生成したあとは、「配列変数[インデックス] = 初期値」と記載していくことで、配列変数に初期データを格納できます。詳しい手順を以下のサンプルコードで確認しましょう。
// 要素数5のint型配列を生成する int[] nums = new int[5]; // 配列の各要素に値を格納する nums[0] = 1; nums[1] = 2; nums[2] = 3; nums[3] = 4; nums[4] = 5;
上記のサンプルコードでは、要素数5のint型配列を生成したあとに、初期値をひとつずつ代入しています。この方法は、生成時ではなく生成後に値を格納しているため、厳密には初期化とはいえないかもしれません。
しかし後述する同時初期化では、プログラムが見づらくなることがあります。たとえば、初期化するデータ数が非常に多い場合など。この方法は、多くのデータを初期化する必要があるときや、forループやforeach文などで初期化したいときなどに便利です。
C#の配列変数は、生成と同時に初期化することもできます。以下のように、「データ型[] 配列変数名 = new データ型[要素数]」で生成した直後に、「{初期値};」を続けて記載するだけです。具体例は以下のとおりです。
// 要素数5のint型配列を生成・初期化する int[] nums = new int[5] { 1, 2, 3, 4, 5 };
上記の例では、要素数5のint型配列を生成するときに、各要素を1~5の値で初期化しています。この方法は、先ほどの生成後に初期値を代入する方法とは異なり、厳密な意味での初期化であるといえるでしょう。
ただし生成と初期化を同時に行う場合は、まとめて記載する必要があるため、場合によってはソースコードが横長になってわかりづらくなることがあるのでご注意を。また、この方法は以下のように、より簡潔に記載することもできます。
// 要素数5のint型配列を生成・初期化する int[] nums = new int[] { 1, 2, 3, 4, 5 };
こちらは要素数を省略したものです。生成と初期化を同時に行う場合に限り、配列生成時の要素数指定を省略できます。なぜなら、初期値を設定すると、要素数も自動的に決まるからです。要素数を指定する場合は、初期値の数と要素数が一致していないとコンパイルエラーが出るので、余計なエラーを出さないためにも要素数は省略するのがおすすめです。
// 要素数5のint型配列を生成・初期化する int[] nums = new[] { 1, 2, 3, 4, 5 };
こちらは、要素数に加えて型名も省略したものです。左側ですでに型名を指定しており、あえてもう記載する必要はないことが理由です。記述量が減るため、ミスの可能性もさらに少なくなります。
// 要素数5のint型配列を生成・初期化する int[] nums = { 1, 2, 3, 4, 5 };
こちらは「new演算子」さえも省略した書き方です。初期値を設定する時点で、配列変数を生成することは確実なので、new演算子が使えます。この書き方が一番短く、記載ミスをする可能性も低いため、基本的にはこの書き方を採用することをおすすめします。
C#の配列は、さまざまなメソッドを使うことで、より便利に活用することができます。ここからは、今すぐ使えるC#の便利テクニック・メソッドを6つご紹介します。
冒頭でも紹介したように、C#の配列の大きなメリットは、複数のデータを一元管理してまとめて処理できることです。配列の各要素に対して順番に何らかの処理を行いたいときは、「for文」もしくは「foreach文」を使用しましょう。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数5のint型配列を生成・初期化する int[] nums = new int[5] { 1, 2, 3, 4, 5 }; // 通常のforループで配列の全要素を表示する for (int i = 0; i < 5; i++) { // WriteLineメソッドでフォーマットを指定する System.Console.WriteLine("{0}番目:{1}", i + 1, nums[i]); } // foreach文で配列の全要素を表示する foreach (int i in nums) { System.Console.WriteLine(i); } } } }
for文とforeach文には、それぞれメリット・デメリットがあります。まず、for文は「ループカウンタ」と「条件式」を指定する必要があります。ループカウンタと条件式は、forループを何回繰り返すか制御・判定するためのものです。
条件式には、「i < 配列の要素数」の形式で、配列変数の要素数(サイズ)を指定しないといけません。この要素数が不正確な値だと、ループを最後まで回しきれなかったり、範囲外アクセスで例外が発生してしまったりします。しかし、後述する「Lengthプロパティ」を使えば、正確な要素数を取得できるので、とくに大きな問題ではないでしょう。
for文のメリットは、ループ内で「現在のインデックス」がわかることです。たとえば以下のサンプルプログラムのように、何らかの形でインデックスを使いたいときは、for文を使うのが便利です。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数5のint型配列を生成・初期化する int[] nums = new int[5] { 1, 2, 3, 4, 5 }; // 通常のforループで配列の全要素を表示する for (int i = 0; i < 5; i++) { // WriteLineメソッドでフォーマットを指定する System.Console.WriteLine(nums[i]); // もし3番目の値なら「当たり!」を表示する // それ以外なら「ハズレ!」を表示する if (i == 3) { System.Console.WriteLine("当たり!"); } else { System.Console.WriteLine("ハズレ!"); } } } } }
//実行結果
foreach文のメリットは、ループカウンタや条件式を指定する必要がなく、簡潔に記載できることです。「foreach (型名 変数 in 配列変数)」と記載するだけで、最初から最後までの要素へ順番にアクセスできます。
ただし、foreach文の場合は「現在のインデックス」がわかりません。そのため、ループ内でインデックスを使いたい場合は、以下のようにインデックス用の変数を別途用意して加算していく必要があります。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数5のint型配列を生成・初期化する int[] nums = new int[5] { 1, 2, 3, 4, 5 }; // インデックス用の変数を用意する int index = 0; // foreach文で配列の全要素を表示する foreach (int i in nums) { // WriteLineメソッドでフォーマットを指定する System.Console.WriteLine(i); // もし3番目の値なら「当たり!」を表示する // それ以外なら「ハズレ!」を表示する if (index++ == 3) { System.Console.WriteLine("当たり!"); } else { System.Console.WriteLine("ハズレ!"); } } } } }
//実行結果
以上のように、for文とforeach文は一長一短なので、どちらがより効率的なソースコードになるか考えて、メリットが大きいほうを選ぶようにしましょう。
通常のforループを使う場合は、繰り返し処理を行う回数を指定する必要があります。そのため、たとえば配列のサイズを書き換える場合は、forループの条件式も変えないといけません。そこで配列の「Lengthプロパティ」を使うと、以下のように柔軟なプログラムが書けます。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数5のint型配列を生成・初期化する int[] nums = new int[5] { 1, 2, 3, 4, 5 }; // 通常のforループで配列の全要素を表示する for (int i = 0; i < nums.Length; i++) { System.Console.WriteLine("{0}番目:{1}", i + 1, nums[i]); } } } }
//実行結果
上記のサンプルコードでは、forループの条件式が「i < nums.Length」となっています。自分で要素数を指定する必要がなくなるためミスが減るうえに、仕様変更で配列のサイズを変更したときも自動的に反映されます。Lengthプロパティは、配列のコピーや結合などさまざまな処理を行うときに使うので、ぜひ覚えておきましょう。
C#の配列は、インスタンス生成後でも簡単に拡張して、新たな要素を追加できます。そのためには、以下の構文で「Array.Resizeメソッド」を使う必要があります。
Array.Resize(ref 配列変数, 新しいサイズ);
ここで注意が必要なのが、第1引数に「refキーワード」が付いていることです。これは、メソッドの引数を「参照渡し」で扱うときに必要になります。Array.Resizeメソッドでは、実は内部で新たな配列変数を生成して、第1引数の配列変数を書き換えています。しかし、値渡しではその変更が呼び出し元で反映されないため、参照渡しが必要になります。Array.Resizeメソッドの使い方を、以下のサンプルコードで確認しましょう。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数5のint型配列を生成・初期化する int[] nums = new int[5] { 1, 2, 3, 4, 5 }; // 通常のforループで配列の全要素を表示する for (int i = 0; i < nums.Length; i++) { System.Console.WriteLine("{0}番目:{1}", i + 1, nums[i]); } // 改行する System.Console.WriteLine(); // 配列のサイズを10に拡張する // 第1引数には配列変数の参照を渡す Array.Resize(ref nums, 10); // 通常のforループで配列の全要素を表示する for (int i = 0; i < nums.Length; i++) { System.Console.WriteLine("{0}番目:{1}", i + 1, nums[i]); } } } }
//実行結果
以上のサンプルプログラムのように、Array.Resizeメソッドを使うと配列のサイズを変更できます。なお、拡張した要素には初期値として「0」もしくはそれに相当する値が格納されるので、必要に応じて新しい値に変更しましょう。
また、Array.Resizeメソッドで配列サイズを変更する前に、「=」演算子で配列変数を「シャローコピー」していた場合は、コピー先の配列変数はそのまま維持されます。Array.Resizeメソッドでサイズ変更した配列変数は、前述したように新しいものに変わっているため、シャローコピー先の配列変数とは「別物」として扱われることも念頭においておきましょう。
配列を別の配列変数にコピーする方法は2つあります。単に「=」演算子で代入する「シャローコピー」と、「Array.Copyメソッド」を使う「ディープコピー」です。ただし、両者には大きな違いがあるので要注意。
配列は「参照型変数」なので、配列変数の中身にはデータそのものではなく、データが保管されている場所への「参照値」が入っています。シャローコピーは参照値(アドレス)がコピーされるだけなので、どちらか一方を変更すると他方も影響を受けてしまいます。
これを防いで「別物の配列変数」として扱うためには、「Array.Copyメソッド」でディープコピーを行う必要があります。Array.Copyメソッドの構文は以下のとおり。
Array.Copy(コピー元配列, コピー先配列, コピーする配列の要素数);
なおコピー先配列は、あらかじめnew演算子でインスタンス化しておく必要があります。インスタンスを生成しておかないと、コンパイルエラーとなります。Array.Copyメソッドの使い方や、シャローコピーとディープコピーの違いについて、次のサンプルコードで詳しく検証していきましょう。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数5のint型配列を生成・初期化する int[] nums = new int[5] { 1, 2, 3, 4, 5 }; // 配列を「=」演算子でシャローコピーする int[] shallowCopy = nums; // ディープコピー用のint型配列を生成する。 int[] deepCopy = new int[nums.Length]; // 配列を「Array.Copyメソッド」でディープコピーする Array.Copy(nums, deepCopy, nums.Length); // 元の配列を変更する for (int i = 0; i < nums.Length; i++) { nums[i] *= -1; } // 元配列の全要素を表示する System.Console.WriteLine("nums(元配列)"); foreach (int i in nums) { System.Console.WriteLine(i); } // シャローコピーした配列の全要素を表示する System.Console.WriteLine("shallowCopy(シャローコピーした配列)"); foreach (int i in shallowCopy) { System.Console.WriteLine(i); } // ディープコピーした配列の全要素を表示する System.Console.WriteLine("deepCopy(ディープコピーした配列)"); foreach (int i in deepCopy) { System.Console.WriteLine(i); } } } }
//実行結果
上記のサンプルプログラムのように、元の配列を変更すると、シャローコピーした変数も変更されてしまいます。一方でディープコピーした配列は、元の配列が変更されても影響を受けません。
シャローコピーでは、コピー先・コピー元が「参照値(アドレス)」を経由して、お互いに繋がりがある状態になります。一方、ディープコピーの場合は配列変数同士がまったくの別物になるため、お互いに影響を及ぼさないのです。
配列のデータそのものをコピーしたい場合は、必ず「Array.Copyメソッド」でディープコピーするようにしましょう。
C#で配列を使用していると、複数の配列同士を結合させたいときがあります。そんなときは、先ほどご紹介した「Array.Copy」を活用すれば、配列同士を結合させることができます。ただし、以下のように使い方が少し複雑なので注意が必要です。
// 2つの配列を合わせたサイズの配列変数を生成する データ型[] 結合先の配列変数 = new データ型[1つ目の配列サイズ + 2つ目の配列サイズ]; // 1つ目の配列をコピーする Array.Copy(1つ目の配列変数, 結合先の配列変数, 1つ目の配列サイズ); // 直後の要素へ2つ目の配列をコピーする Array.Copy(2つ目の配列変数, 0, 結合先の配列変数, 1つ目の配列サイズ, 2つ目の配列サイズ);
配列同士を結合させるためには、結合後の大きな配列を格納できる変数が必要なので、まず2つの配列のトータルサイズの配列変数を生成します。そのうえで、1つ目の配列を結合先にコピーします。ただし2つ目の配列は、1つ目の配列をコピーした直後のインデックスに結合させたいため、コピー場所を指定しないといけません。
そのために、引数が多いバージョンの「Array.Copyメソッド」を使います。第2引数は「0」、第4引数は1つ目の配列サイズ、第5引数は2つ目の配列サイズを指定すればOK。これは配列のデータ型やサイズが変わっても同じなので、「定型文」として覚えておくことをおすすめします。下記のサンプルプログラムで詳細を確認しましょう。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数5のint型配列を生成・初期化する int[] numsA = new int[5] { 1, 2, 3, 4, 5 }; // 要素数5のint型配列を生成・初期化する int[] numsB = new int[5] { 6, 7, 8, 9, 10 }; // 2つの配列を合わせたサイズの配列変数を生成する int[] concatenate = new int[numsA.Length + numsB.Length]; // 1つ目の配列をコピーする Array.Copy(numsA, concatenate, numsA.Length); // 直後の要素へ2つ目の配列をコピーする // 第2引数は「0」、第4引数は1つ目の配列サイズ、第5引数は2つ目の配列サイズを指定する Array.Copy(numsB, 0, concatenate, numsA.Length, numsB.Length); // 結合した配列の全要素を表示する foreach(int i in concatenate) { Console.WriteLine(i); } } } }
//実行結果
以上のサンプルプログラムのように、「Array.Copyメソッド」を2回使用することで、2つの配列を結合できます。ただし、Array.Copyメソッドの引数設定を誤ると、「ArgumentException」例外が出るので要注意。
なお、3つの配列を結合させたい場合は、以下のように3回目のArray.Copyメソッド呼び出しで、第4引数に1つ目と2つ目の配列サイズの合計値を指定すればOKです。さらに多くの配列を結合させる場合も、同様に第4引数を変更しましょう。
/ 3つの配列を合わせたサイズの配列変数を生成する int[] concatenate = new int[numsA.Length + numsB.Length + numsC.Length]; // 1つ目の配列をコピーする Array.Copy(numsA, concatenate, numsA.Length); // 直後の要素へ2つ目の配列をコピーする Array.Copy(numsB, 0, concatenate, numsA.Length, numsB.Length); // 直後の要素へ3つ目の配列をコピーする Array.Copy(numsC, 0, concatenate, numsA.Length + numsB.Length, numsC.Length);
引数の指定方法が少しわかりづらいかもしれませんが、何度もサンプルコードを確認して、少しずつ慣れていきましょう。
配列の一部要素をトリミングする方法は2つあります。LINQで「Skipメソッド」と「Takeメソッド」を使う方法と、「範囲アクセス」の機能を使う方法です。LINQのメソッドは以下の構文で使用します。
型名[] コピー先配列 = コピー元配列.Skip(コピー開始インデックス).Take(コピーする要素数).ToArray();
「Skipメソッド」にはトリミングを開始するインデックスを、「Takeメソッド」にはトリミングする要素数を指定します。以下のサンプルコードで詳細を確認しましょう。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数10のint型配列を生成・初期化する int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // トリミング用のint型配列を生成する // 「Skipメソッド」にはトリミングを開始するインデックス // 「Takeメソッド」にはトリミングする要素数を指定する // 今回は2番目の要素「3」から「8」まで6つの要素をトリミングする int[] extract = nums.Skip(2).Take(6).ToArray(); // トリミングした配列を表示する foreach (var x in extract) { System.Console.WriteLine(x); } } } }
//実行結果
上記のサンプルコードでは、要素数10のint型配列を生成し、SkipメソッドとTakeメソッドを活用し、2番目から数えて6つの要素をトリミングしました。一方で、範囲アクセスは以下の構文で使用します。
トリミング先配列 = 配列変数[トリミング開始インデックス..トリミング終了インデックス+1];
「.」は2つ並べて書く必要があり、1つや3つではエラーになります。トリミング終了インデックスの指定方法が分かりづらいですが、「トリミング終了インデックス+1」の値を指定しないといけません。たとえば、[1..4」とすると、1番目から3番目までの要素がトリミングされます。forループの条件式と同じだと考えるとわかりやすいです。範囲アクセス機能の使い方を、以下のサンプルコードで確認しましょう。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数10のint型配列を生成・初期化する int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; // トリミング用のint型配列を生成する // 「トリミング先配列 = 配列変数[トリミング開始インデックス..トリミング終了インデックス+1];」の構文となる // たとえばa[i..j]と書けば、配列aの「i番目からj-1番目の要素を取り出す」という意味になる // 「iは含んでjは含まない」範囲になるため、for (var x = i; x < j; ++x)のイメージがわかりやすい // 今回は「2番目」の要素3から「8-1番目」の要素8までをトリミングする int[] extract = nums[2..8]; // トリミングした配列を表示する foreach (var x in extract) { System.Console.WriteLine(x); } } } } //
実行結果
上記のサンプルコードでは、「int[] extract = nums[2..8]」と記載して、2番目から7番目までの要素をトリミングしました。基本的には、こちらの範囲アクセスのほうが簡潔に記載できますが、インデックスの指定方法に注意してください。
C#の配列を使用していると、すべての要素を任意の順番で並び替えたいことがあります。この作業を「ソート」と呼びます。たとえば、さまざまな整数値を配列に格納したあとで、昇順(小さい順)や降順(大きい順)で並び替えたいなどです。そのような場合は、以下4つの方法で簡単にソートできます。
「Array.Sortメソッド」は、最も基本的な配列のソート方法です。構文はいたって単純で、「Array.Sort(配列変数)」と記載するだけ。Array.Sortメソッドの使い方を、以下のサンプルコードで確認しましょう。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数5のint型配列を生成・初期化する int[] nums = new int[5] { 3, 1, 5, 4, 2 }; // ソート前の配列を表示する System.Console.WriteLine("[{0}]", string.Join(", ", nums)); // 「Sortメソッド」で配列を昇順(小さい順)でソートする Array.Sort(nums); // ソート後の配列を表示する System.Console.WriteLine("[{0}]", string.Join(", ", nums)); } } }
//実行結果
上記のサンプルコードでは、5つの値を格納したint型配列を、昇順でソートしています。なおArray.Sortメソッドは「破壊的メソッド」なので、引数として渡した配列変数の内容は変更されます。元の配列も保持したい場合は、あらかじめ別の配列変数にディープコピーしてから、Array.Sortメソッドを呼び出しましょう。
なお、Array.Sortメソッドのデフォルト動作は昇順ソートとなっており、そのままでは降順ソードはできません。しかし以下のサンプルコードのように、昇順ソートを行ってから「Array.Reverseメソッド」で逆順に並び替えると、結果的に降順ソートと同じ結果が得られます。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数5のint型配列を生成・初期化する int[] nums = new int[5] { 3, 1, 5, 4, 2 }; // ソート前の配列を表示する System.Console.WriteLine("[{0}]", string.Join(", ", nums)); // 「Sortメソッド」で配列を昇順(小さい順)でソートする Array.Sort(nums); // そのあと逆順に並べ替えることで、降順(大きい順)でソートできる Array.Reverse(nums); // ソート後の配列を表示する System.Console.WriteLine("[{0}]", string.Join(", ", nums)); } } }
//実行結果
上記のサンプルコードでは、まずArray.Sortメソッドで昇順に並び替えてから、Array.Reverseメソッドで降順に並び替えています。あまり効率的な方法ではありませんが、単純でわかりやすいのでおすすめです。なお、Array.Sortメソッドで最初から降順に並び替えたいときは、後述する「ラムダ式」を使う必要があります。
「LINQ」を活用すれば、配列の昇順・降順ソートが簡単にできます。なお、LINQは先ほども紹介しましたが、これは「統合言語クエリ」と呼ばれる機能です。LINQには、配列やListなどの「コレクション」を処理するメソッドを集めたライブラリで、非常に便利な機能が揃っています。LINQで昇順ソートする方法を、以下のサンプルコードで確認しましょう。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数5のint型配列を生成・初期化する int[] nums = new int[5] { 3, 1, 5, 4, 2 }; // ソート前の配列を表示する System.Console.WriteLine("[{0}]", string.Join(", ", nums)); // LINQの「OrderByメソッド」で、配列を昇順(小さい順)でソートする nums = nums.OrderBy(x => x).ToArray(); // ソート後の配列を表示する System.Console.WriteLine("[{0}]", string.Join(", ", nums)); } } }
//実行結果
注目ポイントは、「nums = nums.OrderBy(x => x).ToArray();」という構文で、配列をソートしていることです。OrderByメソッドは、ずばり配列をソートするためのLINQですが、引数に「x => x」を指定しています。これは「メソッド構文」と呼ばれるもので、いわば「定型文」のようなものだと覚えておくといいでしょう。
LINQを使えば、わずかこの1行だけで配列をソートできるので便利です。なお降順ソートにしたい場合は、以下のサンプルコードのように、メソッド名を少し書き換えるだけでOKです。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数5のint型配列を生成・初期化する int[] nums = new int[5] { 3, 1, 5, 4, 2 }; // ソート前の配列を表示する System.Console.WriteLine("[{0}]", string.Join(", ", nums)); // LINQの「OrderByDescendingメソッド」で、配列を降順(大きい順)でソートする nums = nums.OrderByDescending(x => x).ToArray(); // ソート後の配列を表示する System.Console.WriteLine("[{0}]", string.Join(", ", nums)); } } }
//実行結果
降順ソートでは、「OrderByDescendingメソッド」を使います。こちらもメソッド構文が必要ですが、先ほど同様に「x => x」でOK。LINQは見慣れない構文を使用しますが、非常に便利な機能が揃っているので、少しずつ覚えていきましょう。
配列のままソートするのではなく、一度「リスト」に変換してからソートすることもできます。配列からリストへの変換は、以下のようにリストのコンストラクタに配列変数を渡すだけで完了します。
List<型名> リスト変数 = new List<型名>(配列変数);
配列からリストに変換すると、要素の追加や削除など、配列ではひと手間かかることが簡単にできるようになるので便利です。配列をリストに変換したあとは、以下のサンプルコードのように、「Sortメソッド」を呼び出すだけで昇順ソートができます。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数5のint型配列を生成・初期化する int[] nums = new int[5] { 3, 1, 5, 4, 2 }; // 配列をリストに変換する List list = new List(nums); // ソート前のリストを表示する System.Console.WriteLine("[{0}]", string.Join(", ", list)); // リストの「Sortメソッド」で、リストを昇順(小さい順)でソートする list.Sort(); // ソート後のリストを表示する System.Console.WriteLine("[{0}]", string.Join(", ", list)); } } }
//実行結果
上記のサンプルコードでは、5つの値を格納したint型配列をリストに変換し、Sortメソッドで並び替えています。配列と同じように、降順ソートにしたい場合は、Sortメソッドのあとに「Reverseメソッド」を呼び出せば完了です。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数5のint型配列を生成・初期化する int[] nums = new int[5] { 3, 1, 5, 4, 2 }; // 配列をリストに変換する List list = new List(nums); // ソート前のリストを表示する System.Console.WriteLine("[{0}]", string.Join(", ", list)); // リストの「Sortメソッド」で、リストを昇順(小さい順)でソートする list.Sort(); // そのあと逆順に並べ替えることで、降順(大きい順)でソートできる list.Reverse(); // ソート後のリストを表示する System.Console.WriteLine("[{0}]", string.Join(", ", list)); } } }
//実行結果
配列の「Array.Sortメソッド」は、昇順ソートしかできないことが難点でした。しかし、Array.Sortメソッドの引数に「ラムダ式」を渡せば、自由に条件を指定してソートを行うことができます。ラムダ式とは、簡単に言うとメソッドに「処理内容」を渡すための機能。以下のように記述すると、Array.Sortメソッドで昇順ソートができます。
Array.Sort(配列変数, (x, y) => x.CompareTo(y));
第2引数の「(x, y) => x.CompareTo(y)」は、ソートの方法を示すラムダ式です。「x」と「y」の2つの変数を宣言し、「CompareToメソッド」でxとyの値を比較した結果を返します。ラムダ式に慣れていないとわかりづらい部分ですが、ここは「昇順ソートの定型文」として覚えておくといいでしょう。ラムダ式で昇順ソートするサンプルコードをご紹介します。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数5のint型配列を生成・初期化する int[] nums = new int[5] { 3, 1, 5, 4, 2 }; // ソート前の配列を表示する System.Console.WriteLine("[{0}]", string.Join(", ", nums)); // 「ラムダ式」を使って、配列を昇順(小さい順)でソートする Array.Sort(nums, (x, y) => x.CompareTo(y)); // ソート後の配列を表示する System.Console.WriteLine("[{0}]", string.Join(", ", nums)); } } }
//実行結果
処理内容や結果は、これまでのサンプルコードと同じです。降順ソートにしたい場合は、以下のように、ラムダ式の「CompareToメソッド」の戻り値の符号を反転させるだけです。
Array.Sort(配列変数, (x, y) => -x.CompareTo(y));
Sortメソッド内部では、CompareToメソッドの戻り値を参照しているため、その符号を反転させると、ソート条件も反対になります。ラムダ式で降順ソートするサンプルコードを確認しましょう。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数5のint型配列を生成・初期化する int[] nums = new int[5] { 3, 1, 5, 4, 2 }; // ソート前の配列を表示する System.Console.WriteLine("[{0}]", string.Join(", ", nums)); // 「ラムダ式」を使って、配列を降順(大きい順)でソートする Array.Sort(nums, (x, y) => -x.CompareTo(y)); // ソート後の配列を表示する System.Console.WriteLine("[{0}]", string.Join(", ", nums)); } } }
//実行結果
C#の配列を使用する際は、「特定の要素を検索したい」ときもあるでしょう。そんなときは以下3つの方法で、目的の要素を簡単に検索できます。
「IndexOfメソッド」は、引数で指定した検索値に該当する、最初の要素のインデックスを返します。IndexOfメソッドの構文は以下のとおりです。
int index = Array.IndexOf(配列変数, 検索値);
IndexOfメソッドは、第1引数に配列変数、第2引数に検索したい値を渡します。戻り値はint型のインデックス値で、該当する要素が見つかったときはそのインデックスを、検出できなかった場合は「-1以下の値」を返します。
つまり、該当する要素が配列内に含まれているか調べたいときは、if文で戻り値が-1より大きいかチェックすれば良いということ。IndexOfメソッドを使ったサンプルコードを確認しましょう。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数10のint型配列を生成・初期化する int[] nums = new int[10] { 1, 2, 3, 4, 5, 5, 4, 3, 2, 1 }; // 検索値の入力を受け取り、「IndexOfメソッド」で最初の要素を検索する int value = Input(); int index = Array.IndexOf(nums, value); // 「IndexOfメソッド」の戻り値が「-1」より大きければ、要素が配列内に存在する if (index > -1) { System.Console.WriteLine("「{0}」は「{1}番目」の要素です", value, index); } else { System.Console.WriteLine("「{0}」は見つかりませんでした", value); } } static int Input() { int i; // 不正値が入力された場合は、正しい整数値が入力されるまで繰り返す do { System.Console.Write("検索する整数値を入力してください:"); } while (!int.TryParse(System.Console.ReadLine(), out i)); return i; } } }
//実行結果
上記のサンプルコードでは、ユーザーがキーボードから入力した整数値が、配列に含まれているか「IndexOfメソッド」で調べています。ただし、IndexOfメソッドは最初の要素のインデックスを返すため、重複する要素がある場合は注意が必要です。
「LastIndexOfメソッド」は、引数で指定した検索値に該当する、最後の要素のインデックスを返します。IndexLastOfメソッドの構文や戻り値の判定方法は、先ほどのIndexOfメソッドと同じです。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 要素数10のint型配列を生成・初期化する int[] nums = new int[10] { 1, 2, 3, 4, 5, 5, 4, 3, 2, 1 }; // 検索値の入力を受け取り、「LastIndexOfメソッド」で最後の要素を検索する int value = Input(); int index = Array.LastIndexOf(nums, value); // 「IndexOfメソッド」の戻り値が「-1」より大きければ、要素が配列内に存在する if (index > -1) { System.Console.WriteLine("「{0}」は「{1}番目」の要素です", value, index); } else { System.Console.WriteLine("「{0}」は見つかりませんでした", value); } } static int Input() { int i; // 不正値が入力された場合は、正しい整数値が入力されるまで繰り返す do { System.Console.Write("検索する整数値を入力してください:"); } while (!int.TryParse(System.Console.ReadLine(), out i)); return i; } } }
//実行結果
配列内で重複する要素を指定した場合は、最後の要素のインデックスが返ります。そのため、配列内に重複した要素があることが予想される場合は、目的に応じてIndexOfメソッドとIndexLastOfメソッドを使い分けるようにしましょう。
「FindIndexメソッド」は、ラムダ式で自由に検索条件を指定して、それに該当する要素のインデックスを返します。FindIndexメソッドの構文は以下のとおりです。
int index = Array.FindIndex(配列変数, 検索条件 == 検索値);
戻り値の扱い方はこれまでと同じで、検出されたときは要素のインデックスを、検出できなかった場合は「-1以下の値」を返します。第2引数はラムダ式で、要素を検出するための処理を指定します。
たとえば、自作クラスの「score値」を検索キーにする場合は、「int index = Array.FindIndex(nums, x => x.score == value);」と記載すればOKです。FindIndexメソッドで、ラムダ式を使って値を検索するサンプルコードをご紹介します。
//サンプルプログラム
namespace Test { // テスト用のデータ格納クラス class Data { public int ID { get; set; } public int score { get; set; } public string name { get; set; } public Data(int ID, int score, string name) { this.ID = ID; this.score = score; this.name = name; } } class Program { static void Main(string[] args) { // 要素数5のData型配列を生成・初期化する Data[] nums = { new Data(1, 90, "A"), new Data(2, 80, "C"), new Data(3, 70, "E"), new Data(4, 60, "D"), new Data(5, 50, "B") }; // 検索値の入力を受け取り、「FindIndexメソッド」で検索条件を指定する // 今回は「score」の値で検索するように設定 int value = Input(); int index = Array.FindIndex(nums, x => x.score == value); // 「IndexOfメソッド」の戻り値が「-1」より大きければ、要素が配列内に存在する if (index > -1) { System.Console.WriteLine("「{0}」は「{1}番目」の要素です", value, index); } else { System.Console.WriteLine("「{0}」は見つかりませんでした", value); } } static int Input() { int i; // 不正値が入力された場合は、正しい整数値が入力されるまで繰り返す do { System.Console.Write("検索する整数値を入力してください:"); } while (!int.TryParse(System.Console.ReadLine(), out i)); return i; } } }
//実行結果
少し複雑なプログラムになりましたが、重要なポイントはFindIndexメソッドで「複雑な条件での検索」もできることです。第2引数は「int index = Array.FindIndex(nums, x => x.score == value);」となっていますが、これは「score」を検索キーとして、一致するものを検出するということです。
IndexOfメソッドやIndexLastOfメソッドでは、単純な値の比較だけを行うため、自作クラス型の配列には対応できません。しかし、FindIndexメソッドはラムダ式で処理内容を自由に記述できるため、自作クラスの特定のフィールドを検索キーにすることが可能です。たとえば、「x.score」を「x.ID」に置き換えれば、IDの値で要素を検索できます。
これまでは1次元配列について解説してきましたが、C言語では2次元以上の「多次元配列」も扱えます。多次元配列とは、直線的ではなく平面的、あるいは立体的に広がっている配列です。C#の多次元配列には、「四角形配列」と「ジャグ配列」の2種類があり、それぞれ宣言や初期化の方法・使い方が大きく異なります。
C#の「四角形配列」は、各次元で要素数が固定された多次元配列です。C#で「多次元配列」というと、こちらの四角形配列を指す場合が多いです。2次元配列を宣言・生成する構文は以下のとおりです。
データ型[,] 配列変数名 = new データ型[Y次元の要素数, X次元の要素数];
カッコの書式が1次元配列とは異なります。四角形配列では、縦方向の「Y次元」と横方向の「X次元」それぞれの要素数を指定する必要があることを覚えておきましょう。
たとえば、縦方向に5・横方向に2の要素数があるint型2次元配列の場合は、「int[,] 配列変数 = new[5,2];」と記述します。初期値を設定する場合は以下のように、横方向の要素ごとにカッコでまとめて、コンマで区切って記述します。
int[,] _2DArray = new[,] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 }, { 9, 10 } };
慣れるまでわかりにくいかもしれませんが、横方向の次元がこれまでの1次元配列と同じで、それを縦方向に複数並べたのが2次元配列だとイメージするといいかもしれません。2次元の四角形配列を活用したサンプルコードを確認しましょう。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 「縦方向5 × 横方向2」のint型2次元配列を生成・初期化する int[,] _2DArray = new[,] { { 1, 2 }, { 3, 4 }, { 5, 6 }, { 7, 8 }, { 9, 10 } }; // 2次元配列を表示する // まずは縦方向の次元からループを回す // 縦次元の要素数は「GetLength(0)」で取得する for (int y = 0; y < _2DArray.GetLength(0); y++) { System.Console.Write("["); // 次に横方向の次元でループを回す // 横次元の要素数は「GetLength(1)」で取得する for (int x = 0; x < _2DArray.GetLength(1); x++) { System.Console.Write("{0}", _2DArray[y, x]); System.Console.Write((x != _2DArray.GetLength(1) - 1) ? ", " : "],\n"); } } } } }
//実行結果
注意点は「forループの使い方」です。実は四角形配列でforeach文を使うと、全要素が直線的な1次元配列として扱われるため、多次元配列を表現できません。そのため、縦方向と横方向にそれぞれforループを回して、次元を再現する必要があります。なお各次元の要素数は、縦方向がGetLength(0)・横方向がGetLength(1)で取得できます。
3次元配列の場合は、さらに「奥行き方法(Z次元)」の要素数が必要です。以下のように、カッコのなかにそれぞれの次元の要素数を指定して、配列変数を宣言・生成しましょう。
データ型[,,] 配列変数名 = new データ型[Z次元の要素数, Y次元の要素数, X次元の要素数];
生成と同時に初期値を設定する場合は、2次元配列と同じように次元ごとのまとまりを作ってコンマで区切ります。3次元の場合は、2次元配列をさらに複数並べたものが、3次元配列だとイメージするとわかりやすいでしょう。3次元の四角形配列を活用したサンプルコードをご紹介します。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // 「Z方向5 × Y方向2 × X方向3」のint型3次元配列を生成・初期化する int[,,] _3DArray = new[,,] { { { 1, 2, 3}, { 4, 5, 6 } }, { { 7, 8, 9}, { 10, 11, 12 } }, { { 13, 14, 15}, { 16, 17, 18 } }, { { 19, 20, 21}, { 22, 23, 24 } }, { { 25, 26, 27}, { 28, 29, 30 } }, }; // 3次元配列を表示する // まずはZ方向の次元からループを回す // Z次元の要素数は「GetLength(0)」で取得する for (int z = 0; z < _3DArray.GetLength(0); z++) { System.Console.Write("[ "); // 次にY方向の次元でループを回す // Y次元の要素数は「GetLength(1)」で取得する for (int y = 0; y < _3DArray.GetLength(1); y++) { System.Console.Write("( "); // 最後にX方向の次元でループを回す // X次元の要素数は「GetLength(2)」で取得する for (int x = 0; x < _3DArray.GetLength(2); x++) { System.Console.Write("{0,2}", _3DArray[z, y, x]); System.Console.Write((x != _3DArray.GetLength(2) - 1) ? ", " : " )"); } System.Console.Write((y != _3DArray.GetLength(1) - 1) ? ", " : " ],\n"); } } } } }
//実行結果
注意点は先ほどと同じく、forループの回し方です。やはりforeach文では1次元配列として列挙されるため、自分でforループを回しつつ必要に応じて体裁を整える必要があります。なお各次元の要素数は、Z方向がGetLength(0)・Y方向がGetLength(1)・Z方向がGetLength(2)で取得できます。引数を間違えると、不正アクセスで例外が出るので要注意です。
C#のもうひとつの多次元配列が「ジャグ配列」です。ジャグ配列は「配列の配列」とも呼ばれ、配列の内部に配列があるというイメージです。実はC++やJavaの多次元配列は、このジャグ配列で実装されるため、先ほどの四角形配列とは異なります。C#のジャグ配列の構文は以下のとおりです。
データ型[][] 配列変数名 = new データ型[縦方向の要素数][];
こちらは四角形配列とは異なり、それぞれの要素数ごとにカッコで囲っていることがポイントです。C++やJavaを学習したことがある人にとっては、ジャグ配列の方がわかりやすいかもしれません。ただし、生成時は「最上位次元」の要素数しか指定してはいけません。
これは、ジャグ配列は「配列の配列」であり、次元ごとに別オブジェクト扱いとなることが理由です。そのため、多次元ジャグ配列変数の生成時は、最上位次元の要素数のみ指定し、あとから各次元の配列を追加していきます。だからこそ、各次元ごとの要素数がバラバラの配列が作れます。ジャグ配列を活用したサンプルコードをご紹介します。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // int型2次元ジャグ配列を生成・初期化する // 四角形配列とは異なり、横方向の要素数がバラバラでもOK int[][] _2DArray = new int[][] { new int[] { 1, 2 }, new int[] { 3, 4, 5 }, new int[] { 6, 7 }, new int[] { 8, 9, 10, 11 }, new int[] { 12, 13, 14 }, }; // 2次元配列を表示する // まずは縦方向の次元からforeachループを回す foreach (int[] y in _2DArray) { // 次に横方向の次元でforeachループを回す foreach (int x in y) { System.Console.Write("{0,3}", x); } System.Console.WriteLine(); } } } }
//実行結果
重要なポイントは、配列を初期化している部分です。先ほどの四角形配列では、すべての要素がひとつのオブジェクト扱いでした。しかし、ジャグ配列は「配列の中に配列が入っている」ため、配列の初期化時は個別に配列を生成していく必要があります。
また、ジャグ配列はforeach文が便利に使えます。それぞれが個別の配列なので、多次元配列としての体裁を保ちながら、すべての要素に簡単にアクセスできます。
ちなみに3次元配列では、次元数がひとつ増えるのでカッコの数も増えます。ただし、2次元ジャグ配列と同じように、こちらも生成時は最上位次元の要素数しか指定できません。なお生成時に初期化する場合は、最上位次元の要素数を省略できます。
データ型[][][] 配列変数名 = new データ型[Z次元の要素数][][];
2次元配列と同じように、それぞれが別の配列という扱いなので、要素数がバラバラでも構いません。3次元ジャグ配列のサンプルコードを確認しましょう。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // int型3次元ジャグ配列を生成・初期化する // 四角形配列とは異なり、Y方向・X方向の要素数がバラバラでもOK int[][][] _3DArray = new int[][][] { new int[][] { new int[] { 1, 2 }, new int[] { 3, 4, 5 } }, new int[][] { new int[] { 6, 7, 8, 9 }, new int[] { 10, 11 }, new int[] { 12, 13, 14, 15, 16 } }, new int[][] { new int[] { 17, 18, 19 }, new int[] { 20 } }, new int[][] { new int[] { 21, 22 }, new int[] { 23, 24, 25 }, new int[] { 26, 27 }, new int[] { 28 } }, new int[][] { new int[] { 29, 30, 31, 32 }, new int[] { 33, 34 }, new int[] { 35 } }, }; // 3次元配列を表示する // まずはZ方向の次元からforeachループを回す foreach (int[][] z in _3DArray) { // 次にY方向の次元でforeachループを回す foreach (int[] y in z) { System.Console.Write("["); // 最後にX方向の次元でforeachループを回す foreach (int x in y) { System.Console.Write("{0,3}", x); } System.Console.Write("], "); } System.Console.WriteLine(); } } } }
//実行結果
foreach文がわかりにくいかもしれませんが、高次元から低次元に向かってアクセスしていくイメージでOKです。なお、ジャグ配列の生成と初期化を個別に行う場合は、以下のサンプルコードのように、forループで個別に配列を追加していきましょう。
//サンプルプログラム
namespace Test { class Program { static void Main(string[] args) { // int型2次元ジャグ配列を生成する // 初期値を設定しない場合は、「最上位次元の要素数だけ」指定する必要がある // 下位次元の配列は「別オブジェクト」扱いなので、ここでは指定できない int[][] _2DArray = new int[10][]; // 最上位次元のそれぞれの要素に、配列を追加していく for (int i = 0; i < _2DArray.Length; i++) { _2DArray[i] = new int[5]; } // インデックスカウント用の変数を宣言する int index = 0; // 配列に値を格納していく // まずは縦方向の次元からforループを回す for (int y = 0; y < _2DArray.Length; y++) { // 次に横方向の次元でforループを回す for (int x = 0; x < _2DArray[y].Length; x++) { // 順番に値を設定していく _2DArray[y][x] = index++; } } // 2次元配列を表示する // まずは縦方向の次元からforeachループを回す foreach (int[] y in _2DArray) { // 次に横方向の次元でforeachループを回す foreach (int x in y) { System.Console.Write("{0,3}", x); } // 改行して体裁を整える System.Console.WriteLine(); } } } }
//実行結果
C#では四角形配列が一般的に使われていますが、実際のところはあまり扱いやすくありません。他言語ではジャグ配列が基本で、foreach文もジャグ配列のほうが扱いやすいです。そのため、C#の多次元配列は四角形配列だけではなく、場合によってはジャグ配列を使うほうがいいでしょう。
C#の配列は、同じデータ型の値をまとめて管理できる便利な機能です。うまく使えばソースコードを簡潔化でき、さまざまな便利メソッドも使えます。とくに、ソートや検索などの機能は、配列やリストのような「コレクション」でなければ使えません。多次元配列は2種類ありますが、それぞれ使い勝手が大きく異なるので、必要に応じて使い分けましょう。
2024.06.17
子供におすすめのプログラミングスクール10選!学習メリットや教室選びのコツも紹介
#プログラミングスクール
2022.01.06
【完全版】大学生におすすめのプログラミングスクール13選!選ぶコツも詳しく解説
#プログラミングスクール
2024.01.26
【未経験でも転職可】30代におすすめプログラミングスクール8選!
#プログラミングスクール
2024.01.26
初心者必見!独学のJava学習方法とおすすめ本、アプリを詳しく解説
#JAVA
2024.01.26
忙しい社会人におすすめプログラミングスクール15選!失敗しない選び方も詳しく解説
#プログラミングスクール
2022.01.06
【無料あり】大阪のおすすめプログラミングスクール14選!スクール選びのコツも紹介
#プログラミングスクール
2024.01.26
【目的別】東京のおすすめプログラミングスクール20選!スクール選びのコツも徹底解説
#プログラミングスクール
2024.01.26
【無料あり】福岡のおすすめプログラミングスクール13選!選び方も詳しく解説
#プログラミングスクール
2024.01.26
【徹底比較】名古屋のおすすめプログラミングスクール13選!選び方も詳しく解説
#プログラミングスクール
2024.01.26
【徹底比較】おすすめのプログラミングスクール18選!失敗しない選び方も徹底解説
#プログラミングスクール