Visual Basic 初級講座 [改訂版]
VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

第28回 型の関係

2021/1/17

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

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 × 対象外です。

 

目次

 

1.代入可能な型

型変換なしで 変数に別の変数や値を代入できる条件は、型が一致していることです。

次の a = b というシンプルな代入で考えて見ましょう。

VB6 VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019


a = b

型が一致しているとは、次の3つの場合のどれかを指します。

1.a と b の型がまったく同じである場合。例: a は Integer, b も Integer

2.a の型 が b の基本型である場合。例: a は FileSystemInfo型、 b は DirectoryInfo型

3.a の型 が b が実装しているインターフェースである場合。

 

今回は型が一致しているとはどういうことかという視点で、型同士の関係を説明します。

 

2.継承

 

2-1.型の親子関係

.NETの型システムでは型は(いびつな)ピラミッド型の階層構造になっています。型には親子関係があるのです。すべての型の親をたどっていくとObject型にいきつきます。Object型はピラミッドの頂点にある型です。

つまり、Object型以外のすべての型は、何らかの型を必ず1つだけ親としているということです。親がない型や親が2ついる型は存在しません。

この型の親子関係は 継承(けいしょう) という機能 によって構成されます。継承機能では、親にあたる型のことを基本型、基底クラス、親クラスなどと呼びます。子にあたる型のことを派生型、派生クラス、子クラスなどと呼びます。これらの呼び方は文脈によって使い分けられますが、区別しなくてもだいたい意味は通じます。たとえば、型の話をしているときは基本型・派生型、クラスやオブジェクト指向の話をしているときは基底クラス・派生クラスなどと使い分けます。

メモ メモ  - 派生クラス・子クラスには構造体も含むことにします

派生クラス・子クラスという表現は「クラス」と言っていますが例外的に構造体も含めた表現と考えて問題ありません。構造体からは何も派生できないという特徴があるので、構造体の継承関係はあまり問題にならないためです。とは言え、言葉に厳しい先輩は「構造体」と「クラス」は違うから、構造体も含めてこのように表現することは間違っているというかもしれません。それは正しいです。でも、意味は通じますし、実用上問題ありません。何より区別すると「子クラスまたは構造体」のような記述が増えてしまい文章が長くなるので私はこの記事では派生クラス・子クラスという言葉には構造体も含んで使うことにします。

継承は階層構造になっていて、派生型からまた別の型が派生するということもよくあります。ですので、ある型から見ると直接派生している子供にあたる型もあれば、その子供から派生している孫となる型もあるという具合になります。

継承関係にある型の場合、基本型(親)には派生型(子供・孫・ひ孫・・・)を代入することができます。そのため、階層の頂点にあるObject型にはすべての型を代入できます。

派生型は基本型の機能を引き継ぎます。詳しくは別の機会に説明しますが、デフォルトでは基本型がもっているメソッドやプロパティは派生型ももっています。たとえば、Object型には そのオブジェクトを文字列で表現する ToString というメソッドがあります。そのため、すべての型で ToString というメソッドが使用できます。このデフォルトの状態を変更するようにプログラムすることもできるので、実際に何が使用できてどういう機能なのかはリファレンスで確認する必要があります。

 

ある型の基本型(基底クラス)を調べるにはMicrosoft Docsを見るのが一般的です。

たとえば、TextureBrushクラスを調べると、「継承」という欄に Object → MarshalByRefObject → Brush → TextBrush と書いてあります。これが継承の階層です。TextBrushクラスの基底クラスはBrushクラスで、その基底クラスがMarshalByrefObjectで、その基底クラスがObjectということがわかります。

Micorosft Docsで基本型を調べる

 

発展 発展学習  -  プログラムで基底クラスの名前を取得する

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

プログラムでは次のようにして基底クラスの名前を確認することができます。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim baseTypeName As String = GetType(System.Drawing.TextureBrush).BaseType.FullName
Debug.WriteLine(baseTypeName) ' System.Drawing.Brush と表示されます。

Debug.WriteLineで出力される場所

