連載:C# 2.0入門

第2回 ジェネリック

株式会社ピーデー 川俣 晶
2007/06/29
Page1 Page2 Page3 Page4

制約の付いたジェネリックなクラス

 どのような型でも受け入れるジェネリックは過剰といえるほど強力である。そのような特徴は、あくまで入れ物に徹するコレクションでは有効だが、たいていのクラスでは持てあますことになるだろう。

 そこで、その過剰な強力さを制約して使いやすくする手段が用意されている。具体的には、型パラメータに「制約リスト」を付けることができるのである。制約リストには、クラスやインターフェイス、コンストラクタの制約を指定することができる。

 利用頻度は低いと思われる割に長くなるので、ここでは詳しい説明は割愛する。1つだけ実例を見てみよう。リスト12は、インターフェイス制約、コンストラクタ制約の付いたMyClass<T>クラスを宣言している。

using System;

// IDisposableインターフェイスを実装するSampleクラス
public class Sample : IDisposable
{
  public void Dispose()
  {
    Console.WriteLine("Disposed");
  }
}

// 制約の付いたMyClass<T>クラス
public class MyClass<T> where T : IDisposable, new()
{
  public T Value = new T();

  public void Close()
  {
    Value.Dispose();
  }
}

class Program
{
  static void Main(string[] args)
  {
    MyClass<Sample> sample = new MyClass<Sample>();
    sample.Close(); // 出力:Disposed
  }
}
リスト12 制約の付いたジェネリックなクラスの例

 このリストで制約を付けているのは、

where T : IDisposable, new()

という部分である。「where」はそれ以降が制約であることを示し、「T」が制約を付ける対象の型パラメータを意味する。その後に書かれたインターフェイス名の「IDisposable」は、型パラメータTにはIDisposableインターフェイスを実装した型しか指定できないことを制約する。続くカンマで区切られた「new()」は、型パラメータTの型に引数のないコンストラクタが存在することを制約する。

 この制約は、単に使い方に制限を加えるだけではない。MyClass<T>クラス内で、

Value.Dispose();

という(IDisposableインターフェイスの)Disposeメソッド呼び出しを記述できるのは、必ずIDisposableインターフェイスを実装しているという制約があればこそである。IDisposableインターフェイスの制約がなければ、TにDisposeメソッドが存在するか否かは予測不可能であり、コンパイル・エラーになる。

 また、

public T Value = new T();

を記述できるのは、コンストラクタ制約によってTには引数のないコンストラクタが存在すると制約したからである。もし、そのような制約がなければ、Tに引数のないコンストラクタがあるか否かは予測できず、コンパイル・エラーになる。

C++のtemplate機能との相違

 最後に、C++のtemplate機能との相違について触れておく。

 ジェネリックはtemplateではない。表面的に似ていても、実現するメカニズムはまったく異なっている。その相違を簡単に見てみよう。

 まず、C++のtemplate機能はコンパイル時にすべての仕事を完了する。

 例えば、以下のようなコードがあった場合、実行ファイルの中にはCMyClass1クラスを扱うCListクラスのコードと、CMyClass2を扱うCListクラスのコードの2つが書き込まれる。つまり、さまざまな型をtemplate機能で使えば使うほど、実行ファイルのサイズは大きくなる。

// C++
CList<CMyClass1,CMyClass1&> list1;
CList<CMyClass2,CMyClass2&> list2;

 一方、C#のジェネリックは、指定された型の種類に関係なく、コンパイル時には1つのクラスしか実行ファイルに書き込まれない。

 例えば、以下のようなコードがあっても、実行ファイルに書き込まれるListクラスは1つだけである。つまり、使用する型の種類が増えても実行ファイルのサイズがそれに比例して大きくなるわけではない(もし、このListクラスがクラス・ライブラリのListクラスなら、すでに用意されているのであらためて実行ファイルに書き込まれることはない)。

// C#
List<MyClass1> list1 = new List<MyClass1>();
List<MyClass2> list2 = new List<MyClass2>();

 そして、実行時に「ジェネリック型のインスタンス化」と呼ばれる処理が行われ、個々の型に対応する実体のある実行コードが生成される。

 ただし、個別の型に対して実体が生成されるのは値型だけで、参照型についてはたった1つの実体のみが生成される。これはList<string>クラスだろうと、List<System.Drawing.Image>クラスだろうと、List<System.Windows.Forms.Form>だろうと、すべてたった1つの実体で処理されることを意味する。

 なぜたった1つの実体でこれほど違うオブジェクトを処理できるのだろうか。その理由は、どれほどオブジェクトに違いがあっても、そのオブジェクトを指し示す「参照」はまったく同じ内部表現だからである。

 つまり、C++のtemplate機能と比較したとき、C#のジェネリックには以下の特徴があるといえる。

  • 実行ファイルのサイズがより小さくなる
  • 実行時に必要なメモリがより小さくなる

 だから恐れる必要はない。湯水のようにジェネリックを使って、より良いソース・コードを書こう。

次回予告

 実は筆者も油断していたのだが、ジェネリックは予想以上に多くの内容を持つ難物であった。このため今回は、ほんのさわりしか説明できなかった。

 しかし、たいていのC#プログラマーにはそれで十分だろう。なぜかといえば、ジェネリックの詳細まで使いこなす機会はそれほど多くはないからだ。たいていの場合、ジェネリック・コレクションを使いこなせれば十分であり、それが大きなパワーをもたらすだろう。

 さて、次回は反復子について説明する。C# 1.xで自作クラスを作るとき、foreach文で使用できる列挙機能を提供するようなクラスの記述はかなり面倒だった。しかし、それを大幅に楽にしてくれる新機能がC# 2.0には存在するのである。End of Article


 INDEX
  C# 2.0入門
  第2回 ジェネリック
    1.ジェネリックとは何か?/新しいコレクションの紹介
    2.ジェネリック・コレクションの使い方/ジェネリック・メソッドと型推論
    3.HashtableクラスとDictionaryクラスの非互換性/ジェネリックなクラスを自作する
  4.制約の付いたジェネリックなクラス/C++のtemplate機能との相違
 
インデックス・ページヘ  「C# 2.0入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間