この記事はC#アドベントカレンダー 19日目の記事です。
C#入門書を読んだ、C#触っているけど業務経験での開発経験はない、Unityで動くものはつくれるようになってきたけどもう少しC#を上手く書きたい、といった人向けに、少しだけC#が上手くなるかも知れない知識を書いてみました。
はじめに
プログラミングの入門的な内容に関しては書籍が充実していたりWebにも記事があるので学びやすいです。その一方で、入門書を終えた後に何を学べばスキルアップしていけるのか、どうやったら業務経験者に近いコードが書けるようになるのかといった内容はまだまだ世の中に少ないなと感じています。(※)
情報が体系的にある程度の量まとめられる書籍と違ってWebの技術記事は一つ一つが小さいので、これだけ読めばスキルアップできる、業務経験者に近いコード書けるというのは難しいのですが、少なくともそのごくごく一部は伝えれるかなと思います。
ということで、自分が初心者のときにできていなかったと感じるポイントでもあり、そして初心者~中級者のコードをいくつか見てきた中でここができていれば初心者ぽさが減るなというポイントの中から一つ、あるテーマについてこの記事ではまとめようかなと思います。
そのテーマというのがタイトルにもなっているSystem.Collecitons.Generic
の使い方です。
(※)正確にいうとそういう情報自体はWebなどに存在してるのですが、入門者ゆえにそもそもどういうことを検索したらいいのかわからない、何を学べばいいのかわからない、Webの情報は大抵ばらばらの記事として存在していて順序立てて学ぶのが難しいという感じかなと思います。
System.Collections.Generic名前空間
まずは、この記事のテーマになっているSystem.Collecitons.Generic
名前空間について軽く説明しておきます。
順番に説明します。
名前空間(namespace)
System.Collecitons.Generic
の前にまずは名前空間について説明します。
名前空間はクラスを種類ごとに分けて管理するための機構です。
普通にPCを使っていれば、テキストファイルや画像ファイルをフォルダ分けして整理すると思いますが、それと同様にC#のプログラミングにおいてもクラスを整理する仕組みがあり、それが名前空間です。
もちろんクラスが書かれている各ソースコードファイルを、テキストファイルなどの通常のファイルのようにフォルダで整理することもできますが、それとは意味が異なります。各クラスは特定の名前空間に所属することができ、別の名前空間に所属しているクラスを参照(そのクラスの機能をつかったり、変数を読み取ったり)するときには、明示的にそれを示す必要があります。
コードの例を見てみましょう。
using System;
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
上のコードで名前空間が登場しているところは二か所です。
-
using System
: Systemという名前空間を利用することを宣言している -
namespace ConsoleApp1
:Program
クラスはCosnoleApp1
という名前空間に所属することを示している
他の名前空間(ここではProgram
クラスから見て他の名前空間という意味なので、ConsoleApp1
でない名前空間という意味)を参照するには、using
を使います。
System
名前空間をつかっているのは、文字をコンソールに出力するためのメソッドConsole.WriteLine()
が、System
名前空間に所属しているためです(正確にいうとConsole
クラスが所属しています)。試しに手元で同じコードを書き、using System
の行をコメントアウトしてみてください。VisualStudioなどのエディタを使っているなら、Console.WriteLine()
の部分にエラーがでると思います(多分"missing reference"みたいなエラーがでてくると思います。)
また、なぜ自分がコードを書いていないSystem
という名前空間が存在して使えるのかというと、それはC#側が標準で用意してくれているからです。System.Collections.Generic
も標準で用意されているものです。
名前空間についてもう少し知りたい場合は、ufcpp-名前空間を読んでください。
System.Collections.Generic
名前空間
上でも書いたようにSystem.Collections.Generic
というのもC#が標準で用意してくれている名前空間の一つです。通常、同じ名前空間に所属するのはある程度類似した(あるいは関連性の強い)クラス群です。
まず、System.Collections
名前空間(Genericがついていない)についてみてみましょう。Microsoftのドキュメントには以下のように書いてあります。
リスト、キュー、ビット配列、ハッシュ テーブル、ディクショナリなど、オブジェクトのさまざまなコレクションを定義するインターフェイスとクラスが含まれています。
System.Collections 名前空間 - Microsoft Docs
色々用語が並んでいますが、↓のコードのように入門書でもリストは使ったことがあるのではないでしょうか?(正確にいうとしたのコードのリストはSystem.Collections.Generic
の方に属していますが)
List<int> intList = new List<int>();
intList.Add(4);
キュー、ハッシュテーブルなどもリストと同じように、ある一種類のデータの塊を扱うためのクラスで、それがいわゆるコレクションと呼ばれるものたちです。そのコレクションが定義されているため、System.Collections
という名前なのです。
次にSystem.Collections.Generic
はどのようなクラスのまとまりになっているかというと、Microsoftのドキュメントには以下のように書いてあります。
ジェネリック コレクションを定義するインターフェイスとクラスが含まれています。このコレクションを使用することにより、ユーザーは、汎用的でない厳密に型指定されたコレクションに比べてタイプ セーフでパフォーマンスが高い、厳密に型指定されたコレクションを作成できるようになります。
System.Collections.Generic 名前空間 - Microsoft Docs
少し説明が難しいですが、要はSystem.Collections
にあるコレクション群のジェネリック版を提供してくれているということです。ジェネリックというのは、同じ機能を各型ごとにつくらずに使いまわすことができる機構です。
// <型名>の型名の部分を変えると、異なる型に対して同じList機能が使える。
// class IntList, class StringListのように型ごとにクラスを定義する必要がなく便利。
List<int> intList = new List<int>();
List<string> stringList = new List<string>();
ジェネリックがあまりよくわかっていない人は、ufcpp-ジェネリックを読むと良いでしょう。
ところ、ジェネリック版があるのに何故そうでないコレクション達もあるのでしょうか?ジェネリック版を使えばそうでないコレクションも表現できてしまうので不要に思えます。その理由は、C#には元々ジェネリックがなかったという歴史的な経緯から来ています。ジェネリックが登場したのはC#2.0のため、それ以前の時代と互換性を残すためにジェネリックでないコレクションたちもまだ残っているのです。なので、基本的にSystem.Collections
の方は使わないと思っていて大丈夫です。
[余談] - C#入門者はMicrosfotのドキュメントやufcppで調べものをするのがおすすめです
先ほどから記事の中でMicrosoftのドキュメントやufcppというサイトからたくさん引用していますが、C#入門者の方には、なにかC#でわからないことがあるときは、これらのサイトで情報を調べるのを強くおすすめします。
これらのサイト以外にも、Qiitaや個人のブログなど検索するとたくさんの技術記事がでてきますし、そういった記事の方がより噛みくだいた説明がされており、上記のサイトよりわかりやすいことも多いです。
しかし、それらの情報は正確性に欠けていたり、古かったり、あるいは順序だてて勉強していないと内容を誤解してしまうものが多くあります。もちろん有用な記事もたくさんありそれらを活かせるとより良いのですが、入門者の段階だとどれが有用化を見極めるのがそもそも難しいと思います。
もちろんそれらの記事を読んでもいいのですが、C#の基礎的な文法やルールなど大事なところはMicrosfotのドキュメントやufcppのサイトを読むことをおすすめします。これらのサイトの内容はかみ砕いて書かれている技術記事より、理解に少し時間がかかるかもしれませんが、正確な情報を体系立てて学びやすいので結局は遠回りせずにすみます。
以上余談でした。
配列、List以外のコレクション
ここまでの前置きが長くなりましたが、いよいよ本題のSystem.Collecitons.Generic
の使い方のTipsに入っていきます。まずは配列、List以外コレクションについて見ていきましょう。
おそらく、配列とListは入門書でもでてきたのではないでしょうか?
// 別々にしておくこともできる
int length1 = 20;
int length2 = 50;
int length3 = 100;
Console.WriteLine(length1);
Console.WriteLine(length2);
Console.WriteLine(length3);
// 関連のある数値はひとまとめにしておくと便利
int[] lengthArray = new int[3]{
20,
50,
100,
}
for(int i = 0; i < lengthArray.Length; i++){
Console.WriteLine(lengthArray[i]);
}
もちろん配列を使わなくても別々に変数を定義できますが、それは不便です。上の例なら、今は変数が3つだけですが、これが100個になった場合は100回Console.WriteLine()
を書かなくていけません。また、同じ配列に入ってる数字は何か同じ性質を持っているということがわかりやすくなります(上のコードだと長さを表す数字という性質)。
リストは配列と似ていますが、こちらは要素数を後から増やしたり減らしたりできます。
int[] intArray = new int[3];
intArray[1] = 0;
List<int> intList = new List<int>();
intList.Add(3);
intList.Add(4);
intList.Remove(3);
List<T>
のドキュメントはこれです。List クラス - Microsoft Docs
少し長いですが、このクラスの概要、使い方、持っているメソッド・変数が網羅されています。
そして、この配列やList以外にも実は色んな種類のコレクションがあり、それらはどのような目的でデータを格納したいかによって使い分けられます。この記事では全てのコレクションを紹介できませんが、いくつかメジャーかつ入門者でも理解しやすいものを紹介していきます。
HashSet
まずは、HashSet<T>
です。HashSet クラス - Mircrosfot Docs
ドキュメントによるこのクラスの説明はとてもシンプルです。
値のセットを表します。
ここで"セット"という言葉は、被りのない値の集合という意味です。
例えば、SetA = {0, 1, 2}はありますが、SetB={0, 1, 1}というはあり得ません。1が被っているからです。
コード例を示します。
(コードの中でusing部分は省略されています。using System.Collections.Generic
やusing System
が上に書かれていると思って読んでください。以下この記事中のコード全て同様です。)
void Main()
{
HashSet<string> cameToStore = new HashSet<string>();
cameToStore.Add("Bob");
cameToStore.Add("Calen");
cameToStore.Add("Hiroshi");
cameToStore.Add("Bob");
// Bob, Calen, Hiroshi
foreach (var name in cameToStore)
{
Console.WriteLine(name);
}
// true
Console.WriteLine(cameToStore.Contains("Bob"));
// false
Console.WriteLine(cameToStore.Contains("Masao"));
}
/*
実行結果(コンソールに表示されたもの)
Bob
Calen
Hiroshi
True
False
*/
まず、HashSet
クラスのインスタンス生成(ここではコレクションする対象をstring
型にしている)と、要素の追加を行っています。Bobは二回追加されています。
HashSet<string> cameToStore = new HashSet<string>();
cameToStore.Add("Bob");
cameToStore.Add("Calen");
cameToStore.Add("Hiroshi");
cameToStore.Add("Bob");
次にコレクションに含まれる名前を全て出力しています。foreachというのはコレクション系のクラス(正確にいうとIEnumerable
, IEnumerable<T>
インターフェースを継承しているクラス。)の要素を順番に取り出せる構文です。
foreach (var name in cameToStore)
{
Console.WriteLine(name);
}
/*
実行結果(コンソールに表示されたもの)。
Bob
Calen
Hiroshi
*/
先ほどBobは二回登録していましたが、コレクションの中には一つしかありません。これはセットの性質でだぶっているものは記録されないためです。
また、指定した要素が含まれているか確認するメソッド(HashSet<T>.Constains()
)が提供されています。
// true
Console.WriteLine(cameToStore.Contains("Bob"));
// false
Console.WriteLine(cameToStore.Contains("Masao"));
このコード例は、お店に来店したお客さんを記録する処理です。来店してくれた人のユニークな数と名前が重要で、同じ人が何回来たかが重要でないという想定があります。このようなものを記録するのに向いているのがHashSet<T>
クラスです。
ところで、同じようなことがList<T>
クラスでも可能です。
void Main()
{
List<string> cameToStoreList = new List<string>();
cameToStoreList.Add("Bob");
cameToStoreList.Add("Calen");
cameToStoreList.Add("Hiroshi");
cameToStoreList.Add("Bob");
foreach (var name in cameToStoreList)
{
Console.WriteLine(name);
}
Console.WriteLine(cameToStoreList.Contains("Bob"));
}
/*
実行結果(コンソールに表示されたもの)
Bob
Calen
Hiroshi
Bob
True
*/
要素を順番にも並べれるし、Contains()
メソッドも提供されています。
もちろんリストはセットと違い要素の被りがあるという違いがあります。
そして、実はコードの裏に隠れた違いとして、パフォーマンス(計算量、処理時間の長さ、メモリ効率など)の違いがあります。HashSet<T>
はデータ構造としてある要素が含まれているかを探すのに特化している分、その使い方に限ればよりList<T>
パフォーマンスが良いのです。(※)
入門書で書くようなプログラムは規模が小さいため、このようなパフォーマンスを意識しなくても問題ないことが多いのですが、業務でアプリケーションをつくる場合には、このような点が大事になってきます。そのため、「実現したい機能に対して適切なコレクションが選択できている = パフォーマンスを気にする視点を持っている」ということになり、業務経験者からみるとそれができているプログラムを書く人の方がより優秀に見えます(※2)。
なぜList<T>
とHashSet<T>
でパフォーマンスが異なるかの説明は長くなるためここではしませんし、入門者の段階であれば知らなくていいかなと(個人的には)思います。ただ、ざっくりとしたコレクションクラスの種類と、それらをどのような用途で使うべきかは把握しておくと良いでしょう。以下のドキュメントが参考になります。
何故パフォーマンスが違うのかについて深く知りたい方は「データ構造」「アルゴリズム」といったキーワードで調べてみたり、書籍を読むとよいでしょう。例えばufcpp - アルゴリズムとデータ構造とか。
(※)要素に特化しているからパフォーマンスがいいというのは正確な説明ではないです。ただ計算量の説明などをここに書くにと長くなるため、ここではわかりやすさを重視してこう書いています。
(※2)プログラムのどの部分も完璧に最適なコレクションを選択できている必要はありません。開発途中の機能を試作している段階ではパフォーマンスを無視してとりあえず動けばいい場面もありますし、最終成果でもそこまでパフォーマンスが重要でないとわかっている部分に対してはシビアに気にする必要ないと思います。
Queue, Stack
次はQueue<T>
, Stack<T>
です。Queue クラス - Mircrosfot Docs, Stack クラス - Mircrosfot Docs
Queueは追加した要素を追加した順に取り出せるコレクション、Stackは追加した要素を最後に追加したものから取り出せるコレクションです。
Queueのコード例から見ます。
void Main()
{
Queue<string> numbers = new Queue<string>();
numbers.Enqueue("one");
numbers.Enqueue("two");
numbers.Enqueue("three");
Console.WriteLine(numbers.Count());
Console.WriteLine(numbers.Dequeue());
Console.WriteLine(numbers.Dequeue());
Console.WriteLine(numbers.Dequeue());
Console.WriteLine(numbers.Count());
}
/*
実行結果(コンソールに表示されたもの)
3
one
two
three
0
*/
Enqueue()
で要素の追加、Dequeue()
で要素の取り出しを行います。追加した順に要素が取り出されています。
Stackのコード例も見ます。
void Main()
{
Stack<string> numbers = new Stack<string>();
numbers.Push("one");
numbers.Push("two");
numbers.Push("three");
Console.WriteLine(numbers.Count());
Console.WriteLine(numbers.Pop());
Console.WriteLine(numbers.Pop());
Console.WriteLine(numbers.Pop());
Console.WriteLine(numbers.Count());
}
/*
実行結果(コンソールに表示されたもの)
3
three
two
one
0
*/
Push()
で要素の追加、Pop()
で要素の取り出しを行います。Queueとは違い、後で追加されたものから順に取り出されています。
先ほどのHashSet<T>
と同様、Queue<T>
とStack<T>
どちらの処理もList<T>
でもやろうと思えば可能ですがパフォーマンス面でより優れるため、用途が適している場合にはQueueとStackを積極的に使えばいいでしょう。
また、何でもできるList<T>
に比べて機能が限定されているため、どのような意図で処理を行っているかがよりわかりやすくなります。例えば、Queueは重たい処理が同時に走らないようにいくつかの処理を待たせながら順番に実行していく場合などに使われます。またStackも様々なアルゴリズムで使われます。(すいません、Stackのコンパクトな例が思いつかなったです。この記事スタックとキューを極める! 〜 考え方と使い所を特集 〜でわかりやすく紹介されているので参考にしてください。)
このように、どういう意図で処理を行おうとしているかをわかりやすくする意識というのもまた業務経験者と入門者を分けているポイントだと思います。業務では複数人でプログラムを書いてアプリケーションをつくる場面が多いです。プログラムは、複数の書き方で同じ処理を書けることが多いため、何も考えずに書いてしまうとどのような意図があってこのような処理を書いているのかが他の人に伝わりにくくなってしまいます。もちろんコード内のコメントやドキュメントで補足もすることはできますが、そもそものプログラムコード自体で意図が明確になっていることが一番です。このように他の人に実装意図を伝える意識のあるコードでは、他の人がコードの内容を誤解しにくく、バグの発生確率を下げたり、理解を促して開発効率を上げたりするという効果があります。これは、業務ではとても重要視されるポイントなのです。
Dictionary (HashTable)
次はDictionary<TKey, TValue>
です。Dictionary クラス - MircroSoft Docs
ここまで紹介したクラスと違い、TKey, TValueという二つを元に宣言されます。
現実世界の辞書では、ある用語の名前をもとに、その用語の説明を調べることができます。ここで、"用語の名前"がKey, "用語の説明"がValueに相当します。
コード例を見てみましょう。
void Main()
{
Dictionary<string, int> ageDict = new Dictionary<string, int>();
ageDict.Add("Bob", 20);
ageDict.Add("Caren", 32);
ageDict.Add("Hiroshi", 99);
Console.WriteLine(ageDict["Bob"]);
foreach(var nameAgePair in ageDict){
Console.WriteLine($"{nameAgePair.Key}-{nameAgePair.Value}");
}
// 同じKeyを再度Addしようとするとエラーがでる
// ageDict.Add("Bob", 45);
// 更新するときはこの書き方
ageDict["Bob"] = 45;
Console.WriteLine(ageDict["Bob"]);
}
/*
実行結果(コンソールに表示されたもの)
20
Bob-20
Caren-32
Hiroshi-99
45
*/
string型をKey, int型をValueとするDictionaryを定義しています。現実の辞書はKeyもValueも文字列(文章)ですが、プログラミングではKeyもValueも文字列である必要がありません。ここでは人物の名前を元に、その人の年齢を調べれるDictionaryを定義しています。
-
Add()
メソッドではKey, Valueを指定して追加します。 - foreach文ではKeyとValueのペアを順番にとりだすことができます。(実はこの順番は保証されていませんが)
- 同じKeyに対するValueを上書きすることもできます。
このDictionary<T>
の動きもList<T
を2つ使えば似たようなことが可能です。ただやはりパフォーマンスが悪いです。検索機能をつくりたいだけなら、それに特化したDictionaryを使うのが良いでしょう。
ちなみに、Dictionary<TKey, TValue>
の非ジェネリック版はHashtable
クラスです。プログラミング言語によらない一般的なデータ構造の名前としては、こちらのハッシュセットという名前が一般的です。Hashtable クラス - Microsfot Docs
その他のコレクション
HashSet, Queue, Stack, Dicrionaryを紹介しましたが、System.Collections.Generic
にはまだ他のコレクションクラスが有ります。余力があれば調べてみてください。
また入門段階では必要ないと思いますがいずれ以下の名前空間にあるコレクションについても知っていく必要があるでしょう。いずれ必要になったときに調べてみてください。
-
System.Collections.Concurrent
:非同期処理、マルチスレッド処理を行う際に用いるコレクション群が所属する名前空間です。System.Collections.Concurrent 名前空間 - Microsoft Docs -
System.Collections.Immutable
:変更不可能コレクション群が所属する名前空間です。System.Collections.Immutable 名前空間
配列、List以外のコレクション、まとめ
配列、List以外のいくつかのコレクションについて説明してみました。
- 各コレクションには想定されている用途があり、適したものを選ぶとパフォーマンスが良くなること
- 各コレクションには想定されている用途があり、他のメンバーに実装の意図を伝えやすくなること
- そして、これらができていることでプログラミングスキルに対する評価があがること
を確認しました。
この記事ではC#を例に説明していますが、実はコレクションというのは現代の多くのプログラミング言語で共通の考え方です。実際、クラス名やメソッド名は違えど同じ機能をもったものが各種言語で用意されています。そのため、コレクションについて理解を深めるというのはとても重要なのです。
(情報工学的にいうと、"コレクション"より"データ構造"という言葉で説明されることが多いと思います。)
実はこの記事で他にも書きたい内容があったのですが、長くなってきたので別の記事にわけたいと思います。そちらが書けたらここにリンクを貼るようにします。今度は主にSystem.Collections.Generic
のインタフェースを中心に記事を各予定です。
おわりに
今回は脱初心者C#perと題して、C#が少し上手くなるかもしれない知識をSystem.Collections.Generic
というテーマでまとめてみました。入門者の人だと一度読んだだけでは理解できないものも多いと思いますが、何回か読み直したり、コードを書いて動かしてみたり、他の人の解説記事を読んでみるなどすると、理解できていくと思います。
この記事に書ききれなかったものとして、別の記事に主にSystem.Collections.Generic
のインタフェースをベースにまとめたいなと思っています。
その他参考になるリンク
この記事内容からもっと発展した内容を学びたい人向けに、いくつか記事のリンクを貼っておきます。
-
.NET のコレクションついて再確認する
- .NET(C#は.NETの言語の一つ)のコレクションについて、ここに書いたより広い内容をざっとまとめてくれています。
-
基礎から学ぶLINQの仕組み
- データ処理を行う上で便利な機能LINQを説明する過程で、Generics、拡張メソッド、デリゲート、
IEnumerable
,IEnumrabe<T>
インターフェース、イテレータ構文というC#でよく使われる機構や構文をわかりやすく順番に説明してくれています。
- データ処理を行う上で便利な機能LINQを説明する過程で、Generics、拡張メソッド、デリゲート、