変数から取得するには次のようにします。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim tBrush As New System.Drawing.TextureBrush(Image.FromFile("C:\temp\test.png"))

Dim baseTypeName As String = tBrush.GetType.BaseType.FullName
Debug.WriteLine(baseTypeName) ' System.Drawing.Brush と表示されます。

 

2-2.FileInfoクラスの場合

FileInfoクラスを例に「継承」という機能がどのように作用するのか見てみましょう。

FileInfoクラスは フォルダーを表すDirectoryInfoクラスとファイルを表すFileInfoクラスの親クラスです。

FileInfoクラスを使うと次のようにファイルの情報を取得することができます。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim file As New IO.FileInfo("C:\temp\test.txt")

If file.Exists Then
    '存在する場合サイズを出力
    Debug.WriteLine($"{file.Name} {file.Length \ 1024} KB")
    'Debug.WriteLine(file.Name & " " & file.Length \ 1024 & " KB") '←VB2013以前の場合
Else
    Debug.WriteLine($"{file.FullName} は存在しません。")
    'Debug.WriteLine(file.FullName & " は存在しません。") '←VB2013以前の場合
End If

Debug.WriteLineで出力される場所

ここで使っているFileInfoクラスのプロパティを表にして見ます。

プロパティ 意味
Exists ファイルが存在するかどうか。
Name ファイルの名前。フォルダー名は含まない。
Length ファイルのサイズ。バイト単位。
FullName ファイルの名前。フォルダー名を含む。

 

DirectoryInfoクラスも同じように扱うことができますが少し違います。

DirectoryInfo暮らすには サイズを表すLengthというプロパティはありません。フォルダーのサイズとはその中にあるファイルのサイズの合計のことなのでフォルダー自体にはサイズというものはないということです。その代わり、中にあるファイルを取得する EnumerateFiles というメソッドがあるので、下記の例ではそれをつかってファイル数を数えてみました。なお、このメソッドはVB2010以降でのみ使用可能で、それ以前の場合はGetFilesメソッドがその代わりになります。どちらのメソッドも引数なしで呼び出した場合、そのフォルダーに直接格納されているファイルだけを対象とし、子フォルダー・孫フォルダーの中のファイルは対象外になります。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim folder As New IO.DirectoryInfo("C:\temp\folder1")

If folder.Exists Then
    '存在する場合ファイル数を出力
    Debug.WriteLine($"{folder.Name} {folder.EnumerateFiles.Count} ファイル")
    'Debug.WriteLine(folder.Name & " " & folder.GetFiles.Length & " ファイル") '←VB2013以前の場合
Else
    Debug.WriteLine($"{folder.FullName} は存在しません。")
    'Debug.WriteLine(folder.FullName & " は存在しません。") '←VB2013以前の場合
End If

ここで使っているDirectoryInfoクラスのメソッド・プロパティを表にして見ます。

プロパティ 意味
Exists ファイルが存在するかどうか。
Name ファイルの名前。フォルダー名は含まない。
EnumerateFiles/GetFiles ファイルの数の取得。
FullName ファイルの名前。フォルダー名を含む。

FileInfoクラスの例と比較するとわかるようにExists, Name, FullNameはどちらでも共通します。確かにこれらの意味はファイルでもフォルダーでも同じです。この3つのプロパティは親クラスであるFileSystemInfoクラスで実装されています。そして、ファイルだけで意味があるLengthプロパティはFileInfoクラスで実装され、フォルダーだけで意味があるEnumerateFiles/GetFilesメソッドはDirectoryInfoクラスで実装されています。

メモ メモ  - EnumerateFiles と GetFiles の違い

ほとんどの場合 EnumerateFiles の方が優れているので、新しいバージョンではEnumerateFilesを優先的に使うようにしましょう。

GetFilesメソッドは、対象のファイルの一覧を作成してから次の処理にとりかかります。EnumerateFilesは対象のファイルがすべて見つからなくても列挙を開始します。そのため次の処理をしている間に続きのファイルを見つける時間が稼げるので処理が速いというわけです。とは言え、ファイルの数を数えるような処理の場合は、結局全部のファイルを見つけないと数え終わらないのでどちらを使っても大差ないはずです。

 

