基礎から学ぶC#入門 90日コース | 実践力を高める - Day 65:ジェネリック

Web APP 90日で身につけるC#
スポンサーリンク
スポンサーリンク

Day 65 のゴールと全体像

Day 65 では、C# の中でも“ちょっとカッコいい”機能である ジェネリック(Generics) に入門していきます。 今日のキーワードは次の3つです。

  • T
  • 型安全
  • ジェネリックメソッド

ジェネリックは、ひと言でいうと

「型をあとから差し替えられる“型のテンプレート”」

のような仕組みです。 少し抽象的に聞こえるかもしれませんが、 実はみなさんがすでに使っている List<int>Dictionary<string, int> などが、 まさにジェネリックの代表例です。

ここでは、

  • 「T って何者?」というところから始めて
  • 型安全という考え方をかみ砕いて説明し
  • ジェネリックメソッドを自分で書いてみる

ところまで、ステップバイステップで進めていきます。

ジェネリックとは何かをイメージでつかむ

「型のテンプレート」という考え方

ジェネリックは、

「中身の型だけをあとから決められるクラスやメソッド」

を作るための仕組みです。

たとえば、

  • List<int>
  • List<string>

は、どちらも「リスト」という同じ仕組みですが、 中に入っている型だけが違いますよね。

この「仕組みは同じだけど、中身の型だけ変えたい」というときに、 ジェネリックが大活躍します。

すでに使っているジェネリックの例

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // int 型のリスト
        List<int> numbers = new List<int>();
        numbers.Add(10);
        numbers.Add(20);

        // string 型のリスト
        List<string> names = new List<string>();
        names.Add("Alice");
        names.Add("Bob");

        Console.WriteLine($"numbers[0] = {numbers[0]}");
        Console.WriteLine($"names[0] = {names[0]}");
    }
}
C#

ここで使っている List<int>List<string> は、

  • 「List というジェネリッククラス」
  • <int><string> で“型のパラメータ”を指定している」

という形になっています。

T とは何者なのか

T は「型の名前の仮置き」

ジェネリックの説明でよく出てくる T は、

「ここには、あとで具体的な型が入りますよ」という“仮の型名”

です。

たとえば、次のようなクラスを考えてみます。

// T という「型パラメータ」を持つジェネリッククラス
class Box<T>
{
    // T 型の値をひとつ持つ
    public T Value { get; set; }
}
C#

この Box<T> は、

  • 「中に何かひとつ値を入れておける箱」
  • 「その“何か”の型は、あとで決める」

というクラスです。

T に具体的な型を入れて使ってみる

using System;

class Program
{
    static void Main()
    {
        // int を入れる箱
        Box<int> intBox = new Box<int>();
        intBox.Value = 123;
        Console.WriteLine($"intBox の中身: {intBox.Value}");

        // string を入れる箱
        Box<string> stringBox = new Box<string>();
        stringBox.Value = "こんにちは";
        Console.WriteLine($"stringBox の中身: {stringBox.Value}");
    }
}

// ジェネリッククラスの定義
class Box<T>
{
    public T Value { get; set; } // T 型のプロパティ
}
C#

ここでのポイントは、

  • Box<int>Box<string> は、同じクラスの“型違いバージョン”
  • T のところに intstring を差し込んで使っている

というところです。

T は、

「ここには、あなたが好きな型を入れてくださいね」

という“型の穴”のようなものだとイメージしてもらうと、 少し親しみやすくなると思います。

型安全とは何か

「型が決まっているから、間違いに気づける」

型安全(型の安全性)という言葉は、 少し堅く聞こえますが、意味としてはシンプルです。

「変数やメソッドの型がきちんと決まっていることで、 間違った使い方をコンパイル時に防げる状態」

のことです。

ジェネリックは、この「型安全」を保ちながら、 柔軟に型を差し替えられるところが大きな魅力です。

型安全でない書き方の例

