Visual Basic 初級講座 [改訂版]
VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

第20-3回 ジェネリック

2021/9/26

この記事が対象とする製品・バージョン

Visual Studio 2022 Visual Studio 2022 対象です。
VB2019 Visual Basic 2019 対象です。
VB2017 Visual Basic 2017 対象です。
VB2015 Visual Basic 2015 対象です。
VB2013 Visual Basic 2013 対象です。
VB2012 Visual Basic 2012 対象です。
VB2010 Visual Basic 2010 対象です。
VB2008 Visual Basic 2008 対象です。
VB2005 Visual Basic 2005 対象ですが、一部対応していない機能も扱います。
VB.NET 2003 Visual Basic.NET 2003 × 対象外です。
VB.NET 2002 Visual Basic.NET (2002) × 対象外です。
VB6対応 Visual Basic 6.0 × 対象外です。

 

この記事は 後から追加したため、第20-3回という、中途半端な名称になってしまいました。

 

目次

1.ジェネリック型

1-1.用語

ListやStackなどのコレクションでは、単にそれが配列であるとか、Listであるとか、Dictionaryであるという以外に、StringのListであるとか、IntegerのStackであるとか、LongのQueueであるというように、「○○型の△△である」という表現が適切な場合があります。

コレクションのところで説明したようにVBではこれを Of を使って表現できます。

たとえば、次の例はStringのListの変数を定義します。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

 
Dim items As New List(Of String)

 

これはList(Of String)が型です。つまり、Integer型や String型があるのと同じように List(Of Integer)型や、List(Of String)型があります。このような型をジェネリック型と呼びます。

Of で指定されてている型は「型パラメーター」と呼びます。たとえば、List(Of String)の型パラメーターがStringです。型パラメーターは複数する場合もあります。たとえば、DictionaryではDictionary(Of String, Integer)のように2つの型パラメーターを採りますし、もっとたくさんの型パラメーターを指定するものもあります。

 

1-2.ジェネリック型のメリット

ジェネリック型の型の扱いが簡単になります。たとえば、List(Of Integer)とIntegerを使って次のようなプログラムを記述できますが、ここでは型変換など型の処理は一切必要ありません。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Dim items As New List(Of Integer)

items.Add(3)
items.Add(5)

Dim value As Integer = items(0)

Debug.WriteLine(value) ' 3 と出力されます。

Debug.WriteLineで出力される場所

 

Of String を使用すると、同じような処理を文字列型で記述することもできます。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Dim items As New List(Of String)

items.Add("Apple")
items.Add("Banana")

Dim value As String = items(0)

Debug.WriteLine(value) ' Apple と出力されます。

Debug.WriteLineで出力される場所

 

これがジェネリック型の効果であることを実感するために、ジェネリックがなかったVisual Basic.NET 2002, 2003 の時代に同じプログラムをどのように書いていたか見てみましょう。この時代にはListがそもそもなく、これに相当するのは ArrayList(読み方:ArrayListアレイリスト)というクラスです。

ArrayListは2021年現在では出番はありませんが、Visual Studio 2022でも実行することはできます。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

'(Of Integer)のように型を指定できない。
Dim items As New ArrayList

'型が不明なので、値はObject型で定義されており、とりあえずなんでも値をセットできる。
items.Add(3)
items.Add(5)

'itemsが Integer であることが保証されていないので型変換が必要。
Dim value As Integer = CInt(items(0))

Debug.WriteLine(value) ' 3 と出力されます。

Debug.WriteLineで出力される場所

ポイントはコメントでも記載していますが、項目の型が不明な点です。ArrayListではどのような型でも代入できるように項目はObject型で定義されています。ListにたとえるとList(Of Object)という形です。そのため、具体的なIntegerやStringなどとして扱おうとすると型変換が必要になります。

この例では代入するときに一か所型変換するだけですが、集計したり、メソッドやプロパティを呼び出したり型を前提としたさまざまな処理をするときに型変換が必要になります。

