ジェネリック型やジェネリックメソッドではOf句を使うことであらゆる型をパラメータとして使用できるようになります。 しかし、型パラメータに指定できる型をある程度限定したい場合もあります。 例えば、型パラメータに指定できる型をなんらかのインターフェイスを実装するクラスや、値型・参照型のみにしたい場合などです。 こういった場合には、Of句に続けてAs句を加えることで型パラメータに制約を設けることができます。

§1 制約の例

制約の例として、オブジェクトを複製するジェネリックメソッドを作成する例を考えてみます。 ここでは、次のようなメソッドを作成することにします。

' TObject型の引数objを複製するメソッド
Function CloneObject(Of TObject)(ByVal obj As TObject) As TObject
  ' objを複製したものを返したい
  Return obj
End Function

CloneObjectメソッドは、引数で与えられたオブジェクトを複製して返すものとします。 このメソッドでは引数objの型および戻り値の型を決定するために型パラメータTObjectを使用します。

上記の動作を満たすようにメソッドの内容を書き換えていきます。 まず、引数objを複製するためにCloneObjectメソッドを使うように書き換えます。 同時に、戻り値の型をTObjectにするためDirectCast演算子でキャストするようにします。

' TObject型の引数objを複製するメソッド
Function CloneObject(Of TObject)(ByVal obj As TObject) As TObject
  ' Cloneメソッドを使って複製を作成し、TObjectにキャストして返すようにしたい
  Return DirectCast(obj.Clone(), TObject)
  ' BC30456: 'Clone' は 'TObject' のメンバではありません。
End Function

この段階では上記のようにコンパイルエラーが発生します。 objの型はTObjectですが、TObjectにはCloneメソッドが存在しないためこのようなエラーとなります。 これは呼び出し側でTObjectの具体的な型が指定されるまでTObjectがCloneメソッドを持つかどうかわからないためです。

このエラーを回避するため、型パラメータTObjectに制約を指定することにします。 型TObjectがCloneメソッドを持つ型に限定されるようにするため、「TObjectはICloneableインターフェイスを実装していなければならない」という制約を設けます。 具体的には、メソッド宣言部のOf句に続けてAs ICloneableを追加します。

' ICloneableインターフェイスを実装するTObject型の引数objを複製するメソッド
Function CloneObject(Of TObject As ICloneable)(ByVal obj As TObject) As TObject
  ' Cloneメソッドを使って複製を作成し、TObjectにキャストして返す
  Return DirectCast(obj.Clone(), TObject)
End Function

このように制約を設けることで、ジェネリックメソッド内では型パラメータが実装するICloneableインターフェイスのメンバ(=Cloneメソッド)を呼び出すことができるようになり、同時に型パラメータに指定できる型をICloneableインターフェイスを実装する型に限定することができるようになります。 一方、制約に反する型を指定してこのメソッドを呼び出そうとした場合にはコンパイルエラーとなります。

Imports System

Module Sample
  Function CloneObject(Of TObject As ICloneable)(ByVal obj As TObject) As TObject
    ' Cloneメソッドを使って複製を作成し、TObjectにキャストして返す
    Return DirectCast(obj.Clone(), TObject)
  End Function

  Public Sub Main()
    Dim s1 As String = "foo"
    Dim s2 As String = CloneObject(s1) ' StringはICloneableを実装しているため、CloneObjectメソッドを呼び出せる

    Dim u1() As Integer = New Integer() {1, 2, 3}
    Dim u2() As Integer = CloneObject(u1) ' 配列(Array)もICloneableを実装しているため、CloneObjectメソッドを呼び出せる

    Dim i1 As Integer = 1
    Dim i2 As Integer = CloneObject(i1) ' IntegerはICloneableを実装していないため、CloneObjectメソッドを呼び出せない
    ' BC32044: 型引数 'Integer' は、制約型 'System.ICloneable' から継承したり、この型を実装したりしません。
  End Sub
End Module


§2 インターフェイスの制約

先の例のように、制約には通常のインターフェイスを指定できるほか、ジェネリックなインターフェイスを指定することもできます。 例えば、IComparable(Of T)インターフェイスを実装する二つの引数のうち大きい方を返すMaxメソッドを作成すると次のようになります。 このメソッドでは、型パラメータTに「IComparable(Of T)インターフェイスを実装していること」を制約として求めています。