ジェネリックがない世界を想像してみましょう。 たとえば、何でも入れられる箱を object で作ってみます。

class ObjectBox
{
    public object Value { get; set; }
}
C#

これを使うと、

using System;

class Program
{
    static void Main()
    {
        ObjectBox box = new ObjectBox();

        // int を入れる
        box.Value = 123;

        // 取り出すときに、int だと信じてキャストする
        int number = (int)box.Value;
        Console.WriteLine($"number = {number}");

        // string を入れる
        box.Value = "こんにちは";

        // でも、うっかり int として取り出そうとすると…
        int wrong = (int)box.Value; // 実行時に例外が発生する
    }
}
C#

このコードは、コンパイルは通りますが、

  • box.Value"こんにちは" が入っているのに
  • int としてキャストしようとしている

ため、実行時に InvalidCastException が発生します。

つまり、

  • 「コンパイル時には気づけない」
  • 「実行してみて初めて問題が表面化する」

という状態になってしまいます。

ジェネリックで型安全にする

同じような箱を、ジェネリックで書き直してみます。

class Box<T>
{
    public T Value { get; set; }
}
C#

これを使うと、

using System;

class Program
{
    static void Main()
    {
        // int 専用の箱
        Box<int> intBox = new Box<int>();
        intBox.Value = 123;

        int number = intBox.Value; // キャスト不要で安全
        Console.WriteLine($"number = {number}");

        // string 専用の箱
        Box<string> stringBox = new Box<string>();
        stringBox.Value = "こんにちは";

        string text = stringBox.Value;
        Console.WriteLine($"text = {text}");

        // 間違った代入はコンパイル時に防がれる
        // intBox.Value = "文字列"; // コンパイルエラーになる
    }
}
C#

ここでは、

  • Box<int> は「int 専用の箱」
  • Box<string> は「string 専用の箱」

になっているため、

  • Box<int> に文字列を入れようとすると、コンパイルエラー
  • Box<string> に数値を入れようとすると、コンパイルエラー

になります。

これが 型安全 です。

「間違った型を入れようとした瞬間に、コンパイラが止めてくれる」

という状態を作ることで、 実行時の予期せぬエラーを減らすことができます。

ジェネリックメソッドとは

「型をあとから決めるメソッド」

ジェネリックは、クラスだけでなく メソッド にも使えます。

「このメソッドは、どんな型に対しても同じような処理をしたい」

というときに、ジェネリックメソッドが活躍します。

いちばんシンプルなジェネリックメソッド

using System;

class Program
{
    static void Main()
    {
        // int を表示
        PrintValue<int>(123);

        // string を表示
        PrintValue<string>("こんにちは");

        // double を表示
        PrintValue<double>(3.14);
    }

    // T 型の値を受け取って表示するジェネリックメソッド
    static void PrintValue<T>(T value)
    {
        Console.WriteLine($"値: {value}");
    }
}
C#

ここでのポイントは、

  • メソッド名の前に <T> が付いている
  • 引数の型が T になっている
  • 呼び出し側で PrintValue<int>(123) のように、 具体的な型を指定して呼び出している

というところです。

型推論を使って、もっとシンプルに書く

C# では、呼び出し側で型を明示しなくても、 コンパイラが「渡された値の型」から T を推論してくれます。

using System;

class Program
{
    static void Main()
    {
        // 型を明示しなくても OK
        PrintValue(123);          // T は int と推論される
        PrintValue("こんにちは"); // T は string と推論される
        PrintValue(3.14);         // T は double と推論される
    }

    static void PrintValue<T>(T value)
    {
        Console.WriteLine($"値: {value}");
    }
}
C#

このように、

  • ジェネリックメソッドは「型をあとから決めるメソッド」
  • 呼び出し側で型を指定してもいいし、 コンパイラに推論してもらってもいい

という柔軟な使い方ができます。

ジェネリックメソッドの実践的な例