メモ メモ  -  Option Strict Off という手もありますが…
Option Strict Off にするとこの手の面倒から解放されますが、型が変換できることの責任はすべてプログラマーが負うことになり、型のメリットを享受できません。なにか型を前提とした処理をするときにその処理が成功することはプログラマーが確認する必要があります。Visual StudioやVisual Basicのエラーチェックや入力補完などの便利機能の支援も受けられなくなります。
もっとも、その前のVB6自体ではそれが常識だったので、VB.NET2002, 2003の時代ではあまり違和感がなかったようにも思います。

 

 

1-3.List<String>のような記述方法

Visual Basic では List(Of String) のような構文でジェネリック型を表現しますが、C# では List<String> のように記述します。

このことから、インターネット上のドキュメントやリファレンスではList(Of String)型のことを List<String> と表現する場合があるので覚えておいてください。

たとえば、Visual Basic のListクラスのリファレンスは2021年9月現在タイトルが「List<T> クラス」となっています。

List<T> クラス (System.Collections.Generic) | Microsoft Docs

このURL自体は各言語共有で、ページ左上の言語設定を切り替えるとVBやC#などの言語を切り替えられます。VBを選択してもタイトルは List<T>です。Visual Basicのリファレンスなのだから、Visual Basicの構文に合わせたタイトルにしてほしいものですが…。

 

また、List<String> でも List<Integer> でも、 その他のList<xxxxx> でも当てはまることを表現する場合、慣用的に T という文字を使って、List<T> と表現します。VBに配慮してList(Of T)と表現される場合もあります。

Dictionaryのように複数の型パラメーターをとるものは Dictionary<T1, T2> のように記述されることもありますし、1つ目の型パラメーターがキーの型、2つ目の型パラメーターが値の型であることから Dictionary<TKey, TValue>のように意味を表現する記述担っている場合もあります。

 

 

2.ジェネリック型の代入

ジェネリック型の代入はルールが難しく混乱することがしばしばです。

簡単にポイントを説明しますが、先にアドバイスを書いておくと、あまりルールにこだわらすとりあえずプログラムしてみて、ルールに違反しているかの判断は、Visual Studioのエラーや警告表示に任せるというスタンスです。

 

最初に指摘しておきしておきたいのは、型パラメータが異なるList型間では代入ができないということです。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Dim itemsA As New List(Of Integer)({1, 2, 3})

'この代入はできません。ビルドエラーになります。
Dim itemsB As List(Of Long) = itemsA

このプログラムは、やろうとしている意図が明確であり、実行できてもいいように思えますがVBの言語仕様では許可されません。このような合理的でないエラーがなくなるようにVBは進化してきましたが、このプログラムは数少ない不合理な例です。

ジェネリックでなければ同じ意味の代入は可能です。

たとえば、次のプログラムは何の問題もないことから考えても、上記のジェネリックの場合に失敗するというのは意表をつかれます。とにかくできないということを覚えておきましょう。

VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Dim numberA As Integer = 627

'この代入はOK
Dim numberB As Long = numberA

 

一方、型パラメーター同士に継承関係がある場合は、代入に成功する場合があります。残念ながら常に成功するとは言えません。

次のIEnumerableという型を使ったジェネリック型では成功します。(これはVB2010からできるようになりました。VB2008以前では実行時に失敗します。)

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Dim itemsA As IEnumerable(Of IO.FileInfo)

'この代入はOK
Dim itemsB As IEnumerable(Of IO.FileSystemInfo) = itemsA

この例では型パラメーターが異なる代入が成功していることに注目してください。FileInfoクラスはFileSystemInfoクラスの親クラスです。IEnumerableはこのような代入が許可されるように定義されており、この性質を「共変性」(きょうへんせい)と呼びます。