では、もう1歩すすめてフォルダーの中にある直接子ファイル・子フォルダーの一覧を出力するプログラムを書いて見ましょう。

DirectoryInfoクラスには子ファイルを列挙するEnumerateFiles/GetFilesメソッドと、子フォルダーを列挙するEnumerateDirectories/GetDirectoriesメソッドがあり、それぞれを使用するとこれが実現できますが、もう1つ子ファイルと子フォルダーの両方を1回で列挙できるEnumerateFileSystemInfos/GetFileSystemInfosというメソッドがあります。

それぞれで列挙される型は次のようになります。

列挙するメソッド 列挙される型
EnumerateFiles/GetFiles FileInfo
EnumerateDirectories/GetDirectories DirectoryInfo
EnumerateFileSystemInfos/GetFileSystemInfos FileSystemInfo

EnumerateFileSystemInfos/GetFileSystemInfosというメソッドの場合、列挙されるものがファイルの場合、FileInfoクラスのインスタンスが返されますし、フォルダーの場合、DirectoryInfoクラスのインスタンスが返されるかもしれません。対象のフォルダーの中に何があるかによって変わります。このような場合に、親クラスが役に立ちます。どちらの型が返ってくるかわからないので、FileInfo型やDirecotryInfo型の親クラスであるFileSystemInfo型として待ち構えておくことになります。(もし、都合よく共通の親クラスが存在しない場合は、最後の手段としてObject型で待ち受けることになるのです。)

これを使って次のように列挙できます。

実際にやってみる人は C:\temp\folder1 フォルダーの中に、複数のファイルと子フォルダーを格納しておくと試しやすいです。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim parentFolder As New IO.DirectoryInfo("C:\temp\folder1")

'For Each item As IO.FileSystemInfo In parentFolder.GetFileSystemInfos '←VB2008以前の場合
For Each item As IO.FileSystemInfo In parentFolder.EnumerateFileSystemInfos
    Debug.WriteLine(item.Name)
Next

これで、親フォルダーの中にあるファイルとフォルダーの名前の一覧を出力することはできました。

 

2-3.型の判断と分岐

プログラム上は列挙される変数 item は FileSystemInfo型 で定義していますが、実際にはファイルの場合はFileInfo型、フォルダーの場合にはDirectoryInfo型が返されています。

GetType.FullName を使うと型の名前を出力できるので、これを使って動作を確認してみましょう。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim parentFolder As New IO.DirectoryInfo("C:\temp\folder1")

'For Each item As IO.FileSystemInfo In parentFolder.GetFileSystemInfos '←VB2008以前の場合
For Each item As IO.FileSystemInfo In parentFolder.EnumerateFileSystemInfos
    Debug.WriteLine($"{item.Name} {item.GetType.FullName}")
    'Debug.WriteLine(item.Name & " " & item.GetType.FullName) '←VB2013以前の場合
Next

この例を実行すると、itemの型がSystem.IO.FileInfoまたはSystem.IO.DirectoryInfoと表示され、場合によって異なるということが確認できます。

 

プログラムの中では If item.GetType.FullName = "System.IO.FileInfo" Then のように記述することで、もしFileInfoクラスだったら・・・という条件分岐を書くことができますが、もっと効率的に書ける TypeOf (読み方:TypeOf=タイプオブ)というキーワードがあり、通常、型で条件分岐するときはこれを使います。次のようにIsを使った特殊な構文になります。

VB6 VB.NET 2002 VB.NET 2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

If TypeOf 変数 Is 型 Then

     変数が 指定した型 である場合(つまり、条件に当てはまる場合)に実行する処理

End If

これを使って条件分岐させてもLengthプロパティを使用することはできません。次の例はダメな例です。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim parentFolder As New IO.DirectoryInfo("C:\temp\folder1")

'For Each item As IO.FileSystemInfo In parentFolder.GetFileSystemInfos '←VB2008以前の場合
For Each item As IO.FileSystemInfo In parentFolder.EnumerateFileSystemInfos
    If TypeOf item Is System.IO.FileInfo Then
        '↓ここでビルドエラーです。
        Debug.Write($"{item.Name} {item.Length}")
    End If
