1. ちょっとした歴史
昔々、C#でコレクションを扱うときは、配列か、いわゆる「浅い型付け」のコレクション――ArrayListみたいな、どんな型のオブジェクトでも突っ込めるやつ――しかなかったんだ。自由そうに見えるけど、そんなコレクションに文字列と整数を混ぜて入れたら、もうカオス!型チェックを毎回しないと安全に取り出せないし、しかもそれがうまくいくとも限らない。さらに、思わぬところで変なエラーが出たり、キャスト(型変換)で踊らされたりするんだよね。
非genericコレクション(ArrayList)の例:
using System.Collections;
ArrayList stuff = new ArrayList();
stuff.Add(42);
stuff.Add("C#へようこそ");
int number = (int)stuff[0]; // OK
string text = (string)stuff[1]; // OK
int fail = (int)stuff[1]; // ドカン!InvalidCastException(実行時エラー)
そう、コンパイラは何も言わない――エラーは実行時にしか出ない!冷蔵庫を開けたら熱い鍋が入ってた、みたいなサプライズだよ。
ジェネリックって何がいいの?
ジェネリックコレクション(Generics)は、コレクションの中身の型を保証できるように登場したんだ。これで3つの大きなメリットがあるよ:
- コンパイル時の型安全性。 コンパイラが、変なものをコレクションに入れようとしたら止めてくれる。
- 便利さ: 毎回手動で型変換しなくていい。
- パフォーマンス: 余計な"boxing/unboxing"(値型のラッピング・アンラッピング)に時間を使わなくて済む。
2. Generics(ジェネリック)ってそもそも何?
Genericsは、データ構造やメソッドをどんな型でも動くように書けるけど、ちゃんと型付けも守れる魔法みたいな仕組みだよ。
本や靴下、どっちにも使えるけど「一度に一つの型だけ」入れられる万能な箱を想像してみて。箱が「本専用」なら、誰も靴下を入れられない。genericコレクションも同じで、int用に作ったら、stringは絶対に入らないんだ。
ジェネリッククラスの宣言例
public class Box<T>
{
public T Value { get; set; }
}
var boxOfInt = new Box<int> { Value = 42 };
var boxOfString = new Box<string> { Value = "Genericsへようこそ!" };
Tは「型パラメータ」ってやつで、箱が何を入れるか指定するんだ。C#は、必ず何の型を入れるか明示しないとダメって言ってくるよ。
.NETのジェネリックコレクション
.NET Frameworkの現代的なコレクションは、ほぼ全部genericバージョンがあるよ。例えば:
- List<T> ― T型の動的リスト。
- Dictionary<TKey, TValue> ― キーと値のペアを持つ連想配列(辞書)。
- Queue<T>、Stack<T> ― キューやスタック。
- 他にもいろいろ。
3. どう動くの?Genericsの仕組み
型パラメータ
例えばList<int>みたいなコレクションを宣言すると、C#のコンパイラはint専用のバージョン(specialization)を作ってくれる。List<string>なら、今度は文字列専用のバージョンを作る、って感じ。
プログラマーから見たら、まるで魔法みたいに動くよ:
List<int> numbers = new List<int>();
numbers.Add(1); // intしか追加できない
List<string> words = new List<string>();
words.Add("hello"); // stringしか追加できない
違う型を追加しようとしたら、コンパイラがすぐ怒る:
numbers.Add("fail"); // コンパイル時エラー!
型安全性:コンパイル時 vs. 実行時
つまり、さっきみたいなエラーはプログラムを走らせる前(run-time)に防げるってこと。コレクションは超ガードされてて、コンパイラが番犬みたいに守ってくれるんだ。
裏側では
- 参照型(stringやobjectなど)は、もう余計な型変換がいらない。
- 値型(intやdoubleなど)は、"boxing/unboxing"問題が消える。
- .NETでgenericsを使っても、コードが無駄に増えることはない――CLRがJITコンパイル時に最適化してくれる。
4. Genericsでパフォーマンスが上がる理由
Genericsがコードにもたらす魔法のような効果をもう一度まとめるね:
型安全性(Type Safety):
これが一番大事かも。Genericsのおかげで、コンパイラが君のボディーガードになって、コレクションに「よそ者」を入れさせない。List<Product>には絶対Productしか入らないし、変な文字列や数字は入らない。これで、昔は実行時にしか分からなかった「爆発」(InvalidCastException)みたいなエラーを根絶できる。コードが信頼できて予測しやすくなるよ。
パフォーマンス(Performance):
さっきも言ったけど、値型(int、double、DateTimeなど)は、Genericsを使うとメモリやCPUの効率がめっちゃ良くなる。何百万個も要素があるコレクションや、頻繁に中身を変える場合は特に重要。みかんを箱から袋に移してまた戻す、みたいな無駄がなくて、最初から正しい箱に入れられる感じ。
コードの再利用性(Code Reusability):
Genericsなら、同じコードをいろんな型で使い回せるから、何度も書き直す必要がない。例えば、2つの変数を入れ替える関数が欲しいとき、GenericsなしだとSwapInt(ref int a, ref int b)、SwapString(ref string a, ref string b)、SwapProduct(ref Product a, ref Product b)…って全部書かないといけない。でもGenericsなら、Swap<T>(ref T a, ref T b)だけでOK。コレクションも同じで、IntList、StringList、ProductListなんていらなくて、List<T>一つで済む。コードがコンパクトで、メンテも楽、拡張もしやすいよ。
5. ジェネリックメソッドと自作genericクラス
ジェネリックメソッド
Genericsはコレクションだけじゃない!自分でどんな型にも使えるジェネリックメソッドを書けるんだ。これでコードがもっと汎用的になるよ。
//メソッド名の直後に型パラメータTを指定
public static void Swap<T>(ref T x, ref T y)
{
T temp = x;
x = y;
y = temp;
}
// 使い方:
int a = 10, b = 20;
Swap(ref a, ref b); // これでa == 20, b == 10
string one = "いち", two = "に";
Swap(ref one, ref two); // 文字列でも動く!
C#のコンパイラは、渡した変数の型から型パラメータを自動で推論してくれる。だから上の例みたいに、Swapメソッドに型を明示しなくてもOKなんだ。
自作genericクラス
自分だけの「箱」(genericクラス)も作れるよ。意外と簡単!
public class Pair<TFirst, TSecond>
{
public TFirst First { get; set; }
public TSecond Second { get; set; }
}
// 使い方:
var pair = new Pair<int, string> { First = 42, Second = "こたえ" };
6. genericコレクションの使い方例
前のレクチャーで作り始めた「やることリスト」(To-Do List)に戻ろう。前は配列に入れたり、画面に出すだけだったけど、今度はList<string>で動的に管理しよう。
例:やることをリストに動的追加
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> tasks = new List<string>();
Console.WriteLine("タスクを入力してね(空行で終了):");
string input;
while (!string.IsNullOrWhiteSpace(input = Console.ReadLine()))
{
tasks.Add(input);
Console.WriteLine("タスク追加!さらに入力(空行で終了):");
}
Console.WriteLine("\n今日のやることリスト:");
foreach (string task in tasks)
{
Console.WriteLine("- " + task);
}
}
}
ここがイイ!
- 新しいタスクを追加するたびにコレクションが自動で拡張される。
- 文字列以外をうっかり追加できない。
- リストを簡単にループして中身を表示できる。
7. 初心者がやりがちなミス・注意点・アドバイス
非genericコレクションからgenericに移ると、ちょっと混乱することもある。よくあるパターンを挙げておくね:
- 違う型の要素を追加しようとする(List<int> numbers = new List<int>(); numbers.Add("hi"); // エラー)。
- 一つのコレクションに色んな型を混ぜたくなる――その場合は基底型(例:List<object>)を使うしかないけど、毎回元の型に戻す必要がある。
- constraintsの存在を忘れて、広すぎるジェネリックを書いて、コンパイル時に変なエラーを食らう。
GO TO FULL VERSION