IEnumerableはコレクションのような集合を扱うのにとても便利なものでインターフェースという種類のものです。インターフェースについては初級講座第28ー2回、IEnumerableについては第36回で扱います。

これをListでやるとエラーになります。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Dim itemsA As List(Of IO.FileInfo)

'この代入はエラー
Dim itemsB As List(Of IO.FileSystemInfo) = itemsA

このときVBは次のエラーメッセージを出力します。(バージョンによって違うかもしれません。私はVisual Studio 2022 + .NET 6で試しています。)

エラー BC36756 'List(Of FileInfo)' を 'List(Of FileSystemInfo)' に変換できません。'IEnumerable(Of FileSystemInfo)' の使用を検討してください。

Listだとダメだから、IEnumerableを使いましょうというメッセージですね。

IEnumerableの説明は後日の予定ですが、どういうことかサンプルだけ紹介しておきます。次の例は成功します。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Dim itemsA As New List(Of IO.FileInfo)
itemsA.Add(New IO.FileInfo("C:\temp\test.txt"))
itemsA.Add(New IO.FileInfo("C:\temp\index.html"))

'この代入はOK
Dim itemsB As IEnumerable(Of IO.FileSystemInfo) = itemsA

'IEnumerableのままコレクションを操作するプログラムをすることもできます。
Dim file1 As IO.FileSystemInfo = itemsB(0)
Dim file2 As IO.FileSystemInfo = itemsB(1)

'ToListを使ってListに変換することもできます。
Dim itemsC As List(Of IO.FileSystemInfo) = itemsB.ToList

IEnumerableはコレクションの機能を持っていますので、この後のプログラムはListの代わりにIEnumeralbe型を使って記述することができるでしょう。どうしてもListが必要であれば ToList メソッドを使ってList型に戻すことができます。

その他 雑談 - いやいや難しい

いかがでしょうか。ジェネリック型の代入はわけがわからないと感じたなら、あなたの感性は正常です。なぜこのようなことになるのかという理屈はこの記事では省略していますけどまた難しいです。どうか気にしないでください。最初にも書いたようにこのことは気にせずプログラムしてVisual Studioがエラーメッセージを表示したときに、どうするか考えましょう。それに型パラメーターが違う代入というのはあまり使わないものです。

この手のことにはたいてい深い事情があり、どういう事情か調べていくといろいろ知識も増えるものですが、ここで時間を使っているより新しいことの習得に時間を使った方が有意義だと私は思います。理屈が気になる方はこのあたりで解説されています。

ジェネリックの共変性と反変性 | Microsoft Docs

共変性と反変性 - Visual Basic | Microsoft Docs

 

発展 発展学習  -  リファレンスにも記述されています

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

IEnumerableのリファレンスには型パラメーターが「共変」であると明記されており、Listのリファレンスにはそうは書かれていません。なお、仕様上Listのようなクラスで型パラメーターに共変性を持たせることはできません。

IEnumerable<T> インターフェイス (System.Collections.Generic) | Microsoft Docs

List<T> クラス (System.Collections.Generic) | Microsoft Docs

 

3.ジェネリックメソッド

3-1.単純なジェネリックメソッドの呼び出し

ジェネリックはクラスだけではなく、メソッドで使用される場合もあります。このようなメソッドをジェネリックメソッドと呼びます。

たとえば、指定した型の要素だけを抽出する OfType (読み方:OfType=オブタイプ)はジェネリックメソッドです。

次のプログラムでは Object型の要素を持つListを定義して、整数(Integer), 文字列(String)、小数(Double)の項目をそれぞれ2個ずつ持つようにしています。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Dim source As New List(Of Object)({123, 456, "ABC", "XYZ", 1.1, 2.2})

'String型の要素だけ抽出します。
Dim stringItems = source.OfType(Of String)
For Each item In stringItems
    Debug.WriteLine(item)
Next

Debug.WriteLineで出力される場所

この中から、指定した型の値だけ抽出するのに OfType メソッドが便利に使用できます。