Next

この例を入力すると Length のところに、赤い波線が表示されて、'Length' は 'FileSystemInfo' のメンバーではありません。 というエラーになります。

確かに item は As IO.FileSystemInfo で宣言されており、FileSystemInfo には Lengthプロパティが存在しません。If TypeOf を使って 実体が FileInfo型である場合だけ実行するプログラムなので、このまま実行してくれても良いように思いますが、見逃してもらえません。これがVBやC#, Javaなどの強い型付け言語の特徴です。実行してくれれば動くはずなんですが、型として定義されていないものを呼び出そうとしているのでそもそも実行してくれないわけです。強い型付け言語は、このことでエラーになることに事前に気がつけるというメリットを重視しています。

この問題の一般的な解決方法は型変換です。itemがFileInfo型に変換してから Length を呼び出しましょう。型変換には CType を使うのが万能ですが、このケースでは DirectCast (読み方:DirectCast=ダイレクトキャスト)の方が優れています。この例の場合、型はもともとFileInfoなのでなにも変換する必要は本来ないのですが、プログラム上As FileSystemInfoと宣言しているので、これをFileInfoとして扱って欲しいわけです。このように実体が変換されるわけではない場合 DirectCast は余計なことをせず高速に動作します。CTypeでも機能は問題がありませんが、CTypeでは実体を変換する可能性も含んだ処理が実行されるので少し遅くなります。

  使用例 何が起こるか?
CType CType(item, IO.FileInfo) itemをIO.FileInfo型に拡大変換または縮小変換します。
DirectCast DirectCast(item, IO.FileInfo) itemをIO.FileInfo型として扱います。実際の変換処理は行われません。itemはIO.FileInfo型であるか、その派生クラスである必要があります。CTypeより高速。

 

 

DirectCastを使って次のように書き換えると無事呼び出せます。ついでにDirectoryInfo型の場合はファイル数を表示するロジックも追加してみました。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim parentFolder As New IO.DirectoryInfo("C:\temp\folder1")

'For Each item As IO.FileSystemInfo In parentFolder.GetFileSystemInfos '←VB2008以前の場合
For Each item As IO.FileSystemInfo In parentFolder.EnumerateFileSystemInfos

    Debug.Write(item.Name & " ")

    If TypeOf item Is IO.FileInfo Then
        'ファイルの場合、サイズを出力
        Dim file As IO.FileInfo = DirectCast(item, IO.FileInfo)
        Debug.WriteLine(file.Length \ 1024 & " KB")
    ElseIf TypeOf item Is System.IO.DirectoryInfo Then
        'フォルダーの場合、ファイル数を出力
        Dim folder As IO.DirectoryInfo = DirectCast(item, IO.DirectoryInfo)
        Debug.WriteLine(folder.EnumerateFiles.Count & " ファイル")
        'Debug.WriteLine(folder.GetFiles.Length & " ファイル") '←VB2013以前の場合
    End If
Next

なお、プログラムがわかりにくくなるのでお勧めではありませんが、変数を経由しないで、DirectCast(item, IO.FileInfo).Length のように Length プロパティを呼び出すこともできます。

メモ メモ  - CType と DirectCast

CTypeは構文的上はDirectCastと同じです。上記の例では CType(Item, IO.FileInfo)のように記述します。本文でも説明しているように性能が悪いことに目をつむればすべてのDirectCastをCTypeに置き換えても問題ありません。性能の差もそれほど大きくはありません。プログラムが遅い原因のほとんどはこういった細かいキーワードの使い方よりもネットワークやディスクIOなど外部のデバイスの応答待ちです。

ところで、CTypeは第2引数に構造体を指定することができますが、DirectCastはできません。というのはDirectCastは実体に対しては何の変換も行わないので、親クラスになれない構造体では使用するシーンがないからです。DirectCast(item, IO.FileInfo) item の記述上の型(FileSystemInfo)をFileInfoに変換しようとしています。つまり、第2引数で指定されている型が第1引数の型の子クラスである場合にだけ使用できるのです。

 