Imports System

Module Sample
  ' 引数で与えられた二つの値のうち、大き方を返すジェネリックメソッド
  Function Max(Of T As IComparable(Of T))(ByVal x As T, ByVal y As T) As T
    ' IComparable(Of T).CompareToメソッドを使ってどちらが大きい値を持つか判別する
    If 0 <= x.CompareTo(y) Then
      Return x
    Else
      Return y
    End If
  End Function

  Public Sub Main()
    Console.WriteLine(Max(3, 16))

    Console.WriteLine(Max(3.1416, 2.7183))

    Console.WriteLine(Max("bar", "foo"))
  End Sub
End Module
実行結果
16
3.1416
foo

§3 クラスの制約

制約にはインターフェイスだけでなく次の例のように基底クラスを指定することもできます。

Imports System

' 基底クラス
Class Control
  ' Clickイベント
  Public Event Click As EventHandler
End Class

' Controlを継承したButtonクラス
Class Button
  Inherits Control
End Class

' Controlを継承したCheckBoxクラス
Class CheckBox
  Inherits Control
End Class

Module Sample
  ' Clickイベントのハンドラ
  Sub OnClick(ByVal sender As Object, ByVal e As EventArgs)
    ' 実装は省略
  End Sub

  ' ControlにClickイベントのハンドラを割り当てるジェネリックメソッド
  Sub AddClickEvent(Of TControl As Control)(ByVal c As TControl)
    AddHandler c.Click, AddressOf OnClick
  End Sub

  Public Sub Main()
    Dim b As New Button()
    Dim c As New CheckBox()

    AddClickEvent(b)
    AddClickEvent(c)
  End Sub
End Module

ただ、この例のような場合、引数の型を単に基底クラスの型にすればよいので制約を用いる必要性はありません。

' ControlにClickイベントのハンドラを割り当てる(非ジェネリックな)メソッド
Sub AddClickEvent(ByVal c As Control)
  AddHandler c.Click, AddressOf OnClick
End Sub

このように、ジェネリクスを使わなくてもポリモーフィズムによって実現できる場合と、ジェネリクスと制約を使わなければ実現できない場合があるため、両者を適切に使い分ける必要があります。

§4 値型・参照型の制約

インターフェイスやクラスなどの具体的な型による制約だけでなく、値型または参照型のどちらかに限定する制約を指定することもできます。 型パラメータが値型であることを要求する場合はOf句に続けてAs Structure、参照型であることを要求する場合は続けてAs Classを指定します。

Imports System
Imports System.Collections.ObjectModel

' 参照型の型パラメータTRefを要求するコレクション
Class ReferenceTypeCollection(Of TRef As Class)
  Inherits Collection(Of TRef)
End Class

' 値型の型パラメータTValを要求するコレクション
Class ValueTypeCollection(Of TVal As Structure)
  Inherits Collection(Of TVal)
End Class

値型・参照型と制約については値型と参照型 §.ジェネリック型の制約でも解説しています。

§4.1 デフォルト値

ジェネリックメソッド・ジェネリック型で総称型(=パラメータ化された型)の値を初期化する場合、値型か参照型かによって場合分けする必要はなく、ともにNothingを代入することで初期化できます。 例えば次のような配列の内容をクリアするジェネリックメソッドを考えます。

Imports System

Module Sample
  ' 参照型配列の内容をクリアするメソッド
  Sub ClearRefArray(Of TRef As Class)(ByVal arr() As TRef)
    For i As Integer = 0 To arr.Length - 1
      ' 配列内の各要素にNothingを指定する
      arr(i) = Nothing
    Next
  End Sub

  ' 値型配列の内容をクリアするメソッド
  Sub ClearValArray(Of TVal As Structure)(ByVal arr() As TVal)
    For i As Integer = 0 To arr.Length - 1
      ' 配列内の各要素にデフォルトコンストラクタで初期化した値を指定する
      arr(i) = New TVal()
    Next
  End Sub

  Public Sub Main()
    Dim refarr() As String = New String() {"foo", "bar", "baz"}

    ClearRefArray(refarr)

    Dim valarr() As Integer = New Integer() {1, 2, 3}

    ClearValArray(valarr)
  End Sub