OfTypeメソッドの戻り値はさきほど少し登場した IEnumerable型であり、コレクションの機能を持っているので、これに対してFor Eachするなどして個々の値を取り出すことができます。

このプログラムを実行すると、"ABC" と "XYZ" が出力されます。

OfTypeでは、抽出対象の型を指定する必要があるのですが、通常のメソッドの引数だと型ではなく値を指定するので、型を指定するというのはなかなか面倒なものです。一方、ジェネリックの型パラメーターを使うと直接型を指定できるので、このような機能はとても使いやすくなります。

メモ メモ  -  OfType は List以外に配列やStackや各種コレクションで使用できます。
本文ではListの例を紹介していますが、OfTypeメソッドは、List以外でも配列やStackなどの各種コレクションで使用できます。たとえば、次の例では、配列に対して OfType メソッドを使用します。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Dim source As Object() = {123, 456, "ABC", "XYZ", 1.1, 2.2}

'String型の要素だけ抽出します。
Dim stringItems = source.OfType(Of String)
For Each item In stringItems
    Debug.WriteLine(item)
Next

Debug.WriteLineで出力される場所

 

3-2.型パラメーターによって引数の型が変わるジェネリックメソッド

ArrayクラスのIndexOfメソッドは配列から要素を検索する機能で、これもジェネリックメソッドです。配列を対象にしたメソッドなので使い方がちょっと前時代的ですが、題材にちょうどよいので、少し一緒に見てみましょう。

次のプログラムは文字列型の配列から値を検索します。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Dim source As String() = {"い", "ろ", "は", "に", "ほ", "へ", "と"}

Dim index As Integer = Array.IndexOf(Of String)(source, "ほ")

Debug.WriteLine(index) '4 と出力されます。

このIndexOfメソッドには、いくつかオーバーロードがあります。この例で使用しているオーバーロードの定義は次の通りです。

慣れないうちは、この定義が意味するところがどういうことかわかりにくいと思うので、少し分解して考えてみましょう。

①のPublicはこのメソッドがどこからでも呼び出せることを示しています。②のSharedはこのメソッドが共有メソッドであることを示しています。つまり、クラスのインスタンスから呼び出すのではなく、クラスから直接呼び出す使い方をするということです。③のFunctionはこのメソッドに戻りがあることを示しています。④のIndexOfはこのメソッドの名前です。飛ばして最後の⑦はこのメソッドの戻り値です。

ポイントは⑤と⑥です。⑤はこのIndexOfメソッドに与える型パラメーターを示しています。Tという名前の型パラメーターを1つ指定できることが示されています。⑥はこのIndexOfメソッドの通常の引数です。引数は2つあって、1つ目の引数の名前はarrayで型は T の配列です。T というのは⑤で指定する型パラメーターのことです。つまり、⑤でIntegerを指定した場合、第1引数arrayはIntegerの配列ということになります。このように、ジェネリックメソッドは型パラメーターを使って自分自身の定義やプログラムに含まれる型を変えることができるのです。これはすごい!2つ目の引数は名前は value で 型は T です。こちらは () がついていないので配列ではありません。

機能の観点で眺めると、IndexOfメソッドは第1引数 array の中から、第2引数 value を探し出して、その位置を返します。配列にはいろいろの型があるので、このように型パラメーターを持つジェネリックメソッドにすることでいろいろな型に1つの定義で対応できるようにしているわけです。

なお、このArray.IndexOfというメソッドはあまり重要ではなく、私がここで文字数を割いてみなさんに理解してもらいたいのは、この手のヒントの見方です。Visual Studioを扱っているとこのようなヒントが頻繁に表示されますが、ジェネリックを使う場合などはヒントの表記が複雑になり、せっかくのヒントも何をどう見ればよいのかわからないというもったいない状況になってしまいます。そうならないようにArray.IndexOfを題材にしてここで説明しています。

 