メモ メモ  - 上のプログラムの Else なんですが・・・

上記のプログラムで If文はまずFileInfoクラスであるか確認して、そうでない場合DirectoryInfoクラスであるか確認しています。つまり、FileInfoクラスでない場合は、DirectoryInfoクラスであるというような決め付けをしていないということです。FileSystenInfoクラスのリファレンスには派生クラスとして FileInfoクラスとDirectoryInfoクラスしか記載されていないので問題ないように思いますが・・・

FileSystemInfo クラス (System.IO) | Microsoft Docs

実のところ、この例では DirectoryInfoクラスであるか確認する必要はほぼありません。唯一、気になるのは、誰かが FileSystemInfoクラスを継承した新しいクラスを作る可能性がゼロではないということです。その誰かはあなたのチームの一員かもしれませんし、新しい機能を思いついたマイクロソフトの開発者かもしれません。それでもまだ、EnumerateFileInfos/GetFileInfosメソッドが、その新しいクラスを生成しない限り問題にならないので、まずこのケースでは気にする必要はないでしょう。でも、決め付けないで確認するようにしておけば、万一のことがあっても安定して動作しますよね。そのことを意識してもらいたかっただけです。正直過剰かもしれません。このケースで私のチームのプログラマーがDirectoryInfoクラスである確認を省略していても、私は何も言わないでしょう。 

 

2-4.TryCast

上記の例はIf文で FileInfo型であることを確認してからFileInfo型に変換するプログラムになっており二度手間です。TryCast (読み方:TryCast=トライキャスト)を使用すると、少しスマートに記述できる場合があります。

DirectCastやCTypeの場合、変換に失敗するとInvalidCastExceptionが発生しますが、TryCastは例外が発生せずに結果がNothingになります。これを利用するとたとえば、次のようなプログラムができます。

VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim file As IO.FileInfo = TryCast(item, IO.FileInfo)

If file IsNot Nothing Then
    'ファイルの場合、サイズを出力
    Debug.WriteLine(file.Length \ 1024 & " KB")
End If

 

2-5. まとめ

ここまで学んだことを整理してみます。

 

・派生型は基本型の変数に代入することができます。

FileInfoクラスとDirectoryInfoクラスがFileSystemInfoクラスに代入されて列挙される例を一緒に見ました。

 

・Object型はすべての型の基本型なので、どのような値でもオブジェクト型に代入することができます。

 

・2つ以上の型が取得される可能性がある場合に、この基本型の機能が役に立ちます。

EnemerateFileSystemInfos/GetFileSystemInfosメソッドは、対象がファイルかフォルダーかで生成する型が違うので、その両方の親クラスであるFileSystemInfo型して定義されていることを見ました。

 

・親クラスの型で定義されている場合、実体は子クラスであっても、子クラスで実装されているメソッドやプロパティを直接呼び出すことはできません。

FileSystemInfo型の変数 item から Length プロパティを呼び出せないことでこのことを学びました。

 

・実体が子クラスの場合 DirectCast などを使用して子クラスの型に変換することで、子クラスで実装されているメソッドやプロパティを呼び出すことができるようになります。

FileSystemInfo型の変数 item を FileInfo型に DirectCast して Length プロパティを呼び出す例を紹介しました。

 

 

3.インターフェース

継承による親子関係とは別に、横の関係を作れるのがインターフェースです。たとえば、機能上はまったく関連のないクラスだけれども、使い終わったときに終了処理を呼び出す必要がある というクラスはたくさんあります。あるいは、ファイルの一覧であるとか、ウィンドウの一覧であるとか、フォントの一覧であるとか、それぞれ関係はないのだけれども「n番目の項目を取得する機能がある」という点は共通しているという場合もあります。

このような関係を.NETではインターフェースというもので表現することができます。親子関係とは違ってインターフェースは複数実装することができます。

たとえば、配列とList はどちらも IListインターフェースを実装しているので、次のようにIList型で宣言した変数に代入することができます。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim array As String() = {"A", "B", "C", "D", "E"}
Dim list As New List(Of String) From {"X", "Y", "Z"}

