配列
配列とは
プログラミングで値を一時保存するには変数を使用しますが、一度に大量の変数が必要になる事もあります。
100個や1000個など、大量に必要になる場合にひとつずつ変数を宣言していたのではコードの記述が大変です。
同じデータ型の変数が複数必要な場合、配列を使用するとコードが簡潔になります。
まずは配列を使用しない場合のサンプルコードから。
static void Main(string[] args)
{
int num1, num2, num3;
num1 = 10;
num2 = 20;
num3 = 30;
Console.WriteLine(num1);
Console.WriteLine(num2);
Console.WriteLine(num3);
Console.ReadLine();
}
配列を使用すると、以下のように書くことができます。
static void Main(string[] args)
{
//配列の宣言
int[] arr = new int[3];
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
Console.WriteLine(arr[0]);
Console.WriteLine(arr[1]);
Console.WriteLine(arr[2]);
Console.ReadLine();
}
4行目が配列の宣言です。
配列は同じデータ型の変数を任意の数まとめて扱うことができます。
配列の宣言
データ型[] 配列名 = new データ型[要素数];
配列の宣言は、まず変数の時のように扱いたいデータ型を記述します。
その次に角括弧[]を記述します。
これは「これから配列を使用する」ということを表します。
続いて、好きな配列名を記述します。
名前は変数名と同じく自由に指定できます。
ここまでで配列の使用宣言になりますが、まだ初期化されていない状態です。
実際に配列の機能を使用するには初期化が必要です。
配列の初期化にはnewキーワードを使用します。
続いて先ほど指定したのと同じデータ型を記述し、[]の中に配列のサイズを数値で指定します。
配列のサイズ(要素数)
配列のサイズとは、その配列に保存できるデータの数です。
要素数とも言います。
配列に保存される各データを要素と言います。
サンプルコードの配列は中にint型のデータを3個保存できます。
配列の要素へのアクセス
配列の各要素に値を代入したり、値を取り出したりするには角括弧[]にアクセスしたい要素を数値で指定します。
この角括弧を添字演算子(インデクサ)と言います。
添え字演算子の中に記述する数値を添え字、indexと言います。
ここで注意があります。
配列の添え字は「0」から始まるということです。
つまり、要素数3の配列を使用宣言した場合、各要素の添え字は「0」「1」「2」の三つとなります。
配列の最後の要素番号は配列のサイズより一つ少ない値になるので注意しましょう。
後は変数と同じように使用することができます。
値の取り出しも代入も変数と同じです。
int[] arr = new int[2];
arr[0] = 999;
arr[1] = arr[0];
Console.WriteLine(arr[0]); //999
Console.WriteLine(arr[1]); //999
配列の初期化
先ほどは単純に配列の要素数を指定するだけの初期化方法でしたが、他にも便利な初期化方法があります。
初期化と同時に各要素に値を格納
int[] arr = new int[3] { 10, 20, 30 };
このようにすると、配列arrの宣言と同時に、先頭の要素から順に「10」「20」「30」の値が格納された状態になります。
このブロック(波括弧)を初期化リストまたは配列初期化子といいます。
最初のサンプルコードでは初期化リストを与えていませんでしたが、その場合は自動的に既定値で初期化された状態になります。
int型の既定値は「0」です。
int[] arr = new int[3];
Console.WriteLine(arr[0]); //0
Console.WriteLine(arr[1]); //0
Console.WriteLine(arr[2]); //0
それぞれのデータ型の既定値については既定値を参照してください。
要素数の省略
初期化リストを使用して初期化する場合、配列の要素数の指定は省略することができます。
//自動的に要素数は3になる
int[] arr1 = new int[] { 10, 20, 30 };
//自動的に要素数は5になる
int[] arr2 = new int[] { 10, 20, 30, 40, 50 };
配列のサイズは初期化リストのデータ数から自動的に決定されます。
配列の初期化宣言で配列の要素数を指定する場合、要素数の指定と初期化リストのデータ数は同じにしないとエラーになります。
つまり、初期化リストのデータ数を変更すると要素数も変更しなければなりませんし、逆も然りです。
これは面倒なので、多くの場合で要素数の指定は省略されます。
データ型の省略
配列の宣言時に初期化リストを使用する場合、newの後ろのデータ型の指定は省略することができます。
int[] arr1 = new int[] { 10, 20, 30 };
//これでもOK
int[] arr2 = new[] { 10, 20, 30 };
newの省略
配列の宣言時に初期化リストを使用する場合、newとその後のデータ型の指定も省略することができます。
int[] arr1 = new int[] { 10, 20, 30 };
//これでもOK
int[] arr2 = { 10, 20, 30 };
初期化リストは初期化時にしか使用できない
初期化リストを使用して配列へ値をセットする方法は、配列の初期化時にしか使用できません。
int[] arr = new int[3];
//エラー
arr = { 10, 20, 30 };
//エラー
arr { 10, 20, 30 };
//newで新しい配列で初期化しなおすのはOK
arr = new int[5] { 10, 20, 30, 40, 50 };
範囲外アクセス
配列の添え字に配列サイズ以上の値を指定したり、マイナス値を指定するとエラーになります。
このエラーはコードの記述時点(コンパイル時点)では検出できず、実行時エラーになります。
(文法的には間違いではないため、実際にプログラムを実行してみるまでエラーかどうかがわからない)
//この配列の添え字の最大値は「2」
int[] arr = new int[] { 10, 20, 30 };
//以下はすべてエラー
//添え字の数値が大きすぎる
arr[3] = 0;
//マイナスは指定できない
arr[-1] = 0;
実行時エラーは正しくは例外と言います。
この例外はコンパイルエラー(文法上の間違い)とは違い、とりあえずプログラムを起動することは可能です。
プログラムの処理が例外の箇所に達すると、そこでプログラムが停止します。
例外は適切に処理することで、プログラムを停止することなく続行させることができます。
詳しくは例外で説明します。
配列のサイズを得る
配列のサイズを調べるにはLengthプロパティを使用します。
int[] arr = new int[] { 10, 20, 30 };
Console.WriteLine(arr.Length);
3
プロパティとは変数などが内部的に持っている情報のことです。
変数名に続いて「.」(ドット)で区切ることで内部の情報にアクセスできます。
これは「Console.WriteLine」の時と同じですが、WriteLineはメソッド(関数)であるのに対してLengthプロパティはただの数値(int型)であることに注意してください。
Lengthプロパティは読み取り専用で、変数のように値を代入することはできません。
Lengthプロパティを用いて配列の最後の要素にアクセスするには以下のようにします。
int[] arr = new int[] { 10, 20, 30 };
Console.WriteLine(arr[arr.Length - 1]);
30
配列の要素数がいくつであっても全く同じ記述で最後の要素にアクセス可能です。
ただし要素数がゼロの場合は範囲外アクセスになります。
なおLengthプロパティで得られる値はただのint型の値ですから、変数に保存しておくこともできます。
他の配列の宣言時のサイズに使用することもできます。
int[] arr1 = new int[] { 10, 20, 30 };
int arr1Length = arr1.Length;
int[] arr2 = new int[arr1.Length];
配列の丸ごとコピー?
配列の各要素は通常の変数と同じように扱うことができますが、「配列」自体、つまり添え字を使用しない場合はやや特殊な扱いとなります。
例えば配列を丸ごとコピーしようとして、以下のように配列変数に別の配列変数を代入してみます。
static void Main(string[] args)
{
int[] arr1 = new int[] { 10, 20, 30 };
int[] arr2 = new int[arr1.Length];
arr2 = arr1;
Console.WriteLine(arr2[0]);
Console.WriteLine(arr2[1]);
Console.WriteLine(arr2[2]);
Console.ReadLine();
}
10 20 30
一見うまく動作しているようにも見えますが、これは避けるべきです。
このコードの配列arr2は、配列arr1の各要素の値はコピーされておらず、配列arr1そのものになってしまっているのです。
試しにコピー元の配列arr1の値を書き換えてみます。
static void Main(string[] args)
{
int[] arr1 = new int[] { 10, 20, 30 };
int[] arr2 = new int[arr1.Length];
arr2 = arr1;
Console.WriteLine(arr2[0]);
Console.WriteLine(arr2[1]);
Console.WriteLine(arr2[2]);
arr1[0] = 999;
Console.WriteLine();
Console.WriteLine(arr2[0]);
Console.WriteLine(arr2[1]);
Console.WriteLine(arr2[2]);
Console.ReadLine();
}
10 20 30 999 20 30
arr2にarr1を代入した後はarr2には手を加えていません。
その後にarr1[0]の値を書き換えるとarr2[0]の値も変わっていることがわかります。
つまり、配列に別の配列を代入すると、各要素のコピーではなく代入した配列の別名になってしまうのです。
それを理解した上で代入するのならば構いませんが、各要素のコピーが欲しいのならばこの方法は使えません。
なぜこのような動作になるかは値型と参照型で解説しています。
要素の中身ごとコピー
以下はまだ説明していない機能を使用しているので、参考程度にみてください。
要素の丸ごとコピーをしたいのならば各要素をひとつずつ代入していくのが最も単純な方法です。
数が多いと大変ですが、for文などのループ文を使用すれば簡単に記述可能です。
int[] arr1 = new int[] { 10, 20, 30 };
int[] arr2 = new int[arr1.Length];
for (int i = 0; i < arr1.Length; i++)
{
arr2[i] = arr1[i];
}
そのほか、専用の関数を使用する方法もあります。
static void Main(string[] args)
{
int[] arr1 = new int[] { 10, 20, 30 };
int[] arr2 = new int[arr1.Length];
//配列のすべての要素のコピー
Array.Copy(arr1, arr2, arr2.Length);
Console.WriteLine(arr2[0]);
Console.WriteLine(arr2[1]);
Console.WriteLine(arr2[2]);
arr1[0] = 999;
Console.WriteLine();
Console.WriteLine(arr2[0]);
Console.WriteLine(arr2[1]);
Console.WriteLine(arr2[2]);
Console.ReadLine();
}
10 20 30 10 20 30
arr1の要素の書き換えがarr2には影響していないことがわかります。
arr2はarr1とは独立した配列になったためです。
.NET Framework3.5以上ならば以下の方法が最も簡単です。
(最近のバージョンならば使用可能)
using System;
using System.Linq; //←これが必要(無ければ追加)
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
int[] nums1 = new int[] { 1, 2, 3 };
//nums1のコピー
int[] nums2 = nums1.ToArray();
}
}
}