以上を踏まえてVisual Studio のヒントにも注目してみましょう。

型パラメーターを入力する直前では、Visual Studioは次のようなヒントを表示します、

ここでは型パラメーターが T と表現されていて、第1引数、第2引数でも型が T で表現されています。

続けて、型パラメーターに String を入力して、引数の入力をする時点では、Visual Studio のヒントは 次のように T だった部分が String に変化しています。

型パラメーターにStringを入力したことで T が String であることが確定したので、それに応じてヒントも自動的に切り替わったのです。

Stringではなく、IntegerやPointなど別の型を指定すると、ヒントもそれに応じたものに変わります。

 

このように型パラメーターは、型の定義を柔軟に変化させる目的で使用できるということを理解していただけと思います。

 

3-3.型パラメーターの推論

型パラメーターが推論可能な場合、型パラメーターの指定は省略することができます。

Array.IndexOf メソッドの例をもう1度眺めてみます。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022


Dim index As Integer = Array.IndexOf(Of String)(source, "ほ")

型パラメーターに String を指定しているので、第2引数に "ほ" のような String型の値が指定できているわけですが、逆に考えると第2引数が "ほ" であるならば、型パラメーターは String 以外ありえないということになります。

VBもこのような推論が可能なので、実際のところこの例では(Of String) を省略して次のように書くことができます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022


Dim index As Integer = Array.IndexOf(source, "ほ")

 

VBは引数の型を手がかりに型パラメーターを推論します。型パラメーターの推論に使用できる引数がない場合、型パラメーターを省略できません。

たとえば、前述したOfTypeメソッドは、指定した型の要素だけの新しいコレクションを作成する機能で次のようにしようしました。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim source As New List(Of Object)({123, 456, "ABC", "XYZ", 1.1, 2.2})

'String型の要素だけ抽出します。
Dim stringItems = source.OfType(Of String)
For Each item In stringItems
    Debug.WriteLine(item)
Next

Debug.WriteLineで出力される場所

この場合、(Of String)を指定することで、文字列型の要素を抽出したいということを明示しています。この(Of String)を省略してしまうと、何がしたいのか推論する手がかりが一切なくなってしまうので、この例では(Of String)を省略するとエラーになります。

 

4.ジェネリックメソッドの作成

4-1.ジェネリックメソッドの定義

自分でジェネリックメソッドを作成する場合も、Of を使って型パラメーターを定義します。

次のような構文になります。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Public Function メソッド名(Of 型パラメーターリスト)(パラメーターリスト) As 戻り値の型

    実行する処理

End Function

通常の関数と同様に戻り値がない場合はFunctionの代わりにSubを使用します。Publicの部分もアクセスレベルに応じてPrivateやProtectedなど他の物を指定することもできます。

次の例は、単純なジェネリックメソッドの例です。

この例では、型パラメーターで受け取った型の完全修飾名(=名前空間を含んだ型の名前。たとえば、System.String)を取得します。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Private Function GetTypeName(Of T)() As String

    Return GetType(T).FullName

End Function

VBでは、GetType(読み方:GetType=ゲットタイプ)を使って、型から、型の情報を持つType型のオブジェクトを生成することができます。TypeクラスのFullNameプロパティを使うとその型の完全修飾名を取得できます。

呼び出し側は次のように記述できます。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Dim typeName As String = GetTypeName(Of Debug)()
Debug.WriteLine(typeName) 'System.Diagnostics.Debug と出力されます。

Debug.WriteLineで出力される場所

 

このような単純な型パラメーターのみのメソッドは、情報量が少ないため、あまり大した処理は記述できません。

もう一例紹介しておきます。次の例は指定した型で定義されているプロパティの一覧を取得します。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