Dim i1 As IList = array
Dim i2 As IList = list

インターフェースの名前は I から始まるという慣例があるので、プログラムの中で IXxxxxx という名前を見かけるとインターフェースであることが多いです。

メソッドの引数にインターフェースを要求される場合もあります。下の画像の例では IComparer(Of String)インターフェースを実装しているオブジェクトが引数として必要です。

インターフェースはとても便利な存在なのですが、少し小難しい存在ですでの、役に立つ個別の例を1つずつ理解していくのが良いと思います。ここではインターフェースの説明は割愛させていただきます。

 

 

4.Object型が役に立つとき

4-1.Object型には何でも代入できる

すでに説明したように Object型は継承階層の頂点にある特別な型であり、すべての型の基本型です。そのため Object型にはすべての型のすべての値を代入できます。

次のコードはどれも有効です。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim o1 As Object = 123 '数値(Integer)
Dim o2 As Object = "ABC" '文字列(String)
Dim o3 As Object = New List(Of String)
Dim o4 As Object = New Point(10, 20)
Dim o5 As Object = False
Dim o6 As Object = Nothing

しかし、このようなObject型の使用方法は決してお勧めではなく、プロのプログラマーがこのようなプログラムを書いていたとしたら、それだけでもうその人にはプログラムを記述する仕事は任せられないと思うレベルです。

メモ メモ  - プロ失格

大げさではなく本当にそう思います。とはいえ、私はこのような記事を書いているくらいですので、そのような人がいたら一生懸命これがダメな理由を説明し始めそうな気もしますが・・・。

 

4-2.Object型のダメな使い方

ダメな理由をもう少し掘り下げて見ます。(その後で、Objectの良い活用方法を説明します。)

変数の型をObjectにしてしまうと、実質的にプログラム上型の区別ができなくなってしまいます。

たとえば、次の関数は引数valueの文字数(正確にはUnicodeのコードポイントの数。以下この注記は省略して「文字数」を記述します)を返す関数です。valueの型は String であるべきですが、あえてObjectにしてダメな点を確認してみます。

このコードを実行するにはOption StrictがOffである必要があります。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Function GetCharCount(value As Object) As Integer

    Return value.Length

End Function

このプログラムでは文字数を取得するのにStringのLengthプロパティを使用しています。

引数がObject型なので、次の呼び出しはすべて有効です。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim result1 As Integer = GetCharCount(123) '数値(Integer)
Dim result2 As Integer = GetCharCount("ABC") '文字列(String)
Dim result3 As Integer = GetCharCount(New List(Of String))
Dim result4 As Integer = GetCharCount(New Point(10, 20))
Dim result5 As Integer = GetCharCount(False)

数値を引数にして呼び出すと、Lengthプロパティが存在しないため例外が発生します。発生する例外は MissingMemberException です。

しかし、このプログラムはVBのエラーチェックをすり抜けてコンパイルして実行することができてしまうので、そのことに気が付くのは実行したときになってしまうかもしれません。

つまり、実行してみるまでエラーかどうかわからないし、使い方によってもエラーかどうか変わるということです。

実用的なプログラムは数千行、数万行というボリュームになるので、この中にこのようなプログラムがたくさんあったらどうなるでしょうか?一生懸命テストしても、エラーになる場合があることに気が付かないかもしれません。ある日、顧客からのクレームではじめて問題があることが発覚するかもしれません。

Objectがダメなことがわかったので、GetCharCount関数の引数の定義を value As String にしてみましょう。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Private Function GetCharCount(value As String) As Integer

    Return value.Length

End Function

 

これでList型とPoint型を使った呼び出しにはVBが赤い波線を表示して実行できないことを教えてくれます。

ListとPointの箇所に赤い波線が表示される

Option Strict が Off なので 123 と False は文字列型に自動変換されて実行できるようになります。例外は発生しません。result3 と result4 の行をコメントにして実行すると、result1 は 3 、result5 は "False"が5文字なので 5 になります。

