[C#] 32. ジェネリックタイプ(Generic Type)を使い方


Study / C#    作成日付 : 2019/07/18 22:50:16   修正日付 : 2021/09/20 20:11:14

こんにちは。明月です。


この投稿はC#のジェネリックタイプ(Generic Type)を使い方に関する説明です。


我々がリスト(List)やディクショナリ(Dictionary)を使う時、どのデータタイプをリスト中で使うのかを括弧で設定します。

using System;
using System.Collections.Generic;

namespace Example
{
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // intタイプを扱うListのインスタンスを生成
      var list = new List<int>();
      // listにデータを入力
      list.Add(1);
      list.Add(2);
      // listの値を順番とおりに出力
      foreach (int node in list)
      {
        // コンソール出力
        Console.WriteLine(node);
      }
      // 任意のキーを押してください
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}


もし、設定したデータを使わなかったらエラーが発生します。


このようにどのデータタイプを使うのかを設定することがジェネリック(Generic)です。

つまり、クラスやメソッド中ではデータタイプを設定せず、インスタンスを生成する位置でデータタイプを設定して使う方法という意味です。

using System;
using System.Collections;

namespace Example
{
  // 連結リストアルゴリズム
  class List
  {
    // リストから使うNodeクラス
    private class Node
    {
      // データ
      public int Data { get; set; }
      // リストの次のポインタ
      public Node Next { get; set; }
    }
    // foreachを使うためにIEnumeratorインタフェースを継承して関数を再宣言
    private class Pointer : IEnumerator
    {
      // Listインスタンスを受け取ってポインタ管理
      private List list;
      // 現ポインタ
      private Node pointer;
      // コンストラクタ
      public Pointer(List list)
      {
        // インスタンスをメンバー変数から管理
        this.list = list;
        // ポインタを最初に移動
        this.pointer = list.start;
      }
      // 現在値を出力
      public object Current { get; set; }
      // Currentポインタの値があるか確認する関数
      public bool MoveNext()
      {
        // 無かったらfalseをリターンしてforeachを停止
        if (this.pointer == null)
        {
          return false;
        }
        // 現在値をCurrentに格納
        Current = this.pointer.Data;
        // ポインタを移動する。
        this.pointer = this.pointer.Next;
        // 現ポインタ値はあるのでtrue
        return true;
      }
      // ポインタリセット
      public void Reset()
      {
        this.pointer = this.list.start;
      }
    }
    // リストのポインタの頭
    private Node start = null;
    // リストのポインタの後
    private Node end = null;
    // foreachで使うpointer管理クラス
    private Pointer pointer;
    // コンストラクタ
    public List()
    {
      // pointer管理クラスのインスタンスを生成
      pointer = new Pointer(this);
    }
    // データ追加する。
    public void Add(int val)
    {
      // リストのポインタの頭が無かったら
      if (start == null)
      {
        // リストのポインタの頭にインスタンス生成
        start = new Node()
        {
          Data = val
        };
        // リストのポインタの後とリストのポインタの頭を一致する。
        end = start;
      }
      else
      {
        // リストのポインタの後に続けてNodeを連結
        end.Next = new Node()
        {
          Data = val
        };
      }
    }
    // foreachから使うIEnumeratorタイプをリターンする。
    public IEnumerator GetEnumerator()
    {
      // ポインタリセット
      pointer.Reset();
      // ポインタリターン
      return pointer;
    }
  }
  // 実行関数クラス
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // 任意で作ったリストのインスタンスを生成
      var list = new List();
      // intタイプのデータを入力
      list.Add(1);
      list.Add(2);
      // listの値を順番とおりに出力
      foreach (var item in list)
      {
        // コンソール出力
        Console.WriteLine(item);
      }
      // 任意のキーを押してください
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}


上の例は簡単な連結リストのアルゴリズムです。

実は連結リストのアルゴリズムは凄く簡単なアルゴリズムですが、foreachでデータを出力するためにインラインクラスのPointerを作りました。その理由で少し複雑になりました。


上の例のListクラスはintタイプだけ使えます。Add関数のパラメータとNodeクラスのDataのタイプをintタイプに設定したのでintだけ使えます。

しかし、状況によりintタイプではなくStringタイプのリストも作りたいです。

現在の状況では上のクラスでAdd関数とNodeクラスのデータタイプだけ変更してコピペするべきですね。そしてC#.Net frameworkで提供するクラスや原始データではなければ、使いたい時たびに作成しなければならないです。


でも、我々は実際のリスト(List)クラスを使う時にはそのように使いません。括弧(<>)を使ってデータタイプを設定して使います。

using System;
using System.Collections;

namespace Example
{
  // 連結リストアルゴリズム(データタイプをジェネリックで設定する。)
  class List <T>
  {
    // リストから使うNodeクラス
    private class Node
    {
      // データ - データタイプを決めなく、Listのジェネリックで設定したデータタイプで設定
      public T Data { get; set; }
      // リストの次のポインタ
      public Node Next { get; set; }

    }
    // foreachを使うためにIEnumeratorインタフェースを継承して関数を再宣言
    private class Pointer : IEnumerator
    {
      // Listインスタンスを受け取ってポインタ管理
      private List<T> list;
      // 現ポインタ
      private Node pointer;
      // コンストラクタ
      public Pointer(List<T> list)
      {
        // インスタンスをメンバー変数から管理
        this.list = list;
        // ポインタを最初に移動
        this.pointer = list.start;
      }
      // 現在値を出力
      public object Current { get; set; }
      // Currentポインタの値があるか確認する関数
      public bool MoveNext()
      {
        // 無かったらfalseをリターンしてforeachを停止
        if (this.pointer == null)
        {
          return false;
        }
        // 現在値をCurrentに格納
        Current = this.pointer.Data;
        // ポインタを移動する。
        this.pointer = this.pointer.Next;
        // 現ポインタ値はあるのでtrue
        return true;
      }
      // ポインタリセット
      public void Reset()
      {
        this.pointer = this.list.start;
      }
    }
    // リストのポインタの頭
    private Node start = null;
    // リストのポインタの後
    private Node end = null;
    // foreachで使うpointer管理クラス
    private Pointer pointer;
    // コンストラクタ
    public List()
    {
      // pointer管理クラスのインスタンスを生成
      pointer = new Pointer(this);
    }
    /// データ追加する。(パラメータタイプをジェネリックタイプに設定する。)
    public void Add(T val)
    {
      // リストのポインタの頭が無かったら
      if (start == null)
      {
        // リストのポインタの頭にインスタンス生成
        start = new Node()
        {
          Data = val
        };
        // リストのポインタの後とリストのポインタの頭を一致する。
        end = start;
      }
      else
      {
        // リストのポインタの後に続けてNodeを連結
        end.Next = new Node()
        {
          Data = val
        };
      }
    }
    // foreachから使うIEnumeratorタイプをリターンする。
    public IEnumerator GetEnumerator()
    {
      // ポインタリセット
      pointer.Reset();
      // ポインタリターン
      return pointer;
    }
  }
  // 実行関数クラス
  class Program
  {
    // 実行関数
    static void Main(string[] args)
    {
      // 任意で作ったリストのインスタンスを生成(List中で使うデータタイプはstringで設定)
      var list = new List<string>();
      // stringタイプのデータを入力
      list.Add("Node 1");
      list.Add("Node 2");
      // listの値を順番とおりに出力
      foreach (var item in list)
      {
        // コンソール出力
        Console.WriteLine(item);
      }
      // 任意のキーを押してください
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}


ジェネリックはデータタイプが設定されてないのでTという任意の文字で置換します。

つまり、インスタンスを生成する場所でintやStringで設定すればTという文字が全部intやStringタイプで変更すると思えば良いです。

そうことでデータタイプによりリストコードを作成する必要せずに、ジェネリックを使えばどのタイプでも対応ができるという意味です。


ジェネリックは基本的にクラスで宣言する方法があるし、メソッドだけ使う方法もあります。

using System;
using System.Collections.Generic;

namespace Example
{
  class Program
  {
    // リストを配列に変更する関数
    static T[] ConvertToArrayFromList<T>(List<T> list)
    {
      // 配列のインデクス
      int i = 0;
      // ジェネリックタイプを利用して配列を生成
      T[] ret = new T[list.Count];
      // Listを繰り返しを通って抽出
      foreach(var item in list)
      {
        // 配列に値を格納
        ret[i++] = item;
      }
      return ret;
    }
    // 実行関数
    static void Main(string[] args)
    {
      // stringタイプを扱うListのインスタンスを生成
      var list = new List<string>();
      // データを追加
      list.Add("Node 1");
      list.Add("Node 2");
      // Listを配列に変換
      var array = ConvertToArrayFromList<string>(list);
      // 配列の2つ目を出力する。
      Console.WriteLine(array[1]);
      // 任意のキーを押してください
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}


メソッドジェネリックは関数名の隣に設定します。


上の例では関数を呼び出す時に括弧(<>)を使いますが、実際にはパラメータにstring値を入れたら自動にジェネリックが設定されます。

なので関数ジェネリックを使う時は呼び出すときにジェネリックを設定しなくてもプログラムでエラーが発生しません。


ここまでC#のジェネリックタイプ(Generic Type)を使い方に関する説明でした。


ご不明なところや間違いところがあればコメントしてください。

#C#
最新投稿