''' <summary>
''' Publicで定義されているプロパティの一覧を取得します。
''' 共有プロパティは取得できません。
''' </summary>
''' <typeparam name="T">対象の型を指定します。</typeparam>
''' <returns>プロパティの名前の一覧</returns>
Private Function GetProperties(Of T)() As IEnumerable(Of String)

    '抽出対象を指定するフラグを作成します。
    'ここでは Public かつ 非共有メンバー かつ 基底クラスも含むものを指定しています。
    Dim flag As Reflection.BindingFlags = Reflection.BindingFlags .Public Or
        Reflection.BindingFlags .Instance Or
        Reflection.BindingFlags .FlattenHierarchy

    'フラグが示すプロパティを抽出し、名前だけの一覧を作成し、重複している名前を除去します。
    Dim propNames = From prop In GetType(T).GetProperties(flag) Select prop.Name Distinct

    Return propNames

End Function

このプログラムアGetTypeで取得したType型のオブジェクトで、GetPropertiesメソッドを使うとプロパティの一覧を取得できることを利用しています。抽出を実行しているFrom prop In ... の部分は LINQ という技術で、初級講座第37回 で説明します。ここではあまり内容に踏み込まないことにしましょう。

この例は次のように呼び出せます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Dim propertyNames = GetProperties(Of String)()

For Each propertyName As String In propertyNames
    Debug.WriteLine(propertyName)
Next

Debug.WriteLineで出力される場所

これを実行すると Chars と Length が出力されます。StringのPublicな非共有プロパティはこの2つだけということですね。

 

引数のあるジェネリックメソッドはこれに通常のように引数を追加するだけです。

次の例では引数に IEnumerable をとり、型パラメーターで指定した型の最初の要素を取得します。IEnumerableはコレクションの一般的な機能を表すインターフェースで(まだ説明していないインターフェースがたびたび登場して申し訳ありませんが)、List や配列などをまとめて表現したいときに便利な型です。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Private Function GetFirstItem(Of T)(items As IEnumerable) As T
    For Each item In items
        If TypeOf item Is T Then
            'この型変換は必要。
            'コンパイラーにReturnする型と関数の戻り値で定義されている方が同じだと伝えるため。
            Return DirectCast(item, T)
        End If
    Next
End Function

次のように呼び出せます。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Dim source As New List(Of Object)({123, 456, "ABC", "XYZ", 1.1, 2.2})

Dim firstDouble As Double = GetFirstItem(Of Double)(source)

Debug.WriteLine(firstDouble) ' 1.1 と出力します。

Debug.WriteLineで出力される場所

 

4-2.New制約

単純な型パラメーターだけのジェネリックメソッドが大したことができない最大の理由はインスタンスを作成できないからです。

たとえば、次のように型パラメーターで指定された型に対してNewを使用するプログラムはエラーになります。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Private Sub MyGenericsMethod(Of T)()

    'この例はエラーです。
    Dim instance As New T

End Sub

なぜこれができないかというと、New T という記述は T型のコンストラクターを呼び出してインスタンスを生成するということを意味しています。ところが、コンストラクターはクラスによって異なっており、引数の数や意味が異なります。

New T という記述だと引数なしのコンストラクターを呼び出すという記述になっていますが、T型にそのように呼び出せるコンストラクターが存在しているかどうか判断できません。VBやC#のような強い型付けの言語はこのように型に対する操作は事前に実行できることがわからない場合エラーにしてそもそも実行できないようにします。このようにして実行する前に早めに問題がある可能性をプログラマーに気が付いてもらいたいという趣旨なのです。

そこで、T には引数なしのコンストラクターを持つ型しか指定できないという特別な指示を追加することができます。ジェネリックの型にこのような条件を付けることを「制約」(せいやく)と呼びます。制約は As とともに使用します。これまでキーワード As は型を指定するときにだけ使っていましたが、ここでは制約を指定するという新しい使い方をすることになります。混同しないように気を付けてください。

引数なしのコンストラクターを持つ型しか指定できないという制約はキーワード New で表現します。

