更新日: 2010 年 4 月 9 日

執筆者: エディフィストラーニング株式会社 矢嶋 聡

この記事は、「MSDN プログラミング シリーズ」として発行している技術書籍「ステップアップ Visual Basic 2010 ~開発者がもう一歩上達するための必読アドバイス」(日経 BP 社刊) を基に先進的なテクニックを紹介しています。


目次

  1. はじめに
  2. ジェネリック登場の背景
  3. ジェネリックとは
  4. ジェネリックの制約
  5. 制約の留意点

1. はじめに

Visual Basic 2005 の時点で導入された「ジェネリック」は、それ自身が単独で用いられることもありますが、いまや他の構文やテクノロジーと関連して多数使用されています。

例えば、第 8 回 の For Each 文の説明の際にも IEnumerable (Of T) というジェネリック インターフェイスが登場しました。また、Visual Basic 2008 で導入された LINQ もジェネリックが関連してきます。第 9 回 に引用した以下の LINQ のサンプル コードでは、「(Of Object)」の部分が「ジェネリック」の構文にあたります。

例 1. 第 9 回で引用した LINQ のサンプルコード、ジェネリックも使用

Dim query = col.Cast(Of Object)().Where(Function(d) d > 10)

ジェネリックの典型的な例は、このサンプル コードで使用されている Where メソッドの定義です。クラス ライブラリには、Where メソッドが次のように定義されています。(ここでは、このメソッド定義の意味を正確に理解する必要はありません。)

例 2. Where メソッドの定義、様々なジェネリックが登場する

<ExtensionAttribute> _
Public Shared Function Where(Of TSource) ( _
source As IEnumerable(Of TSource), _
predicate As Func(Of TSource, Boolean) _
) As IEnumerable(Of TSource)

このメソッドの定義では、太字部分がジェネリックに関係する構文であり、様々なジェネリックのバリエーションが見受けられます。この Where メソッドを正しく使いこなすには、これらのジェネリックの意味を正しく理解しておく必要があります。

今後、Visual Basic 2010 を使用する際にも、このようなジェネリックが登場する場面は多くあるはずです。そこで今回は、2 回に分けて、「ジェネリック」の基本的な定義からバリエーションなど、いつくかの重要なポイントを確認していきます。Visual Basic 2010 を活用する準備としても、今のうちにしっかり「ジェネリック」を理解しておきましょう。

ページのトップへ


2. ジェネリック登場の背景

ジェネリックを理解するために、まずは、その必要性から考えてみます。以下のコードは、要素の集合を管理するコレクションのクラス定義です。(ここでは説明の都合上、メソッド内の具体的な実装を省略しています。)

例 3.簡単なコレクション

Public Class MyCollection

Private dt() As Object      ←[1]

Public Sub Add(ByVal val As Object) ←[2]
'引数に指定された値を配列に格納する
End Sub

End Class

ここで留意すべき点は、太字部分です。[1] では、このコレクションで要素集合を管理するために、Object 型の配列を使用しています。また、[2] の要素追加の Add メソッドでは、追加するデータを Object 型の引数として受け取っています。このように、コレクションの対象となるデータ型を汎用的な Object 型にしておけば、Integer 型要素の集合や String 型要素の集合など、様々な種類のデータの集合を扱えます。

しかし、これには問題点もあります。Object 型が汎用的であるということは、その反面、型を限定できないので、1 つのコレクションの中に、Integer 型や String 型などの異なるデータ型の要素が混在できてしまいます。例えば、このコレクションの利用者が、Integer 型の要素のコレクションとして利用していた際に、誤って他のデータ型のデータ (Char 型など) を追加しても、コンパイラはそれをエラーとして検出できないのです。以下の例では、Integer 型の値を追加するつもりが、[1] では日付型データを追加していますが、これも正しい処理としてコンパイラは認識してしまいます。

例 4. コレクションへの値の追加