End Module

上記の例では、参照型用と値型用の二つのジェネリックメソッドを用意しています。 この例の場合、制約によって値型・参照型に限定するまでもなくどちらもNothingの代入によりデフォルト値への初期化を行うことができます。

Imports System

Module Sample
  ' 配列の内容をクリアするメソッド
  Sub ClearArray(Of T)(ByVal arr() As T)
    For i As Integer = 0 To arr.Length - 1
      ' Nothingを指定してデフォルト値に初期化する
      arr(i) = Nothing
    Next
  End Sub

  Public Sub Main()
    Dim refarr() As String = New String() {"foo", "bar", "baz"}

    ClearArray(refarr)

    Dim valarr() As Integer = New Integer() {1, 2, 3}

    ClearArray(valarr)
  End Sub
End Module

値型・参照型とデフォルト値については型の種類・サイズ・精度・値域 §.型のデフォルト値、値型(構造体)の初期化については構造体でも解説しています。

§5 コンストラクタの制約

制約により、パラメータ化された型が必ずコンストラクタを持つように要求することもできます。 Of句に続けてAs Newと指定することにより、型パラメータにコンストラクタを持つ型のみを指定できるように限定することができます。 ただし、この制約により指定できるのは引数を取らないコンストラクタ(デフォルトコンストラクタ)のみとなります。 引数を取るコンストラクタを制約で指定することはできません。

Imports System

Module Sample
  ' 引数のないコンストラクタを持つ型Tを要求するメソッド
  Sub FillArray(Of T As New)(ByVal arr() As T)
    For i As Integer = 0 To arr.Length - 1
      arr(i) = New T()
    Next
  End Sub

  Public Sub Main()
    Dim arr1(2) As UriBuilder

    FillArray(arr1) ' UriBuilderは引数のないコンストラクタを持つため、呼び出せる

    Dim arr2(4) As Integer

    FillArray(arr2) ' Integer(構造体)も引数のないコンストラクタを持つため、呼び出せる

    Dim arr3(9) As String

    FillArray(arr3) ' Stringには引数のないコンストラクタはないため、呼び出せない
  End Sub
End Module

このように、制約でAs Newを指定した場合はデフォルトコンストラクタを持つクラスまたは暗黙的にデフォルトコンストラクタが用意される構造体のみが型パラメータとして指定できるようになります。

§6 制約の組み合わせ

ここまでで紹介してきた制約を複数組み合わせて指定することもできます。 複数の制約を指定する場合は、Of T As {制約1, 制約2, ...}のように中括弧でくくって指定します。 例えば次の制約の場合、「型パラメータTは参照型(Class)で、デフォルトコンストラクタを持ち(New)、かつIDisposableインターフェイスを実装している」ことを要求します。

Class GenericClass(Of T As {Class, New, IDisposable})
End Class

§7 制約と列挙体

値型(構造体)・参照型(インターフェイス・クラス)に対応する制約は存在しますが、列挙体(Enum)に対応する制約は存在しません。 そのため、型パラメータに指定できる型を列挙型に限定したい場合は、列挙体の基底型である構造体(Structure)を制約として指定します。 当然これだけでは制約としては不十分なので、必要に応じてメソッド内で型情報を調べて実際に指定された型が列挙体かどうかを調べるようにすることもできます。

Imports System

Module Sample
  ' 文字列から列挙体の値に変換するメソッド
  Function Parse(Of TEnum As Structure)(ByVal str As String) As TEnum
    ' 型パラメータTEnumの型情報を取得する
    Dim t As Type = GetType(TEnum)

    ' 型パラメータTEnumに指定された型が列挙体かどうか調べる
    If Not t.IsEnum Then Throw New ArgumentException("列挙体ではありません")

    ' 列挙体の場合は、Enum.Parseメソッドを使って文字列から列挙体に変換する
    Return CType([Enum].Parse(t, str, True), TEnum)
  End Function

  Public Sub Main()
    ' 文字列からDayOfWeek列挙体に変換する
    Dim d As DayOfWeek = Parse(Of DayOfWeek)("friday")

    Console.WriteLine(d)
  End Sub
End Module