自動変換も気をつけないとエラーの元になるので業務用のプログラムでは通常 Option Strict を On にします。そうすると、文字列以外の引数を与えているところはすべて赤い波線になります。

文字列以外の引数に赤い波線が表示される

これで大事な顧客からクレームが来る前に、プログラムに問題があることに気が付くことができるようになります。それどころか、プログラムを記述した途端にこの赤い波線が表示されるので、先輩に怒られる心配すらありません。

 

4-3.あらゆるものを対象にする

今度はObject型が役に立つ場合をみてみましょう。

プログラムでどんなものでも対象にできる機能を作成したい場合にはObject型を使う必要があります。

たとえば、この関数は引数に渡されたオブジェクトのプロパティと値の一覧を簡易的にデバッグウィンドウに出力します。その型が何であれ動作します。

VB2015 VB2017 VB2019

Private Sub OutputProperties(o As Object)

    Debug.Indent()
    o.GetType.GetProperties.ToList.ForEach(Sub(p) Debug.WriteLine($"{p.Name}={p.GetValue(o)}"))
    Debug.Unindent()

End Sub

次のように呼び出せます。日付型であるNowでも、FileInfo型でも対象にできます。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Debug.WriteLine("Now")
OutputProperties(Now)

Dim file As New IO.FileInfo("C:\Windows\notepad.exe")
Debug.WriteLine("file")
OutputProperties(file)

 

発展 発展学習  -  ラムダ式

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

上記のOutputProperties関数の例、プロパティと値の一覧を出力するところは本題ではないのでコンパクトに1行で書いてしまいました。下記のプログラムとほぼ同じ意味です。

VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

For Each p In o.GetType.GetProperties
    Debug.WriteLine(p.Name & "=" & p.GetValue(o).ToString)
Next

ForEachメソッドはコレクションの各要素それぞれに対して処理を実行してくれるので、For Each ~ Next の変わりになります。コンパクトに書きたいときに重宝します。

GetTypeメソッドは型の情報を取得するメソッドです。型情報を利用したプログラミングをリフレクションと呼びます。サンプル集でリフレクションのサンプルを公開しています。

ForEachメソッドの引数に Sub(p) ・・・ と書いてある部分は、繰り返し実行する処理で、なんとなく意味はわかると思います。VBの機能としてはこれは ラムダ式 と呼ばれるものです。どれも初級講座ではまだ登場していません。(今後も初級講座で扱うかどうかはわかりません)

 

 

4-4.遅延バインディング

メソッドやプロパティを使用したいけれども実行するまで型がわからないという場合があります。

この場合、プログラム時点ではObject型の変数に代入するしかありません。

Option Strict を Offにするとプログラム時点では存在しているかわからないメソッドやプロパティを呼び出せるようになるので、この2つを組み合わせれば実行時点で存在しているメソッドやプロパティを呼び出すことができます。これを遅延バインディングと言います。事後バインディング、実行時バインディング、レイトバインディングなどいくつかの呼び方があります。(これに対して通常のプログラム時点で型がわかっているやり方を事前バインディング、アーリーバインディングなどと呼びます。)

遅延バインディングを使用する例の代表は COM (読み方:COM = コム)を扱う場合です。

COM というのは、.NETとは別のプログラムの仕様です。C++ や VB6 を使うと COM を作成することができます。VB6まではVBも.NETではなかったのです。

COMで動作する最も有名なアプリケーションは Excel です。

COM の世界にはCOMの型があります。.NETではないので、IntegerやStringやFileInfoなど.NETの型は当然存在しません。

 

VB(.NET)でCOMを扱うお手軽な方法は CreateObjectメソッド(読み方:CreateObject = クリエイトオブジェクト)を使用することです。

たとえば、次のようにして Excel で定義されている Applicationオブジェクトのインスタンスを作成することができます。

なお、.NET CoreではCreateObjectの扱いが少し面倒になっているので、この例を実際に試す場合は .NET Frameworkで試してください。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019


Dim
excel As Object = CreateObject("Excel.Application")