この制約を追加すると、上述のプログラムは次のようになりエラーにはならくなります。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Private Sub MyGenericsMethod(Of T As New)()

    'この例は実行できます。
    Dim instance As New T

End Sub

この制約を追加したので、呼び出し側は自由な呼び出しはできなくなります。

たとえば、 MyGenericsMethod(Of String)() という呼び出しはできません。Stringには引数なしで呼び出せるコンストラクターがないからです。

 MyGenericsMethod(Of Random)() という呼び出しは可能です。Random は引数なしで呼び出せるコンストラクターがあるからです。このように「制約」を付けることで、呼び出し側で指定できる型が制約されるというわけです。

発展 発展学習  -  引数ありのコンストラクターを呼び出す

発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。

ジェネリックの型パラメーターから引数ありのコンストラクターを呼び出すことも可能です。リフレクションというカテゴリーの機能を利用することになります。しかし、引数ありの場合、引数の型や数や順番がクラスによって異なるので、型パラメーターでいろいろな型を指定できるようにするという方向性と逆行しがちです。後述するクラスの制約と組み合わせれば役に立つ場面もあるかもしれませんが、あまり使いたい技ではないです。

次の例は、文字列型の1つの引数を持つコンストラクターを呼び出して型パラメーターのインスタンスを生成します。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Private Sub MyGenericsMethod(Of T)()

    '1つの文字列型の引数をもつコンストラクターを呼び出します。
    Dim ctors As Reflection.ConstructorInfo = GetType(T).GetConstructors.First
    Dim instance As Object = ctors.Invoke({"C"}) 'ここではコンストラクターの引数に "C" を指定します。

End Sub

この例は DriveInfoクラスを念頭に作成しており、を試すには、MyGenericsMethod(Of IO.DriveInfo)() のように呼び出します。

 

4-3.その他の制約

ジェネリックメソッドの型で困ったことがあったら制約を思い出してください。

制約は、指定できる型を限定する代わりにメソッド内でできることを増やしてくれます。

ここでは、New制約以外の制約を簡単に紹介します。

参考:

ジェネリック型 - Visual Basic | Microsoft Docs

 

型の制約

As 型名 で制約すると、その型で制約することができます。その場合、型パラメーターにはその型か、その型の派生クラスしか指定できなくなります。構造体はこの制約では対象にできません。

次の例では、型パラメーター T には Control型か、その派生クラスしか指定できなくなります。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Private Sub MyGenericsMethod(Of T As Control)()

End Sub

この制約は強力で、指定できる型がかなり限定されますが、限定が大きい分、使える機能が増えます。その型のもつメソッドやプロパティが使用できることが保証されるからです。

インターフェースの制約

As インターフェースで制約すると、指定したインターフェースを持つ型しか指定できないように制約できます。

次の例では、型パラメーター T には IEnumerable型か、それを実装する型しか指定できなくなります。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Private Sub MyGenericsMethod(Of T As IEnumerable)()

End Sub

クラスであることの制約

クラスである型しか指定できないように制約できます。キーワード Class を使用します。

次の例では、型パラメーター T には クラスである型しか指定できなくなります。逆に言うと構造体を指定できなくなります。たとえば、IntegerやDateは構造体なのでこの型パラメーターTには指定できなくなります。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Private Sub MyGenericsMethod(Of T As Class)()

End Sub

構造体であることの制約

構造体である型しか指定できないように制約します。キーワード Structure を使用します。

次の例では、型パラメーター T には構造体であるInteger や Date、 Point などは指定できますが、String や StremWriter などのクラスは指定できなくなります。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Private Sub MyGenericsMethod(Of T As Structure)()

End Sub

複数の制約

複数の制約を指定する場合は { } 内にカンマで区切って制約を並べます。

次の例では、型パラメーター T には Control型か、その派生クラスで、かつ、引数なしのコンストラクターを呼び出せるものしか指定できなくなります。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Private Sub MyGenericsMethod(Of T As {Control, New})()