例:2つの値を入れ替える Swap メソッド

「2つの変数の値を入れ替える」という処理は、 どんな型でも同じように書けますよね。

これをジェネリックメソッドで書いてみます。

using System;

class Program
{
    static void Main()
    {
        int a = 10;
        int b = 20;

        Console.WriteLine($"入れ替え前: a = {a}, b = {b}");
        Swap(ref a, ref b);
        Console.WriteLine($"入れ替え後: a = {a}, b = {b}");

        string s1 = "Hello";
        string s2 = "World";

        Console.WriteLine($"入れ替え前: s1 = {s1}, s2 = {s2}");
        Swap(ref s1, ref s2);
        Console.WriteLine($"入れ替え後: s1 = {s1}, s2 = {s2}");
    }

    // どんな型でも入れ替えられるジェネリックメソッド
    static void Swap<T>(ref T x, ref T y)
    {
        T temp = x;
        x = y;
        y = temp;
    }
}
C#

ここでのポイントは、

  • Swap<T> というジェネリックメソッドを定義している
  • ref T x, ref T y で「同じ型の2つの変数」を受け取っている
  • 中身の処理は、型に依存しない「入れ替え」のロジック

というところです。

int でも string でも、 同じ Swap メソッドを使って入れ替えができています。

例:配列の要素をすべて表示するメソッド

using System;

class Program
{
    static void Main()
    {
        int[] numbers = { 1, 2, 3 };
        string[] words = { "apple", "banana", "cherry" };

        PrintArray(numbers);
        PrintArray(words);
    }

    // どんな型の配列でも表示できるジェネリックメソッド
    static void PrintArray<T>(T[] array)
    {
        Console.WriteLine("配列の中身:");

        foreach (T item in array)
        {
            Console.WriteLine($" - {item}");
        }
    }
}
C#

この PrintArray<T> は、

  • int[] にも
  • string[] にも
  • double[] にも

同じように使える、汎用的なメソッドになっています。

ジェネリックのミニテンプレート

最後に、今日の内容をそのまま使える形のテンプレートとしてまとめておきます。

ジェネリッククラスの基本形

// T という型パラメータを持つジェネリッククラス
class Box<T>
{
    // T 型の値をひとつ持つプロパティ
    public T Value { get; set; }
}
C#

ジェネリックメソッドの基本形

// T 型の値を受け取って何かするジェネリックメソッド
static void DoSomething<T>(T value)
{
    Console.WriteLine($"値: {value}");
}
C#

呼び出し側の基本形

static void Main()
{
    // ジェネリッククラス
    Box<int> intBox = new Box<int>();
    intBox.Value = 100;

    Box<string> stringBox = new Box<string>();
    stringBox.Value = "Hello";

    // ジェネリックメソッド
    DoSomething(123);          // T は int と推論される
    DoSomething("こんにちは"); // T は string と推論される
}
C#

Day 65 のまとめ

Day 65 では、

  • ジェネリックとは「型をあとから差し込めるテンプレート」であること
  • T は「ここに好きな型を入れてください」という“型の穴”であること
  • 型安全とは「間違った型をコンパイル時に防げる状態」であること
  • ジェネリックメソッドで「どんな型にも共通する処理」を書けること

を、例題コードとともに見ていきました。

ジェネリックは、最初は少し抽象的に感じるかもしれませんが、

  • Box<T> のようなシンプルなクラスを自分で書いてみる
  • PrintValue<T>Swap<T> のようなジェネリックメソッドを試してみる
  • すでに使っている List<T>Dictionary<TKey, TValue> を「ジェネリックの仲間」として意識してみる

といった小さなステップを重ねていくうちに、 「型をうまく扱う感覚」が少しずつ育っていきます。

ぜひ、ジェネリックを

「コードをキレイに、そして安全にしてくれる頼れる相棒」

として、今後のプログラミングに活かしていっていただければうれしいです。

タイトルとURLをコピーしました