CreateObjectの引数に指定している Excel.Application という文字列はProgram IDというもので、通称 ProgId (プログアイディー)と呼ばれます。これはCOMの世界で、COMを識別するために使われます。ProgIdはWindowsのレジストリに登録されており、その登録をみればCOMのプログラムの実体がどこにあるかわかるようになっています。

これで変数 excel に ExcelのApplicationオブジェクトのインスタンスが代入されますが、.NETのものではないので、ここでは Object型 にするしかありません。

 

ExcelのApplicationオブジェクトにはたくさんのメソッドやプロパティがあり、ここにリファレンスがあります。

Application オブジェクト (Excel) | Microsoft Docs

 

ためしに GetPhoneticメソッドを呼び出してみましょう。このメソッドは漢字の日本語の読み仮名を返してくれます。

この例を実行するにはOption Strict をOffにする必要があります。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim excel As Object = CreateObject("Excel.Application")
Dim yomi As String = excel.GetPhonetic("中学校")

Debug.WriteLine(yomi) 'チュウガッコウ と表示されます。

'Excelを終了します。
excel.Quit
Marshal.ReleaseComObject(excel)

Debug.WriteLineで出力される場所

なお、GetPhoneticメソッドは、Microsoft Officeで日本語機能がインストールされている場合のみ使用できるとのことです。

 

メモ メモ  - COMの事前バインディング

COMは.NET登場以前から存在し、2021年現在でもWindowsを支えるている重要な技術です。そのため、.NETはCOMを便利に取り扱えるような特別な仕組みが用意されています。

「COM参照」を使用すると COMを事前バインディングすることができます。事前バインディングなのでCOMのオブジェクトをまるで.NETのクラスのように扱うことができます。使用できるメソッドやプロパティもインテリセンスで表示されます。実体がCOMであることを忘れてしまうほどです。とは言え、やはりCOMと.NETは違う存在なので、COMオブジェクトが不要になった場合はMarshal.ReleaseComObjectを呼び出すべきであるなど独特のルールもあります。

.NET FrameworkでCOM参照するには、プロジェクトを右クリックして、[追加] - [参照] を選択します。参照マネージャーが表示されるので、左端で COM を選択します。そうすると一覧に実行中の端末に存在するCOMが大量に表示されます。この中から Microsoft Excel XX.X Object Library (XX.Xはバージョン番号) を選択してOKをクリックすると、COM参照の完了です。

COM参照している場合はCreateObjectではなく、.NETのクラスのようにNewを使ってインスタンスを作成できます。たとえば、次のように記述できます。

VB2002 VB2003 VB2005 VB2008 VB2010 VB2012 VB2013 VB2015 VB2017 VB2019

Dim excel As New Microsoft.Office.Interop.Excel.Application
Dim yomi As String = excel.GetPhonetic("中学校")

Debug.WriteLine(yomi) 'チュウガッコウ と表示されます。

'Excelを終了します。
excel.Quit
Marshal.ReleaseComObject(excel)

このやり方だとObject型が不要になるので、Option Strict Onでも実行できます。

なので、Excelを含めてCOMを扱うほとんどの場合では、CreateObjectで遅延バインディングするよりもCOM参照を使うのが優れています。本文中でExcelで遅延バインディングする例をとりあげたのは、Excelならほとんどの人がよく知っていて、実際に試せる人も多いと思ったからです。

何かの事情でCOM参照したくない/できない場合に、CreateObjectを使うことになりますが、そのような事情はほとんどありません。開発する端末に対象のCOMが存在しない場合、CreateObjectのプログラムを書くことはできますが、COM参照はそもそも不可能です。プラグインのような仕組みを開発している場合、CreateObjectを使うと気軽にテストできるかもしれません。プラグイン側が.NETではない場合は、事前の参照設定は不可能なのでCreateObjectを使わないとそもそも呼び出せない場合もありえます。COM参照で生成される名前空間名などは環境によって異なる場合があります。これが困る場合はCreateObjectを使用します。サンプルプログラムを書くときにCreateObjectであればコピー&貼り付けするだけで動作するかもしれませんが、COM参照ではそういうわけにはいきません。このあたりがCreateObjectを使用する数少ない理由になると思います。