今回は「コレクションとジェネリック型」について解説します。
コレクションとは、複数のオブジェクトを一括して扱うための仕組みです。
特に、リスト型(List型)と辞書型(Dictionary型)はC#プログラミングにおいて頻繁に利用される基本的なコレクションです。
- リスト型:順序付けられた要素の集合を管理する。
- 辞書型:キーと要素(値)のペアの集合を管理する。
例えばユーザー情報を管理する場合、リストを使えば複数のユーザー情報を順番に保持できます。
そして、辞書を使えばユーザーIDとユーザー情報を関連付けて保持できます。
リストと辞書は以下のようなイメージです。
これらのリストや辞書などのコレクションはC#における「ジェネリック型」という仕組みで作られています。
ジェネリック型とは、型をパラメータとして取ることで、様々なデータ型に対応できるようにしたクラスやメソッドのことを指します。
これにより、リストや辞書は任意のデータ型のオブジェクトを扱うことができます。
例えば、「整数のリスト」や「文字列のリスト」を作成するといった場合に同じListクラスを使用できるわけです。
辞書でも同様に、キーと値の型をそれぞれ自由に定義できます。
例えば、ジェネリック型のリスト型を1つ用意しておけば、任意の型の要素をもつリスト型に対応できるため、ジェネリック型はコード再利用性のための強力な仕組みです。
本記事では、レストランメニューを扱うプログラムへメニュー追加・メニュー注文の機能を追加する演習を通して以下を学びます。
- 主要なコレクション型であるリスト・辞書の役立つ場面と基本的な使い方
- ジェネリック型の考え方
コレクションは、実用的なC#アプリケーションにおいて様々なデータの処理を行っていく上で非常に重要です。
また、ジェネリック型はリスト・辞書といったコレクションを理解する上で重要な概念となります。
YouTubeの動画もあります。
演習1:メニューを入力する機能を作る ~リスト~
元となるプログラム
まず演習1の土台となるプログラムとして、入門編でずっと使っているレストランメニューをHTMLで生成するプログラムを使います。
具体的には、名前空間・ファイル分割を行った入門編(11)の演習2コードを使います。
メニューとして、もともとは主菜、飲み物、デザートがあるのですが、今回は少し簡易化してデザートを除いたものをベースとします。
作る機能
これまでの演習で作ってきたレストランメニュー生成プログラムでは、メニューを以下のようにコードに直接記述していました。
//メニューデータを定義し、Menu配列に格納
Menu[] menus = new Menu[] {
new MainMenu("黒毛和牛ステーキ", "ジューシーで柔らかなステーキです。", 3000, false),
new MainMenu("ベジタブルカレー", "野菜をたっぷりと使った、スパイシーなカレーです。", 2400, true),
…
};
今回は、次の機能を作ってみましょう。
- ユーザがコンソール画面から新しくメニューを登録する機能
- 登録された順でメニュー一覧をHTMLコードとして出力する機能
完成したプログラムの実行イメージは次のようになります。
コンソールで新しいメニュー追加に関する質問が表示され、それに従ってユーザが必要な入力を行います。(赤枠がユーザ入力部分です)
コンソールで出力されたHTMLコードをブラウザで表示すると以下のようになります。
この例では1つですが、メニューの一覧が登録された順番で表示されます。
リスト型とは?配列型との違いは?
ユーザがメニューを次々に登録する機能を実現するには、複数のメニューを管理しておく仕組みが必要になりますね。
そこで、リスト型の登場です。リスト型は順序付けられた要素の集合を管理できます。
順序つけられた要素の集合を管理する型としては配列型もありましたね。
配列とリストには以下のような違いがあります。
- 配列型:長さが固定
- リスト型:長さが可変であり、要素の追加・削除が可能
以下のようなイメージです。
元のコードでも、以下のようにMenu型配列を使っています。
//メニューデータを定義し、Menu配列に格納
Menu[] menus = new Menu[] {
new MainMenu("黒毛和牛ステーキ", "ジューシーで柔らかなステーキです。", 3000, false),
…
};
//メニュー一覧をHTMLコードとして出力
MenuTableGenerator generator = new MenuTableGenerator(menus);
string tableHtml = generator.GenerateTable();
このコードでは、最初に全てのメニューデータを定義し、メニューの途中で追加・削除することがないため、固定長の配列を使っています。
今回の演習では、ユーザの入力によりメニューが次々に追加されるため可変長のリストを使います。
演習1コード
追加・修正部分
演習1コードは元のコードに対して以下の(1)~(4)を追加・修正します。
- (1) メニュー登録し、結果をHTMLコードとして出力
- (2) ユーザ入力によるメニュー登録
- (3) メニュー種別を表す列挙型
- (4) メニュー一覧のHTML全体のコードを生成
具体的なコードを見ながらリスト型やジェネリック型の考え方について説明します。
コード全体はこちらにあります。
(1) メニュー登録し、結果をHTMLコードとして出力
Programクラスは以下になります。
using Restaurant.Generators;
using Restaurant.Handlers;
using Restaurant.Menus;
namespace Restaurant
{
internal class Program
{
static void Main(string[] args)
{
//メニューの入力
MenuInputHandler menuInputHandler = new MenuInputHandler();
//★(a1) リスト型の変数を宣言
List<Menu> menuList = menuInputHandler.CollectMenuItemsFromConsole();
//メニュー表を生成
//★(b) リスト型を配列型に変換
MenuTableGenerator tableGenerator = new MenuTableGenerator(menuList.ToArray());
string tableHtml = tableGenerator.GenerateTable();
//メニュー表をHTMLページとして出力
MenuPageGenerator htmlGenerator = new MenuPageGenerator(tableHtml);
string html = htmlGenerator.GenerateHtml();
Console.WriteLine(html);
}
}
}
ここでのポイントは(a1),(b)の部分です。
リスト型を宣言
(a1)ではリスト型の変数menuListを宣言しています。
C#のリスト型は「List<T> 変数名;」で宣言できます。
List<T>のTは要素の型名です。
List<int>ならばint型のリスト、List<string>ならばstring型のリストを宣言できます。
今回はMenu型のリストを「List<Menu> menuList」と宣言しています。
このような、型をパラメータとして取ることで、様々なデータ型に対応できるようにしたクラスやメソッドのことを「ジェネリック型」といいます。
Listクラスはジェネリック型です。
リスト型のメソッドを使用
(b)ではリスト型の「ToArrayメソッド」を使い、次のようなリスト型から配列型への変換を行っています。
- List<Menu>型→Menu[]型
ToArrayメソッドも、きちんと要素がMenu型になっていますね。
Visual Studioを使っているならば、次のように自動補完するとそのことが確認できます。
もし、List<int>と宣言していたら、ToArrayメソッドは「int[] List<int>.ToArray()」として使えるわけです。
リスト型やジェネリック型の考え方について、リスト操作をいろいろと次の(2)でもう少し見てみましょう。
(2) ユーザ入力によるメニュー登録
MenuInputHandlerクラスのコードは以下になります。
using Restaurant.Helpers;
using Restaurant.Menus;
namespace Restaurant.Handlers
{
internal class MenuInputHandler
{
//コンソールからメニューを入力
public List<Menu> CollectMenuItemsFromConsole()
{
// ★(a2) リスト型の変数を宣言し、空のリストを生成して代入
List<Menu> menus = new List<Menu>();
while (true)
{
Console.WriteLine("新しいメニューを追加しますか?");
//メニューを追加するかどうかを入力
bool isAddingMenu = InputHelper.GetValidBoolean();
if (isAddingMenu)
{
//メニューを追加
AddNewMenuItem(menus);
//現在のメニュー一覧を表示
DisplayCurrentMenus(menus);
}
else
{
//メニュー追加を終了
break;
}
}
return menus;
}
//現在のメニュー一覧を表示
private void DisplayCurrentMenus(List<Menu> menus)
{
//★(c) リスト型の変数をforeachで処理
Console.WriteLine("現在追加されているメニュー名一覧:");
foreach (Menu menu in menus)
{
Console.WriteLine(menu.Name);
}
}
//新しいメニューを追加
private void AddNewMenuItem(List<Menu> menus)
{
//メニュータイプを入力
MenuType menuType = GetMenuType();
if (menuType == MenuType.Unknown)
{
Console.WriteLine("無効なメニュータイプです。");
return;
}
//メニュー名、説明、価格を入力
Console.WriteLine("メニュー名を入力してください:");
string name = InputHelper.GetValidString();
Console.WriteLine("説明を入力してください:");
string description = InputHelper.GetValidString();
Console.WriteLine("価格を入力してください:");
int price = InputHelper.GetValidInteger();
switch (menuType)
{
case MenuType.Main:
Console.WriteLine("ベジタリアン向けですか?");
//ベジタリアン向けかどうかを入力
bool isVegetarian = InputHelper.GetValidBoolean();
//主菜メニューを追加
// ★(d1) リスト型の変数に要素を追加
menus.Add(new MainMenu(name, description, price, isVegetarian));
break;
case MenuType.Drink:
Console.WriteLine("冷たい飲み物ですか?");
//冷たい飲み物かどうかを入力
bool isCold = InputHelper.GetValidBoolean();
//飲み物メニューを追加
// ★(d2) リスト型の変数に要素を追加
menus.Add(new DrinkMenu(name, description, price, isCold));
break;
}
}
//メニュータイプを入力
private MenuType GetMenuType()
{
Console.WriteLine("メニュータイプを選択してください:");
string? menuTypeInput = Console.ReadLine();
switch (menuTypeInput)
{
case "main":
return MenuType.Main;
case "drink":
return MenuType.Drink;
default:
return MenuType.Unknown;
}
}
}
}
ちょっと長いコードですが、今回は主にリスト型の使い方に焦点を当てて説明します。
CollectMenuItemsFromConsoleメソッドでは、(a2)でメニューのリストを作成してリスト型変数へ代入しています。
リスト型のインスタンス生成は「new List<要素の型名>();」で行います。
そして、whileループの中で以下のようにリストへのメニュー追加を行っています。
- メニューの追加の有無を確認し、無ければループを抜ける。
- コンソール入力で新しいメニューを受け付けてリストへ追加する。
(AddNewMenuItemメソッドが担当) - 現在追加されているメニューの一覧をコンソール上に表示する。
(DisplayCurrentMenusメソッドが担当)
whileループを抜けたら、作成したメニューリストを戻り値として返します。
この中で、リストを扱っているのは2番目(AddNewMenuItem)と3番目(DisplayCurrentMenus)です。それぞれ見てみましょう。
リストとforeach
DisplayCurrentMenusの(c)の部分で、メニュー名一覧をコンソールへ表示しています。
//現在のメニュー一覧を表示
private void DisplayCurrentMenus(List<Menu> menus)
{
//★(c) リスト型の変数をforeachで処理
Console.WriteLine("現在追加されているメニュー名一覧:");
foreach (Menu menu in menus)
{
Console.WriteLine(menu.Name);
}
}
配列型と同じように、「foreach(要素の型名 x in リスト型の変数){…}」とすることで、リストの各要素を参照できます。
リストへの要素追加
AddNewMenuItemの(d1)の部分で、リストへ要素を追加しています。
要素は追加された順番でリストへ格納されます。
//主菜メニューを追加
// ★(d1) リスト型の変数に要素を追加
menus.Add(new MainMenu(name, description, price, isVegetarian));
Addメソッドは「void List<Menu>.Add(Menu item)」というメソッドです。
ジェネリック型であるため、ToArrayメソッドと同様にきちんとMenu型に対応しています。
演習1では使っていませんが、リスト型ではRemoveなどの要素を取り除くメソッドも用意されています。
その他
以下のメソッドは、コンソール上でユーザからメニュー情報を受け付けるためのものです。
- InputMenuHandler.GetMenuType
- InputHelper.GetValidBoolean
- InputHelper.GetValidInteger
- InputHelper.GetValidString
GetValidBoolean・GetValidInteger・GetValidStringについては、InputHelperクラスを新たに作りそちらへ定義しました。
これらのメソッドは共通コードとして演習2のコードでも使いたいためです。
(3) メニュー種別を表す列挙型
メニュー種別を受け付けるときに使うため、メニュー種別を表すMenuTypeという列挙型を定義しています。
namespace Restaurant.Menus
{
//メニューの種類を表す列挙型
internal enum MenuType
{
Main,
Drink,
Unknown
}
}
列挙型は特定の値しかとらない型を表現するときに使います。
列挙型については、こちらの記事も参考にしてください。
(4) メニュー一覧のHTML全体のコードを生成
メニュー一覧のHTML全体のコードを生成する部分をクラスとしてまとめただけです。
こちらにコードがあります。
演習1のまとめ
リスト型の使い方と、ジェリック型の考え方について学びました。
リストについてはコレクションに関する記事のインデックス可能なコレクションも参考にしてください。
演習2:注文を入力する機能を作る ~辞書~
作る機能
演習1のコードを少し拡張して次のような機能を作ります。
- ユーザがコンソール画面から注文を登録する機能
注文は、演習1で作った機能でメニュー登録をした後に行います。
あらかじめ、「カレーライス」、「アイスコーヒー」が登録されている場合、完成したプログラムの実行イメージは次のようになります。
メニュー名を入力すると、注文数が1つ増えます。
そして、各メニューの現在の注文数が表示されます。
存在しないメニューが入力された場合は「存在しません」と表示されます。
辞書型とは?
メニューごとの注文数を管理する仕組みが必要になりますね。
辞書型を使うと、キーと要素(値)のペアの集合を管理できます。
メニューごとの注文数ならば例えば、
- キー:メニュー名
- 値:注文数
として、以下のように管理できます。
辞書型を使うと以下のようなことができます。
- キーと値のペアを追加・削除する
- キーを使って値の参照・書き換えを行う
- キーが存在するかどうかを確認する
注意点として、キーは一意(ユニーク)である必要があり、同じ名前のキーを複数登録することはできません。
演習2コード
追加・修正部分
演習2コードは演習1コードに対して以下の(1)・(2)を追加・修正します。
- (1) ユーザ入力によるメニュー登録を行い、その後に注文
- (2) 注文の受付
注文の受付でどのように辞書型を使うのかをみていきましょう。
コード全体はこちらにあります。
(1) ユーザ入力によるメニュー登録を行い、その後に注文
Programクラスは以下になります。
using Restaurant.Generators;
using Restaurant.Handlers;
using Restaurant.Menus;
namespace Restaurant
{
internal class Program
{
static void Main(string[] args)
{
//メニューの入力
MenuInputHandler menuInputHandler = new MenuInputHandler();
List<Menu> menuList = menuInputHandler.CollectMenuItemsFromConsole();
//注文の受付
OrderInputHandler orderHandler = new OrderInputHandler();
orderHandler.CollectOrdersFromConsole(menuList);
}
}
}
演習1のコードで作ったMenuInputHandlerクラスを使い、メニューリストを作成します。
そのメニューリストを受け取り、OrderInputHandlerクラスが注文受付を担当します。
(2) 注文の受付
OrderInputHandlerクラスは以下になります。ここで辞書型を使っています。
using Restaurant.Helpers;
using Restaurant.Menus;
namespace Restaurant.Handlers
{
internal class OrderInputHandler
{
//注文を受け付ける
public Dictionary<string, int> CollectOrdersFromConsole(List<Menu> menus)
{
//各メニューを登録し、注文数を0に初期化
//★(e) 辞書型の変数の宣言とインスタンス生成
Dictionary<string, int> orderCounts = new Dictionary<string, int>();
foreach (var menu in menus)
{
//★(f) キーと値のペアを追加
orderCounts[menu.Name] = 0;
}
Console.WriteLine("注文を開始します。終了するには 'exit' と入力してください。");
while (true)
{
Console.WriteLine("注文するメニュー名を入力してください:");
string menuName = InputHelper.GetValidString();
if (menuName == "exit")
{
break;
}
//★ (g)キーが存在するかを確認
else if (orderCounts.ContainsKey(menuName))
{//メニュー名が存在する場合、注文数を1増やす
//★(h) キーに対応する値を1増やす
orderCounts[menuName]++;
//現在の注文状況を表示
DisplayOrderCounts(orderCounts);
}
else
{
Console.WriteLine($"{menuName}は存在しません。もう一度入力してください。");
}
}
return orderCounts;
}
//注文状況を表示
private void DisplayOrderCounts(Dictionary<string, int> orderCounts)
{
Console.WriteLine("注文の集計:");
//それぞれのメニューの注文数を表示
//★(i) foreachでキーと値のペアを取り出す
foreach (KeyValuePair<string,int> orderCount in orderCounts)
{
Console.WriteLine($"{orderCount.Key}: {orderCount.Value}注文");
}
}
}
}
CollectOrdersFromConsoleメソッドで、whileループを使って注文を次々に受け付けています。
注文を1つ受けるたびに、DisplayOrderCountsメソッドで現在の注文状況を表示します。
辞書型をどのように使っているかに焦点を当て、ポイントを説明します。
辞書型の変数を宣言
OrderInputHandlerクラスの(e)の部分で、メニュー注文数を管理するために辞書型の変数を宣言し、辞書型のインスタンスを生成して代入しています。
//各メニューを登録し、注文数を0に初期化
//★(e) 辞書型の変数を宣言とインスタンス生成
Dictionary<string, int> orderCounts = new Dictionary<string, int>();
C#の辞書型は「Dictionary<K, V>変数名」で宣言できます。
インスタンス生成は「new Dictionary<K, V>()」です。
Kはキーの型名、Vが値の型名です。
今回は、キーがstring型(メニュー名)で値はint型(注文数)として、Dictionary<string, int>としています。
Dictionary型もList型と同じく、ジェネリック型です。
キーと値のペアを追加
(f)の部分でキーと値のペアを追加しています。
foreach (var menu in menus)
{
//★(f) キーと値のペアを追加
orderCounts[menu.Name] = 0;
}
「変数名[キー名]=値」とすることで、キーと値のペアが新しく追加できます。
登録されたメニューごとに注文数0(初期値)としてキーと値のペアを登録しています。
menusが「カレーライス、アイスコーヒー」だとすると、以下の辞書ができます。
キーの存在確認と値の参照
(g)ではContainsKeyメソッドで辞書にキー(メニュー名)が存在するかを確認します。
(h)では「変数名[キー名]」で値を参照して更新を行います。
//★ (g)キーが存在するかを確認
else if (orderCounts.ContainsKey(menuName))
{//メニュー名が存在する場合、注文数を1増やす
//★(h) キーに対応する値を1増やす
orderCounts[menuName]++;
//現在の注文状況を表示
DisplayOrderCounts(orderCounts);
}
入力されたメニュー名が存在するものであれば注文数を増やすという処理を行っています。
辞書とforeach
(i)では辞書のキーと値のペアをforeachで取り出しています。
//それぞれのメニューの注文数を表示
//★(i) foreachでキーと値のペアを取り出す
foreach (KeyValuePair<string,int> orderCount in orderCounts)
{
Console.WriteLine($"{orderCount.Key}: {orderCount.Value}注文");
}
「foreach(KeyValuePair<キーの型名、値の型名> x in 辞書型の変数){…}」とすることで、辞書型のキーと値のペアを取り出せます。
KeyValuePair型はKey・Valueプロパティでキー・値を保持しています。
KeyValuePair型もジェネリック型ですね。
演習2のまとめ
辞書型の使い方について学びました。
辞書については「コレクションに関する記事」における「キーと値のペアのコレクション」も参考にしてください。
講義1:コレクションとジェネリック型
コレクション型の様々なメソッド
リスト型や辞書型には、演習1・2で使ったもの以外にも便利なメソッドがあります。
ぜひいろいろと触って遊んでみてください。
例えば、リスト型ならば次のような機能があります。
- 指定したインデクスの要素を参照する
- 逆順に並べ替える
- 降順・昇順にソートする
- 指定した範囲を新たなリストとして取りだす
- …
リスト、辞書以外のコレクション型
C#にはリスト・辞書以外のコレクション型もあります。以下にいくつかを示します。
コレクション型 | 説明 | 特徴・用途 |
---|---|---|
List<T> | 可変長の一連の要素を保持する | インデックスでのアクセス、要素の追加・削除 |
Dictionary<K,V> | キーと値のペアを保持する | キーによる高速な要素の検索 |
HashSet<T> | 重複しない要素のコレクションを保持する | 重複なしのデータの追加・検索 |
Queue<T> | 先入れ先出し(FIFO)のデータ構造を持つ | データの追加(Enqueue)、取り出し(Dequeue) |
Stack<T> | 後入れ先出し(LIFO)のデータ構造を持つ | データの追加(Push)、取り出し(Pop) |
LinkedList<T> | 連結リストを実装する | 要素の追加・削除が中間でも高速 |
まずは、今回勉強したList、Dictionaryを使いこなすところから始めましょう。
他のコレクション型については必要になってから少しずつ使えるようになれば大丈夫です。
おまけ:今回書いたコードはオブジェクト指向的にどうか?
この章は、主題であるコレクション型とは関係のない話であるため、読み飛ばしてもらっても問題ありません。
今回の演習1,2で書いたコードは、実はあまり良い書き方ではありません。
(オブジェクト指向的な書き方ではありません)
例えば、演習2のコードでメニューとしてDessert型を追加すると以下のように複数箇所を修正する必要がでてきます。
MenuInputHandlerクラスを見ると、以下のように型に応じて条件分岐している部分がありますね。
switch (menuType)
{
case MenuType.Main:
…MeinMenu向けの処理…
break;
case MenuType.Drink:
…DrinkMenu向けの処理…
break;
}
ポリモーフィズムを使うと、このような条件分岐をなくすことができプログラムの変更容易性などが向上するという話を以前しました。
型の種類で条件分岐する処理は、ポリモーフィズムを使って書き直すと、コードを改善できる場合もあるでしょう。
演習1・2のコードも改善の余地があるのですが、今回はコレクション型の説明が主であり、コードの簡潔さを優先しました。
まとめ
今回は、C#における代表的なコレクション型であるリストと辞書について説明しました。
リストを使うと、プログラム実行中に自由に要素を追加・削除できる順序付きのデータ構造を扱えます。
辞書を使うと、キーと値のペアを管理することができ、キーに対応づけられた値を参照することが可能になります。
コレクションを使うことで、プログラム中で様々なデータの集合を定義し、それらに様々な処理を行うことが可能になります。
ジェネリック型の考え方についても学びました。
ジェネリック型のクラスは、型をパラメータとして取ることで様々なデータ型に対応できるため、とても再利用性の高い部品といえます。
クラス定義とコレクション型を使いこなせるようになれば、あらゆるデータ構造を自由に表現して処理できます
次回は例外処理について扱う予定です。