Visual Basic
Dim col As New MyCollection()
col.Add(100)
col.Add(200)
col.Add( #4/10/2010# )	←[1]
col.Add(300)
 

このような予定外のデータ型の混在を認めなくする方法としては、そもそも例 3 のコレクションの太字の型の部分を Integer 型として定義すればよいのですが、それでは、Integer 型のコレクション クラス、String 型のコレクション クラスというように、データの種類の数だけ、コレクションの定義の数が増えてしまい手間がかかります。

Object 型のような汎用的な面も生かしつつ、実際に使用する際には、特定の型に限定する方法はないでしょうか? それに答えることができるのが、ここで紹介するジェネリックです。

ページのトップへ


3. ジェネリックとは

「ジェネリック」とは、本来は「型」をハード コーディングすべき箇所をパラメーター化して、後から特定の型を当てはめることができる仕組みです。例えば、前述の例 3 のコレクション クラスに対して、ジェネリックを使用して書き換えると、次のようになります。

例 5. ジェネリックに対応したコレクション

Public Class MyCollection(Of T) ←[1]

Private dt() As T                   ←[2]

Public Sub Add(ByVal val As T)      ←[3]
'引数に指定された値を配列に追加する
End Sub

End Class

例 3 でハード コーディングしていた Object という型の記述が、例 5 では T というパラメーターになりました。この T には後から「型」を当てはめることができるので、この T のことを「型パラメーター」といいます。そして、その T という型パラメーターを定義している箇所が [1] の太字の部分です。[1] のように、丸括弧の中で Of キーワードに続けて、型パラメーターの名前を書きます。このように、クラス名の直後に型パラメーターを定義した場合、そのクラスの中でこの型パラメーターを実際のデータ型のように使用することができます。

このクラス (例 5 のクラス) のように、クラス名の直後に型パラメーターの定義を伴うものを、「ジェネリック クラス」と呼びます。このジェネリック クラスでは、「(Of T)」の部分もクラスの識別子の一部として認識されるため、例えば、MyCollection クラスと MyCollection (Of T) クラスが同時に定義されていても、名前の重複エラーにはなりません。

また、「型パラメーター」に具体的な型を当てはめる作業は、この定義を使用する際に行います。ジェネリック クラスの場合は、以下のように、そのインスタンスを作成する際に型を当てはめます。

例 6. ジェネリック クラスでの型の当てはめ

Dim col As New MyCollection(Of Integer)()     ←[1]
col.Add(100)
col.Add(200)
col.Add( #4/10/2010# ) ←[2] コンパイルエラー!
col.Add(300)

ここでは、Integer を指定したので、例 5 の [1] の型パラメーターには Integer があてはめられ、この型チェックはコンパイル時に行われます。この結果、例 6 の [2] の Add メソッド呼び出しは、引数が Integer 型でないので、コンパイル時のエラーとして検出できます。

なお、型パラメーターに対して実際に型が当てはめられるのは実行時です。例えば、型を当てはめてない状態のジェネリック クラスも、単独でコンパイルすることができます。これによって、ジェネリック クラスを独立したライブラリにすることもできます。

次に、ジェネリック クラスを利用する上で不可欠な要素である「制約」について確認しましょう。

ページのトップへ


4. ジェネリックの制約

さて、次のようなジェネリック クラスがあるとき、このクラス (特に、太字の部分) はコンパイルできるでしょうか。

例 7. 型パラメーターのオブジェクトに対して様々な操作を行う

Public Class MySample(Of T)    ←[1]

Private dt As T             ←[2]

Public Sub Init()
dt = New T()            ←[3]
End Sub

   Public Sub Proc1()
dt.BackColor = SystemColors.ControlLight ←[4]
End Sub

Public Sub Proc2()
dt.Dispose()                                    ←[5]
End Sub

End Class

実は、[3] 以降の型パラメーターにまつわる操作はコンパイルできません。というのも、コンパイラは、それが正しいかどうか判断できないからです。

例えば、[3] では T 型のインスタンスを作成していますが、そもそも T がインスタンス作成可能な型であるとは限りません。また [4] では、このジェネリック クラスの作者は、T 型の変数 dt が Windows フォームのコントロールであることを前提に、背景 (BackColor プロパティ) を設定していますが、T がコントロール (Control 派生クラス) である保証はありません。同様に [5] では、T 型の変数 dt のオブジェクトが IDisposable インターフェイスを持っていることを前提にしていますが、これも保証されていないのでコンパイル エラーになります。

ジェネリック クラスを定義する際には、[3] から [5] のように、型パラメーターに関して何らかの操作をする場合もあるはずです。このような記述を可能にするには、このような操作が「確実に可能である」ことを保証するため、型パラメーターに当てはめることが可能な型を制限する必要があります。これを「制約」と呼びます。

ここでは、型パラメーター T に当てはめる型に対して、インスタンスが作成可能、Control クラスから派生、および IDisposable インターフェイスの実装という制約をつける必要があります。例 7 の [1] の部分に、次のように追加して制約をつけると (太字の部分)、上記はコンパイルできるようになります。

例 8. 改訂版: 型パラメーターへの制約

Public Class MySample(Of T As { New, Control, IDisposable })

このように、制約を与えるには、型パラメーターの宣言時に型パラメーターに続けて As 句を書きます。As 句の中括弧の中に、カンマ区切りで制約を並べます。ここでの New キーワードは、T が「インスタンス作成可能な型である」という制約です。また、特定のクラスからの派生や、特定のインターフェイスの実装を制約にするには、それぞれ、クラス名とインターフェイス名を記述します。

Note: 正確にいうと、前述の「New」という制約では、型パラメーターに当てはめる型が引数無しのコンストラクターを持っていることも必要になります。また、クラス名を制約として記述する場合、型パラメーターには、そのクラス名か派生クラス名を当てはめることができます。また、インターフェイス名を制約にした場合は、インターフェイスを実装したクラスか構造体、もしくはインターフェイス名自身を型パラメーターに当てはめることができます。

ただし、ここで記載している例では、New という制約も同時にあるので、インターフェイス名自身を型パラメーターに当てはめることはできません。

制約は、ジェネリック クラスを利用する側が型パラメーターに型を当てはめる際に、文字通りに「制約」を加えるだけでなく、このようにジェネリック クラスを作成する側がコンパイルする際に、適切に型チェックを行い、信頼性を向上する上でも貢献します。

ページのトップへ


5. 制約の留意点

制約について、もう 1 つ補足しておきます。前述の特定のクラスからの派生という制約について、次の疑問を持たれるかもしれません。

例えば、次 (例 9) の 2 つのクラスは、いずれも Control 派生クラスのオブジェクトの集合を管理するコレクション クラスです。[1] の MyCollection クラスには、[2] のように Control 型の要素の配列を内部で保持しています。要素の型が Control 型なので、Control および Control の派生クラスを表すことができます。一方、[3] のジェネリック クラス MyCollection (Of T) では、[4] のように型パラメーターを要素の型とする配列を作り、[3] のように、制約で Control の派生クラスという制限を加えています。結局 [1] の場合と [3] の場合で、違いがあるのでしょうか?

例 9. Control 派生オブジェクトを管理するコレクション

Visual Basic
Public Class MyCollection		←[1]
    Private col() As Control		←[2]
    Public Sub Add(ByVal c As Control)
    End Sub
End Class

Public Class MyCollection(Of T As Control)	←[3]
    Private col() As T					←[4]
    Public Sub Add(ByVal c As T)			←[5]
    End Sub
End Class
 

実は、この 2 つは同じではありません。ジェネリックを用いたほうが、型パラメーターに型を割り当てることによって、より型を限定することができます。例えば、例 10 のように、それぞれのクラスについてインスタンスを作ったとします。

例 10. インスタンスの作成、非ジェネリックとジェネリック

Visual Basic
Dim col1 As New MyCollection()			←[1]
Dim col2 As New MyCollection(Of ListBox)	←[2]
col1.Add( ... )		←[3]
col2.Add( ... )		←[4]
 

例 10 の [1] の非ジェネリックのほうは、このインスタンス (col1) の中で、様々な Control 派生オブジェクトを混在して管理することになり、[3] の Add メソッド呼び出しでは、Control 型の引数なら、いずれのコントロールでも追加できます。一方、[2] のジェネリックでは、インスタンスを作成する際に型パラメーターに対して、型を限定しています。(ここでは、ListBox に限定しており、ListBox を管理するコレクションになります。) このため、[4] の Add メソッド呼び出しの引数も ListBox 型になり、この型に合わない他のコントロールを追加することはできません。

今回はジェネリックの基本と制約について確認しました。次回は、ジェネリックのいくつかのバリエーション (ジェネリック メソッドやジェネリック インターフェイスなど) を取り上げます。


Code Recipe .NET Framework デベロッパー センター

ページのトップへ