.NET FrameworkのSystem.Collection名前空間には様々な機能を持った便利なクラスが複数存在します。 これらはコレクションと呼ばれ、一般的に使う配列の様に一つのコレクションオブジェクトが複数のオブジェクトを保持する一方、配列とは異なり、ソートや動的な領域確保など配列ではなしえない機能を持っています。 ここではその便利なコレクションクラスのいくつかを見ていくことにします。
ArrayListクラスは配列に非常によく似ています。 しかし、配列とは異なり、動的に配列の大きさを増減できることがArrayListの特徴であります。 次のコードは、ArrayListを使った一般的なソースコードです。
using System; using System.Collections; namespace Collection { class Collection { [STAThread] static void Main(string[] args) { // ArrayList のインスタンスを作成 ArrayList arr = new ArrayList(); // アイテムを追加 arr.Add( 2 ); arr.Add( 8 ); arr.Add( 5 ); arr.Add( 0 ); arr.Add( 4 ); arr.Add( 9 ); arr.Add( 7 ); arr.Add( 3 ); arr.Add( 6 ); arr.Add( 1 ); // アイテムを削除 arr.Remove( 1 ); arr.Remove( 4 ); arr.Remove( 6 ); arr.Remove( 9 ); // 特定の位置にあるアイテムを削除 arr.RemoveAt( 1 ); // 列挙 EnumerateArrayList( arr ); // ソート arr.Sort(); // 再び列挙 EnumerateArrayList( arr ); // 順番を反転 arr.Reverse(); // 再び列挙 EnumerateArrayList( arr ); } // ArrayListの値を列挙 public static void EnumerateArrayList( ArrayList arr ) { for ( int i = 0; i < arr.Count; i++ ) { Console.Write( arr[i].ToString() + ", " ); } // 改行 Console.Write( Console.Out.NewLine ); } } }
2, 5, 0, 7, 3, 0, 2, 3, 5, 7, 7, 5, 3, 2, 0, Press any key to continue
この例を見てわかるように、ArrayListはインデックスによる操作ではなくAdd()及びRemove()によって値を追加したり削除したりします。 この例ではint型を代入していますが、ArrayListではobject型を扱うことができるので、どのような型でもArrayListに追加することができます。 Remove()メソッドはそのアイテム自体をArrayListから削除するのに対し、RemoveAt()メソッドは指定されたインデックスのアイテムを削除します。
また、ArrayListには便利なメソッドがいくつか含まれていて、Sort()メソッドはアイテムを特定の方法(ここでは大きさ順)で並べ替えたり、Reverse()メソッドで並びを逆にしたりする事もできます。 また、ここでは使用しませんでしたが、アイテム全てを削除するためにClear()メソッドというものも用意されていますし、ArrayListからふつうの配列を作成するToArray()メソッドなども存在します。
上の例では、Add及びRemoveでアイテムを追加・削除していましたが、C#ではインデクサによってアイテムを追加した後の ArrayListのインデックスで指定されたアイテムに直接アクセスすることができます(VB.NETではこの機能は既定プロパティとしてサポートされています)。 その例が次のコードです(この例では文字列とint型の値をArrayListに同居させています)。
using System; using System.Collections; namespace Collection { class Collection { [STAThread] static void Main(string[] args) { // ArrayList のインスタンスを作成 ArrayList arr = new ArrayList(); arr.Add( 0 ); arr.Add( 1 ); arr.Add( 2 ); arr.Add( 3 ); arr.Add( 4 ); arr[0] = "A"; arr[2] = "B"; arr[4] = "C"; EnumerateArrayList( arr ); } // ArrayListの値を列挙 public static void EnumerateArrayList( ArrayList arr ) { for ( int i = 0; i < arr.Count; i++ ) { Console.Write( arr[i].ToString() + ", " ); } // 改行 Console.Write( Console.Out.NewLine ); } } }
A, 1, B, 3, C Press any key to continue
HashtableクラスはArrayList同様、コレクション内の要素数は動的に変化しますが、ArrayListとは異なりキーと呼ばれる値でコレクションにアクセスすることができます。 つまり、インデクサではint値だけでなくすべてのobject値をインデックス(キー)としてとることができます(厳密には全てではなくインスタンスのハッシュ値を取得できるオブジェクト)。 HashtableはPerlなどで連想配列と呼ばれるものに相当します。 Hashtableではキーと値を対にして登録します。
using System; using System.Collections; namespace Collection { class Collection { [STAThread] static void Main(string[] args) { // Hashtable のインスタンスを作成 Hashtable hash = new Hashtable(); hash.Add( "A", 1 ); hash.Add( "B", 3 ); hash.Add( "C", 5 ); hash.Add( "D", 7 ); hash.Add( "E", 9 ); Console.Write( hash["E"].ToString() + ", " ); Console.Write( hash["D"].ToString() + ", " ); Console.Write( hash["C"].ToString() + ", " ); Console.Write( hash["B"].ToString() + ", " ); Console.Write( hash["A"].ToString() + Console.Out.NewLine ); // Hashtableのキーとして「A」が含まれているか if ( hash.ContainsKey( "A" ) ) { Console.WriteLine( "Hashtable contains \"A\" as key" ); } // Hashtableの値として「2」が含まれているか if ( hash.ContainsValue( "2" ) ) { Console.WriteLine( "Hashtable contains \"A\" as value" ); } } } }
9, 7, 5, 3, 1 Hashtable contains "A" as key Press any key to continue
この例では文字列をキーとして値を追加しています。 Hashtable内のアイテムにアクセスする場合は、インデクサにキーを指定します(この例では文字列)。 また、特定のオブジェクトがキーまたは値としてHashtableに含まれているかを取得するために、ContainsKey()及び ContainsValue()メソッドが用意されています。 27から36行目のコードはそれを利用したものです。 また、この例では使用しませんでしたが、HashtableにもArrayList同様にRemove()メソッドが存在します。 ただ、このメソッドもインデックスではなくキーで削除するアイテムを指定します。
SortedListクラスは、HashtableクラスとArrayListクラスの両方の特徴を持っています。 格納しうるアイテム数は増減することができ、ArrayListの様にインデックスでアイテムにアクセスしたりできる一方で、Hashtableのようにキーでアイテムにアクセスすることができます。 実際、どのように動作するか、サンプルを見てみましょう。
using System; using System.Collections; namespace Collection { class Collection { [STAThread] static void Main(string[] args) { // SortedList のインスタンスを作成 SortedList slist = new SortedList(); slist.Add( "加護亜依", 0 ); slist.Add( "松浦亜弥", 16 ); slist.Add( "後藤真希", 17 ); slist.Add( "藤本美貴", 0 ); slist.Add( "矢口真里", 20 ); // キーによる値の設定 slist["加護亜依"] = 15; // インデックスによる値の設定 slist.SetByIndex( 3, 18 ); // インデックスによるキーと値の取得 for ( int i = 0; i < slist.Count; i++ ) { Console.Write( slist.GetKey( i ) + " " ); Console.Write( slist.GetByIndex( i ).ToString() + "歳" ); Console.Write( Console.Out.NewLine ); } // インデックスの取得 Console.WriteLine( "Index of \"加護亜依\" is {0}", slist.IndexOfKey( "加護亜依" ) ); Console.WriteLine( "Index of 18-years-old girl is {0}", slist.IndexOfValue( 18 ) ); } } }
加護亜依 15歳 後藤真希 17歳 松浦亜弥 16歳 藤本美貴 18歳 矢口真里 20歳 Index of "加護亜依" is 0 Index of 18-years-old girl is 3 Press any key to continue
先ほど説明したとおり、キーとインデックスといずれの方法でも値を設定ることができます。 また、インデックスからキー・値を取得することができます。 さらに、キーまたは値からインデックスを取得することができます。 このようにHashtableやArrayListよりも柔軟にアイテムにアクセスすることができるという便利な反面、適切な場面で使わないと逆に混乱を招きそうです。 今回も使用しませんでしたが、SortedListにはキーによりアイテムを削除するRemove()メソッドと、インデックスにより削除するRemoveAt()メソッドの二種類が存在します。
BitArrayクラスは今までのクラスとは異なり、ビット値を扱います。 BitArrayでは、一つ一つのアイテムがboolean型からなり、各ビットを扱います。 これによりビットごとの計算などを容易に行えます。 次のサンプルはbyte型の配列とint型の配列からBitArrayのインスタンスを作成し、8ビットまたは32ビット毎にその値を出力させるものです。
using System; using System.Collections; namespace Collection { class Collection { [STAThread] static void Main(string[] args) { // byte型の配列を作成 byte [] bytArray = { 0, 1, 2, 3, 4 }; // BitArray のインスタンスを作成 BitArray barr = new BitArray( bytArray ); // 列挙 Console.WriteLine( "BitArray from byte-Array." ); EnumerateBitArray( barr, 8 ); // int型の配列を作成 int [] intArray = { 0x100, 0x200, 0x400, 0x800, 0x1000 }; // 新たな BitArray のインスタンスを作成 barr = new BitArray( intArray ); // 列挙 Console.WriteLine( "BitArray from int-Array." ); EnumerateBitArray( barr, 32 ); } // BitArrayを列挙 public static void EnumerateBitArray( BitArray barr, int length ) { for ( int i = 0; i < barr.Count; i++ ) { // true を 1、false を 0として出力 int iValue = barr[i] ? 1 : 0; Console.Write( iValue.ToString() ); // length桁表示したら改行 if ( i % length == length - 1 ) Console.Write( Console.Out.NewLine ); } } } }
BitArray from byte-Array. 00000000 10000000 01000000 11000000 00100000 BitArray from int-Array. 00000000100000000000000000000000 00000000010000000000000000000000 00000000001000000000000000000000 00000000000100000000000000000000 00000000000010000000000000000000 Press any key to continue
このように、byteないしint型の配列に格納された各値から一ビットずつ読み取り、BitArrayインスタンスが作成されます(コンストラクタの種類はいくつかありますが、ここでは説明しません)。 実行結果は出力処理の関係上、左が下位ビットになっていますが、各値のビット表記が成されています。 EnumerateBitArray()メソッドでは、BitArrayの各アイテムの値のtrueとfalseを0と1に置き換え、byte型では 1Byte-8Bit、int型では4Byte-32Bitなのでその桁数毎に改行するようにしています。
さらに、BitArrayはビット毎の配列化だけでなく、他のBitArrayとのビット毎の演算を行うこともできます。 そのサンプルが次のコードです。
using System; using System.Collections; namespace Collection { class Collection { [STAThread] static void Main(string[] args) { // byte型の配列を作成 byte [] bytArray1 = { 0x5d }; byte [] bytArray2 = { 0x7a }; // BitArray のインスタンスを作成 BitArray barr1 = new BitArray( bytArray1 ); BitArray barr2 = new BitArray( bytArray2 ); // 計算結果代入用 BitArray barrResult; // 列挙 Console.Write( "0x5d: " ); EnumerateBitArray( barr1, 8 ); Console.Write( "0x7a: " ); EnumerateBitArray( barr2, 8 ); // 各種演算 barrResult = (BitArray)barr1.Clone(); barrResult.Or( barr2 ); Console.WriteLine( "BitArray1 Or BitArray2" ); EnumerateBitArray( barrResult, 8 ); barrResult = (BitArray)barr1.Clone(); barrResult.And( barr2 ); Console.WriteLine( "BitArray1 And BitArray2" ); EnumerateBitArray( barrResult, 8 ); barrResult = (BitArray)barr1.Clone(); barrResult.Xor( barr2 ); Console.WriteLine( "BitArray1 Xor BitArray2" ); EnumerateBitArray( barrResult, 8 ); barrResult = (BitArray)barr1.Clone(); barrResult.Not(); Console.WriteLine( "Not BitArray1" ); EnumerateBitArray( barrResult, 8 ); } // BitArrayを列挙 public static void EnumerateBitArray( BitArray barr, int length ) { for ( int i = 0; i < barr.Count; i++ ) { // true を 1、false を 0として出力 int iValue = barr[i] ? 1 : 0; Console.Write( iValue.ToString() ); // length桁表示したら改行 if ( i % length == length - 1 ) Console.Write( Console.Out.NewLine ); } } } }
0x5d: 10111010 0x7a: 01011110 BitArray1 Or BitArray2 11111110 BitArray1 And BitArray2 00011010 BitArray1 Xor BitArray2 11100100 Not BitArray1 01000101 Press any key to continue
このように、数種の演算も容易にできてしまうのですが、C++プログラマなどビット演算に慣れている玄人さんからすれば、あまり価値の高い機能ではないかもしれません。
これから説明するStackとこの次で説明するQueueは、今までのコレクションクラスとは少し性格の異なるものです。 StackとQueueは ArrayListやHashtableなどとは異なり、コレクションにどのようなオブジェクトが格納されているかということより、どのような順番で格納されたかと言うことの方が重要視されます。 これらStackやQueueなどのデータ構造はC++などではお馴染みでSTLなどにも組み込まれているので、既に知っている方もいるかもしれません。 .NET FrameworkにおけるStackの概要を説明すると次のようなものです。
スタックは一見ArrayListなどのコレクションと同じ様に見えますが、アイテムの追加の仕方が違います。 スタックは後入れ先出し(LIFO: Last-In, First-Out)のデータ構造と言われます。 スタックにおいては、アイテムを追加することをPush、アイテムを取り出すことをPopといい、 Pushする際にはスタックの一番末尾に追加し、Popする際にはスタックの一番末尾から取り出します。 左の図がそのことを表しています。 イメージしづらい方は、本を一冊ずつ積み上げる作業がPush、積まれた本を一冊ずつ上から取っていく作業がPopに相当すると思って下さい。
.NET Frameworkでは、スタックは最初既定の容量(入れられるアイテムの最大数)をもっています。 これを越えてPushしようとすると、スタックの容量は拡充されます(拡充されずに、一番古いアイテムは破棄されるスタックもあります)。 現在何アイテムあるかを知るプロパティがCountです。 拡充された後、Popによってアイテム数が減っても、スタックの容量は減ることはありません。 また、.NET FrameworkのStackクラスにはPeek()というメソッドがあり、このメソッドはスタックからアイテムを取り出すことなく末尾のアイテムを参照するためのメソッドです。
では、実際にStackクラスを使ったサンプルを組んでみます。
using System; using System.Collections; namespace Collection { class Collection { [STAThread] static void Main(string[] args) { // Stack のインスタンスを作成 Stack st = new Stack(); // Stack に値をPush Console.WriteLine( "Pushing..." ); for ( int i = 0; i < 10; i++ ) { st.Push( i ); } // 末尾のアイテムを参照 Console.WriteLine( "Last item of stack is {0}", (int)st.Peek() ); // Stack から値がなくなるまでPop Console.WriteLine( "Popping..." ); while ( 0 < st.Count ) { Console.WriteLine( (int)st.Pop() ); } } } }
ushing... Last item of stack is 9 Popping... 9 8 7 6 5 4 3 2 1 0 Press any key to continue
このサンプルでは、スタックに0から9の値をPushし、すぐに追加した回数だけPopしています。 スタックの一番最後に入れたアイテムは一番始めに取り出されるという特性上、アイテムを入れた順番とは逆の順番で表示されています。
キューはスタックと似たような構造なのですが、アイテムの取り出し方が異なります。 スタックは後入れ先出しであったのに対し、キューは先入れ先出し (FIFO: First-In, First-Out)のデータ構造と言われます。 キューにおいては、アイテムを追加することをEnqueue、アイテムを取り出すことをDequeue といい、Enququeする際にはキューの一番末尾に追加し、Dequeueする際にはキューの一番先頭から取り出します。 左の図がそのことを表しています。 イメージしづらい方は、おいしいと評判の店の待ち行列を想像して下さい。 客が一人一人と並んでいく様がEnqueue、やっとのことで店に入り、満足のうちに店を去る様がDequeueに相当すると思って下さい。
.NET Frameworkでは、スタック同様キューも最初既定の容量をもっています。 これを越えてEnqueueしようとすると、キューの容量は拡充されます(これもスタック同様、拡充されずに、一番古いアイテムは破棄されるキューもあります)。 スタック同様にキューにもCountプロパティが存在します。 拡充された後、Dequeueによってアイテム数が減っても、キューのの容量は減ることはありません。 また、スタック同様Queueクラスにも Peek()というメソッドがあります。
では、実際にQueueクラスを使ったサンプルを組んでみます。
using System; using System.Collections; namespace Collection { class Collection { [STAThread] static void Main(string[] args) { // Queue のインスタンスを作成 Queue qu = new Queue(5); // Queue に値をEnqueue Console.WriteLine( "Enqueuing..." ); for ( int i = 0; i < 10; i++ ) { qu.Enqueue( i ); } // 末尾のアイテムを参照 Console.WriteLine( "Last item of queue is {0}", (int)qu.Peek() ); // Queue から値がなくなるまでDequeue Console.WriteLine( "Dequeuing..." ); while ( 0 < qu.Count ) { Console.WriteLine( (int)qu.Dequeue() ); } } } }
Enqueuing... Last item of queue is 0 Dequeuing... 0 1 2 3 4 5 6 7 8 9 Press any key to continue
このサンプルはスタックのサンプルと同様に、キューに0から9の値をEnqueueし、すぐに追加した回数だけDequeueしています。 キューの一番最初に入れたアイテムは一番始めに取り出されるという特性上、アイテムを入れた順番と同じ順番で表示されています。
このように、System.Collection名前空間には様々なコレクションクラスが存在するので、それぞれの特長を生かしてコーディングすることができます。 QueueやStackなどのデータ構造は有名なもので、いろいろなところで応用されています。 もっと詳しく知りたい方は調べてみると良いでしょう。