CodeGym /コース /C# SELF /Genericsコレクションのメリット

Genericsコレクションのメリット

C# SELF
レベル 27 , レッスン 1
使用可能

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)に防げるってこと。コレクションは超ガードされてて、コンパイラが番犬みたいに守ってくれるんだ。

裏側では

  • 参照型(stringobjectなど)は、もう余計な型変換がいらない。
  • 値型(intdoubleなど)は、"boxing/unboxing"問題が消える。
  • .NETでgenericsを使っても、コードが無駄に増えることはない――CLRがJITコンパイル時に最適化してくれる。

4. Genericsでパフォーマンスが上がる理由

Genericsがコードにもたらす魔法のような効果をもう一度まとめるね:

型安全性(Type Safety):

これが一番大事かも。Genericsのおかげで、コンパイラが君のボディーガードになって、コレクションに「よそ者」を入れさせない。List<Product>には絶対Productしか入らないし、変な文字列や数字は入らない。これで、昔は実行時にしか分からなかった「爆発」(InvalidCastException)みたいなエラーを根絶できる。コードが信頼できて予測しやすくなるよ。

パフォーマンス(Performance):

さっきも言ったけど、値型(intdoubleDateTimeなど)は、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。コレクションも同じで、IntListStringListProductListなんていらなくて、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の存在を忘れて、広すぎるジェネリックを書いて、コンパイル時に変なエラーを食らう。
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION