2-4 配列
2-4-1 C# の配列
一般的に配列とは、同じデータ型のデータ(要素)の連続的な集合のことで、それぞれのデータ(要素)を識別するために、添え字(インデックス)で区別します。もちろん、C# でも配列を扱うことができます(図 2-2 )。
[例]配列の各要素を識別して値を代入
1: // 配列 d の要素に値を代入
2: d[0] = 100; // 先頭の要素に値を代入
3: d[3] = 200; //4 番目の要素に値を代入
●図 2-2 ● C# の配列
ただし、注意すべき点が 2 つあります。
1 つは、C# の配列変数が常に参照型であるということです。そのため、メモリの領域を確保するために、その表記方法に注意が必要です。この点については、次の 2-4-2 「配列の変数宣言とメモリ確保」で改めて扱います。
もう 1 つは、添え字を複数もつ「多次元」の配列には、「多次元配列」と「多段階配列」の 2 つがある点です。この点については、2-4-4 「多次元配列」、および 2-4-5 「多段階配列」で説明します。
[例]多次元配列と多段階配列
1: tbl[3,4] = 100; // 多次元配列
2: dat[3][4] = 200; // 多段階配列
2-4-2 配列の変数宣言とメモリ確保
配列変数を宣言する書式は、以下のとおりです。角括弧 [] を変数名の前につけます。角括弧の前後はスペースがあってもなくともかまいません。注意する点は、この変数は参照型であり、配列の実体である要素の集合は、まだメモリ上に確保されてないことです。この変数は配列の参照情報をもつに過ぎません。
データ型 [] 配列変数名 ;
[例]配列変数を宣言する
1: int [] dat; // 配列変数 dat を宣言
2: int [] da1, da2; // 配列変数 da1, da2 の 2 つを宣言
3: int[] array1;
配列の実体を確保するには、new キーワードを使って以下のように書きます。
配列変数 = new データ型 [ 要素数 ];
[例]配列の要素を確保する
1: int [] dat = new int[10]; // 配列要素確保
2: int [] lst;
3: lst = new int[20]; // 配列要素確保
4: dat[0] = 100;
new キーワードによって配列が確保されます。new の後ろには、データ型と要素数を示す角括弧 [] を書きます。また、上記の 1 行目と 3 行目の「=」演算子によって、左辺の配列変数に参照情報が格納されます。いったん配列を確保して、参照情報が配列変数に格納されれば、ここで初めて配列変数を利用することができます。4 行目は、配列の先頭の要素(添え字 0 の要素)に値 100 を代入している例です。4 行目の記述は、配列の実体である要素に対して作用します。なお、添え字は常に 0 から始まります。
この配列 dat の状態を図示すると、次のようになります(図 2-3 )。
●図 2-3 ● 配列を参照する配列変数 dat
この図が示すとおり、配列変数 dat は配列の実体ではありません。配列を操作するには、まず配列変数を宣言し、さらに配列の要素を new キーワードで明示的に確保するという、2 ステップの操作が必要です。
● for VB6 VB では、例えば以下のように宣言するだけで、Integer 型の要素を確保することができますが、C# にはこのような表現に該当する記述はありません。明示的に要素を確保する必要があります。 以下の宣言をすると、VB の既定では配列要素( 0 )から( 10 )までの 11 個の要素を確保しますが、C# では new int[10] と記述した場合、[0] から [9] までの 10 個です。また、C# では開始する添え字を変更することはできません。 [例] Visual Basic の配列
● for C++ C++ では、以下の例のように要素数を直接確保する配列宣言ができますが、C# ではこの宣言はできません。 [例] C++ の配列
|
また、C# の配列は、括弧 {} に初期値を並べて、配列の各要素の初期化をおこなうことができます。
配列変数 = new データ型 [ 要素数 ]{ 初期値のリスト };
[例]配列要素の初期化
1: int [] dt1 = new int[5]{ 10, 20, 30, 40, 50};
2: int [] dt2 = new int[]{ 10, 20, 30, 40, 50 };
3: int [] dt3 = { 10, 20, 30, 40, 50 }
4: int [] a, b;
5: a = new int[5]{ 10, 20, 40, 80, 160 };
6: b = new int[]{ 10, 20, 40, 80, 160 };
前記の例の 2 行目や 6 行目にあるように、初期値のリストをともなうときは要素数を省略することができます。また、3 行目のように配列宣言と同時に単に初期値のリストを書くだけで、必要な要素数の配列を確保することもできます。
なお、配列の要素にアクセスする添え字について、存在しない要素をアクセスするような誤った添え字を使うと実行エラー(例外)が発生します。そのまま配列を飛び出して、不正にメモリ領域をアクセスしてしまうことはありません。
[例]配列の誤った添え字を指定すると実行エラーになる
1: int [] dat = new int[5];
2: dat[10] = 100; // この行で実行エラー発生(例外発生)!!
この項のまとめとして、配列変数の宣言、要素の確保、初期化をおこなう完成したサンプルをあげておきます。
[例]配列変数の宣言、要素の確保、初期化
1: namespace MySpace
2: {
3: public class MyApp
4: {
5: public static void Main(string [] args)
6: {
7: int [] dat1 = new int[5];
8: int [] dat2 = new int[3]{ 10, 20, 30 };
9: dat1[0] = 100;
10: System.Console.WriteLine(dat1[0]);
11: System.Console.WriteLine(dat2[1]);
12: }
13: }
14: }
2-4-3 配列変数による参照と配列の解放
C# では、new キーワードで明示的にメモリ上に確保したデータ領域を、明示的に解放する必要はありません。メモリの確保・解放は、CLR ( Common Language Runtime )によって管理されており、CLR がもつガベージコレクタによって、不要になったメモリは自動的に解放されます。
以下の例をもとに、配列の確保・解放の流れを考えてみます。
[例]配列の操作
1: namespace MySpace
2: {
3: public class MyApp
4: {
5: public static void Main(string [] args)
6: {
7: int [] dat, lst;
8: dat = new int[5]{ 10, 20, 40, 80, 160 };
9: lst = new int[] { 10, 20, 30 };
10: // その他の処理(画面表示例)
11: System.Console.WriteLine(dat[0]);
12: //
13: lst = null;
14: }
15: }
16: }
この例では、7 行目でローカル変数として配列変数 dat と lst を宣言しています。配列変数そのものは Main メソッドの開始とともに確保され、Main メソッドの終了とともに解放されます。これはローカル変数の性質です。
一方、配列の実体は、8 行目と 9 行目で new キーワードを使って確保しています。この明示的に確保された配列の実体は、プログラムから参照されなくなると、CLR のガベージコレクタによって削除されます。
8 行目で確保された配列の実体は、配列変数 dat によって参照されています。そのため、配列変数 dat が解放されると、配列の実体は誰からも参照されなくなるので、配列の実体もガベージコレクタによって解放されます。厳密にいえば、誰からも参照されなくなった時点で解放の候補としてマークされ、定期的にガベージコレクタが解放候補のデータを解放しています。
9 行目で確保された配列の実体は、配列変数 lst によって参照されています。この配列変数 lst は、13 行目で「null」が代入されています。null は C# のキーワードであり、「どこも参照していない」ことを表す情報です。つまり、13 行目の記述によって、配列変数 lst は配列の実体を参照しなくなりました。つまり、配列の実体は、この時点で誰からも参照されなくなり、解放の候補としてマークされます。
● for VB6 C# の参照型の変数に null を代入することは、VB にとってオブジェクトを表す変数に Nothing を設定してオブジェクトの参照を止めることに似ています。 [例]オブジェクト変数に Nothing を設定する
|
また、複数の配列変数が、同じ配列の実体を参照することもできます。次の例では( Main メソッドのみ抜粋)、Main メソッドのローカル変数として 2 つの配列変数が用意されていますが、両方とも同じ配列を参照しています。この例では、5 行目で配列変数 dat がもつ参照情報を配列変数 lst に代入しています。この代入文は、配列要素を相手の配列要素に代入している訳ではなく、単に参照情報の代入にすぎません。
[例] 2 つの配列変数が同じ配列の実体を参照する
1: public static void Main(string [] args)
2: {
3: int [] dat, lst;
4: dat = new int[5];
5: lst = dat; //dat がもつ参照情報を lst に代入
6: lst[0] = 100;
7: // その他の処理
8: //
9: dat = null;
10: }
そのため、9 行目で変数 dat のみ参照を止めても、9 行目の時点では変数 lst はまだ同じ配列の実体を参照しています。
2 つの配列変数が同じ配列の実体を参照している様子を表しているのが、以下の図 2-4 です。2 つの配列変数が同じ配列の実体を参照しているということは、dat[0] と lst[0] は、全く同一のメモリ領域を示すことになります。
●図 2-4 ● 2 つの配列変数が同一のメモリ領域を参照する
◆スタックとヒープ C# (というよりは CLR )は、ほかのプログラム実行環境と同様に、データを格納する領域として「スタック」と「ヒープ」を利用しています。「スタック」と「ヒープ」は、プログラミング用語としては、特定の役割をもつメモリ領域を示す言葉です。一般にコンピュータ用語としての「スタック」領域は、「Fisrt-In Last-Out」とよばれる「先入れ後出し」形式のデータ構造のことをさし、データが上にどんどん積まれ、取り出すときは上から順番に取り出すデータの蓄積形態を意味します。 一般にプログラムでは、この「スタック」を内部的にメソッド呼び出しのための一時的なデータ領域として利用しています。例えば、ローカル変数の領域確保にはスタックが使われています。メソッドが呼び出されると、スタックにローカル変数が確保され、メソッドを抜けて終了すると、スタックからローカル変数が解放されます。もし、メソッド内から、さらに別のメソッドを呼び出すと、スタックには新たによばれたメソッドのためのローカル変数領域が確保されます。つまり、メソッドをよぶたびにスタックにローカル変数の領域が積まれ、メソッドを抜けて呼び出しを終了すると、その分だけスタックはクリア されます。つまり、スタック領域に確保された領域は、プログラマが明示的に確保する必要もなく、また明示的に解放する必要もありません。 なお、スタックは、上記のようにどんどんデータを積み上げる構造であることから、連続的な領域である場合が多いですが、CLR 環境ではスタックが必ずしも連続であるとはかぎりません。しかし、メソッド呼び出しごとに空き領域を CLR が確保し、メソッド呼び出しの終了とともにその領域は解放されるので、「論理的なスタック」としてとらえることができます。いずれにしても、大切な点は、スタック領域の確保や解放をプログラマが意識する必要はないということです。 一方、プログラミング用語としての「ヒープ」は、プログラマが明示的に確保することが必要な領域を意味し、前述のスタックのようなしくみはありません。必要に応じて自由なサイズのメモリ領域を確保できます。また、従来の C++ で扱う「ヒープ」領域は、メモリ解放もプログラマの責任ですが、C# などの実行環境である CLR では、メモリ解放はガベージコレクタによって自動的におこなわれます。 C# の場合でも、ローカル変数はスタックに確保されますが、new キーワードで確保したメモリ領域はヒープに確保されます。 つまり、値型や参照型のローカル変数自体はスタック領域にあります。参照型の場合、スタックに存在するのは参照情報を保持する変数だけです。それに対して、参照型の変数で参照する配列の実体はヒープに確保されます。つまり、配列の実体を確保するには、new キーワードで確保しなければなりません。 ◆ガベージコレクション 不要になったヒープ上の領域は、ガベージコレクタによって解放されます。この処理のことをガベージコレクションといいます。この機能は CLR に備わった機能であり、そのため CLR 向けのアプリケーションを開発する C# では、ヒープ領域の解放をプログラマが意識しなくともよいことになります。 |
2-4-4 多次元配列
C# にも、複数の添え字をもつ多次元配列があります。多次元配列では、角括弧 [] の中にカンマで区切って添え字を並べます。以下は、3 × 2 の 2 次元配列の例です。
[例] 2 次元配列
1: int [,] tlb = new int[3,2];
2: int [,] dat = new int[3,2]{ {5,8}, {10,20}, {30,40} };
3: tlb[0,0] = 100;
4: tlb[0,1] = 200;
2 次元は行と列からなるテーブルを表現するときなどに利用しますが、もちろんこのテーブルは仮想的なテーブルであり、メモリ上に本当に 2 次元に並んでいる訳ではありません。メモリはリニアな空間(直列的にデータが並ぶ空間)であり、プログラマからみた際にあたかもテーブルのようにデータを扱えるだけです(ただし、テーブルといってもリレーショナルデータベースのテーブルとは異なり、どの列も同じデータ型です)。
角括弧 [] の中に複数の添え字をカンマで区切って並べる以外は、今まで説明した配列の使い方と同じです(図 2-5 )。
●図 2-5 ● 2 次元配列
以下の例は、2 次元配列に初期値を代入して表示をおこなっているサンプルです。
[例]配列変数の宣言、要素の確保、初期化
1: namespace MySpace
2: {
3: public class MyApp
4: {
5: public static void Main(string [] args)
6: {
7: int [,] dat = new int[3,2] {
8: {10,20}, {100,200}, {300,500} };
9: System.Console.WriteLine(dat[0,0]); //10
10: System.Console.WriteLine(dat[1,0]); //100
11: System.Console.WriteLine(dat[2,1]); //500
12: }
13: }
14: }
2-4-5 多段階配列
多段階配列とは、「配列の配列」ということができます。配列の要素自体が、他の配列を参照する「参照情報の配列」です。以下は 2 段階の配列です。この「段階」が増えるごとに、角括弧 [] の数が増えてきます。
[例] 2 つの段階をもつ配列
1: int [][] m = new int[3][]; // 最初の段階
2: m[0] = new int[2]{ 20, 39 }; // 次の段階
3: m[1] = new int[4]{ 1, 2, 3, 4 };
4: m[2] = new int[3]{ 10, 20, 40 };
5: int [][] n = new int[3][] {
6: new int[2]{ 20, 39 },
7: new int[4]{ 1, 2, 3, 4 },
8: new int[3]{ 10, 20, 40 };
9: };
10: m[2][0] = 100;
この例では、2 つの配列変数 m と n があり、それぞれ別々の配列実体をもっていますが構造は同じです。個々の構文の意味をみる前に、この構造の概念図を示します(図 2-6 )。
●図 2-6 ● 概念図
多段階配列では、文字通り、段階を踏んで配列の実体を確保します。
多段階配列の m では、最初の段階としてまず 1 行目で new キーワードを使って、配列の参照情報のための配列を用意しています。ここでの要素の数は 3 つです。「new int[3][]」というように、角括弧が 2 つあり、2 番目の角括弧 [] が空であることが、2 段階のまだ途中の段階であることを示しているといえます。ここで確保された配列要素には、データそのものではなく、ほかの配列を参照するための参照情報が入ります。この参照情報が入る配列は、m[0]、m[1]、m[2] という形式で参照できます。
次の段階として、2 行目から 4 行目では、1 行目で確保した配列要素に対して、それぞれ別々の配列の参照情報を設定しています。
この 1 行目から 4 行目にかけての 2 段階の処理を 1 つにまとめたのが、5 行目から 9 行目の記述です。いずれにしても、複数の段階に分けて配列要素を確保するので、1 回の new キーワードで多段階配列をつくることはできません。
また要素を参照するときは、段階が増えるにつれて、角括弧 [] の数が増えます。10 行目の記述は、2 段階の多段階配列の末端の要素にアクセスするときの書き方です。
以下のサンプルは、多段階配列を確保し、末端の要素の値を表示している例です。
[例]多段階配列のアクセス
1: namespace MySpace
2: {
3: public class MyApp
4: {
5: public static void Main(string [] args)
7: {
8: int [][] m = new int[3][]; // 最初の段階
9: m[0] = new int[2]{ 20, 39 }; // 次の段階
10: m[1] = new int[4]{ 1, 2, 3, 4 };
11: m[2] = new int[3]{ 10, 20, 40 };
11: System.Console.WriteLine(m[0][1]); //39
11: System.Console.WriteLine(m[1][3]); //4
12: System.Console.WriteLine(m[2][0]); //10
13: }
14: }
15: }
2-4-6 配列サイズの取得
C# では、配列の要素数を取得する記述方法も用意されています。以下のように、「.Length」と記述すると、その配列変数が参照している配列の要素数がわかります( L だけ大文字です)。
配列変数名 .Length
[例]配列の要素数を調べる
1: int num;
2: int [] dat = new int[]{ 10, 20, 40, 80 };
3: num = dat.Length; // 要素数 4 が変数 num に代入される
なお、この記述を多次元配列に使用すると、要素の総数が求められます。
[例]配列の要素数を調べる
1: int num;
2: int [,] dat = new int[3,4];
3: num = dat.Length; // 要素数 12 が変数 num に代入される
多次元配列で、各次元ごとの要素数を調べる場合、「.GetLength()」メソッドを使います。丸括弧 () の中に記述する値(引数)は次元を表し、添え字の左側が最も小さい次元です。
[例]配列の次元ごとの要素数を調べる
1: int num1, num2;
2: int [,] dat = new int[3,4];
3: num1 = dat.GetLength(0); // 要素数 3 が変数 num1 に代入される
4: num2 = dat.GetLength(1); // 要素数 4 が変数 num2 に代入される
また、多段階配列では、それぞれの段階の要素数を求めることができます。
[例]多段階配列での要素数を調べる
1: int num1, num2, num3, num4;
2: int [][] m = new int[3][]; // 最初の段階
3: m[0] = new int[2]{ 20, 39 }; // 次の段階
4: m[1] = new int[4]{ 1, 2, 3, 4 };
5: m[2] = new int[3]{ 10, 20, 40 };
6: num1 = m.Length; // 要素数 3 が num1 に代入
7: num2 = m[0].Length; // 要素数 2 が num2 に代入
8: num3 = m[1].Length; // 要素数 4 が num3 に代入
9: num4 = m[2].Length; // 要素数 3 が num4 に代入
● for VB6 C# では、あらゆるデータ型がオブジェクトであり、配列もオブジェクトです。「.Length」という記述は、このオブジェクトの Length プロパティにアクセスする記述方法です。また、GetLength もこのオブジェクトのメソッドです。 ● for C++ 実は C# で配列として表記される配列の実体は、Base クラスライブラリに定義されている Array クラス型のオブジェクトです。Length や GetLength メソッドは、この Array クラスのメンバです。 |
次に、配列の要素を調べるサンプルをあげておきます。
[例]要素数を調べる
1: namespace MySpace
2: {
3: public class MyApp
4: {
5: public static void Main(string [] args)
6: {
7: int [] d = new int[5]; //1 次元配列
8: int [,]t = new int[3,2]; //2 次元配列
9: int [][] m = new int[3][]; //2 段階の配列
10: m[0] = new int[2];
11: m[1] = new int[4];
12: m[2] = new int[3];
13: System.Console.WriteLine(d.Length); //5
14: System.Console.WriteLine(t.Length); //6
15: System.Console.WriteLine(t.GetLength(0));//3
16: System.Console.WriteLine(m.Length); //3
17: System.Console.WriteLine(m[0].Length); //2
18: System.Console.WriteLine(m[1].Length); //4
19: }
20: }
21: }