End Sub

なお、これを使ってもクラスの制約は1つしか指定できません。

 

5.ジェネリッククラス

5-1.クラスの型パラメーター

メソッド単位ではなく、クラスレベルで型パラメーターを使用することもできます。ListやDictionaryなどで既に使っているものです。

使用する側ではコンストラクターで型パラメーターを指定することになります。

次の例はこれまでもよく出てきている例ですが、List(Of T)クラスを型パラメーターに String を指定してインスタンス化している例です。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022


Dim items As New List(Of String)

ジェネリッククラスを自作する場合は、次のように型パラメーターを指定します。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Public Class MyPoint(Of T)

    Public Property X As T
    Public Property Y As T

End Class

コレクションのように値を単なるデータとして扱う場合は、これでも良いのですが、値を使って何か処理することを考えるとジェネリッククラスはなかなかたいへんです。型の実体が何かわからないからです。

たとえば、上述のMyPointクラスはXプロパティとYプロパティが T型になっています。私はなんとなく、T は Integer とか Double とか、Decimal のような数値が指定されることをイメージしてこの例を書きましたが、機能上はそのような制約は一切ないので T には String や Date や StreamWriter などが指定されるかもしれません。

その可能性を考慮すると X や Y を使った計算処理は記述できません。プログラムしている本人は数値しか指定しないつもりでたし算やかけ算を実行するプログラムを書きたいかもしれませんが、制約のところで説明したようにVBはそのようなプログラムを許可しないので、このクラスに次のようなメソッドを記述することはできません。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Public Function IsPositive() As Boolean
    If X > 0 AndAlso Y > 0 Then
        Return True
    End If
    Return False
End Function

値を比較する X > 0 と Y > 0 の部分が、 X と Y が数値なら問題ないのですが大小比較できないような型が T に指定される可能性があるからです。CInt や CLng でこの部分を強制的に数値に変換することもできません。CInt や CLng で変換できる型かどうかわからないのでコンパイラーはエラーにします。

ジェネリッククラスを採用する際はこのようなことも考慮してください。

なお、この例でどうしてもXやYを数値型に変換したい場合、Convertクラスのメソッドを使用することは可能です。たとえば、次の例は動作します。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Public Function IsPositive() As Boolean
    Dim intX As Integer = Convert.ToInt32(X)
    Dim intY As Integer = Convert.ToInt32(Y)
    If intX > 0 AndAlso intY > 0 Then
        Return True
    End If
    Return False
End Function

ただ、この例だと、X や Y が Integer に変換できることが前提になってしまい、ジェネリックを使用する意味がなくなってしまいます。

 

5-2.ジェネリッククラスの継承

ジェネリッククラスを継承することもできます。たとえば、List(Of T)を継承した独自の文字列のコレクションを作成することができます。

次の例では MyList クラスは List(Of String)を継承しているので、List(Of String)のすべての機能を持ちます。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Public Class MyList
    Inherits List(Of String)

    Public Function GetLongest() As String
        Dim longest As String = ""
        For Each item As String In Me
            If Len(item) > Len(longest) Then
                longest = item
            End If
        Next
        Return longest
    End Function
End Class

この例では、For Each のループで In Me というように Me が使われていることにも気を付けてください。自分自身がコレクションなので、Me を指定してループできます。

このクラスの呼び出し例は次の通りです。

VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

'List(Of String)を継承しているので From での初期化も可能
Dim items As New MyList From {"Apple", "Banana"}

'List(Of String)を継承しているので Add も使用できる。
items.Add("Cat")
items.Add("Dog")

'追加したメソッドを呼び出して、最長の項目を取得します。
Dim longestItem As String = items.GetLongest

 

型パラメーターを保ったまま継承するには次のようにします。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019 Visual Studio 2022

Public Class Class MyList2(Of T)
    Inherits List(Of T)

    '内容は省略

End Class