Visual Basic 初級講座 [改訂版] |
![]() ![]() ![]() ![]() ![]() ![]() ![]() ![]() |
Visual Basic 中学校 > 初級講座[改訂版] >
2020/9/6
この記事が対象とする製品・バージョン
![]() |
Visual Basic 2019 | ◎ | 対象です。 |
![]() |
Visual Basic 2017 | ◎ | 対象です。 |
![]() |
Visual Basic 2015 | ◎ | 対象です。 |
![]() |
Visual Basic 2013 | ◎ | 対象です。 |
![]() |
Visual Basic 2012 | ◎ | 対象です。 |
![]() |
Visual Basic 2010 | ◎ | 対象です。 |
![]() |
Visual Basic 2008 | ◎ | 対象です。 |
![]() |
Visual Basic 2005 | ◎ | 対象です。 |
![]() |
Visual Basic.NET 2003 | △ | 対象外ですがほとんどの説明があてはまるので参考になります。 |
![]() |
Visual Basic.NET (2002) | △ | 対象外ですがほとんどの説明があてはまるので参考になります。 |
![]() |
Visual Basic 6.0 | × | 対象外です。 |
初級講座ではこれまで既存のクラスを利用する方法を説明し、いろいろなサンプルで利用してきました。
今回は自分でクラスを作成する方法を説明します。
クラスを作成するのはとても簡単です。次のように記述するだけです。
Public Class
SampleClass End Class |
これでSampleClassという空のクラスが作成されます。
このプログラムをどこに書くのかというと、たとえば、Windowsフォームアプリケーションの場合、
■画像1:Classの外に別のClassを定義
ここでも良いですし、
■画像2:Classの中に別のClassを定義
ここでも良いのですが、ClassにClassを入れ子にする画像2の位置だと、親クラスと子クラスというような関係になり扱い方が変わるので、普通はこの画像2の書き方はしません。意図的にクラスを親子関係にすることはありますがレアな使い方です。
それから、クラスはVBにおけるプログラムの主役であり、本格的なプログラムを作成するとたくさんのクラスを作って、それぞれのクラスの中にもたくさんプログラムしていくことになります。
そのため、クラスごとにファイルを分けることが最も良いとされています。
その操作方法は、ソリューションエクスプローラでプロジェクトを右クリックして[追加] - [クラス]を選択してから、クラスが選択されているのを確認し、名前にクラス名(この場合、SampleClass)を入力し、「追加」を押すことです。
この操作をすることで、ソリューションエクスプローラーにSampleClass.vbというファイルが追加され、その中に、さきほどのClassのプログラムが生成されます。
クラスの中のプログラムについてはすぐ後で説明しますが、どういうものか少しなれておきましょう。
Windowsフォームアプリケーションを作成し、プロジェクト名を ClassSample としてください。そして今説明した手順でSampleClass.vbを作成し、SampleClass.vbの中に次のようにプログラムしてください。
※もし、読者の方がためしに画像1・画像2の場所にプログラムを書いてみたのであればそれは消しておいてください。同じ名前のクラスが複数あるとおかしなことになってしまいますので。
Public Class
SampleClass ''' <summary> ''' 名前です。 ''' </summary> Public Name As String ''' <summary> ''' 足し算します。 ''' </summary> Public Function Add(x As Integer, y As Integer) As Integer Return x + y End Function End Class |
このプログラムではSampleClassクラスにNameというフィールドと、たし算を行うAddというメソッドを記述しています。
これで、今度はForm1のプログラムに戻ってボタンを貼り付けてClickイベントからSampleClassを呼び出すプログラムを書いてみてください。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim sample As New SampleClass sample.Name = "徳川家康" MsgBox(sample.Name) Dim answer As Integer answer = sample.Add(3, 4) MsgBox(answer) End Sub |
プログラム中に表示されるインテリセンスでプログラムした「Add」やXMLコメントで記述したヒントが表示されるのに気が付いたでしょうか?
それから、NameフィールドとAddメソッドしか追加していないはずなのに、EqualsやGetHashCodeなどいくつかのメソッドが使用できるようになっています。
これらのメソッドはObjectクラスで定義されているメソッドです。VBではすべてのクラスはObjectクラスから機能を引き継ぐことになっているので、自分では何もしていなくても自動的にObjectクラスのメンバーが自作のクラスから呼び出せる状態になります。
今後クラスを作成する場合は、基本的にこの手順で、ソリューションにファイルを追加する形でクラスを追加してください。
※ただ、お手軽に簡単に試したい場合は画像1の場所にクラスを追加することもありますが、本当にこれはお手軽にちょっと試す使い方だけにしておいたほうが良いです。
クラスに「MyClass」という名前をつけることはできません。MyClassが予約語だからです。 |
さきほどの Addメソッドの例では SampleClassというクラスを新しく作成してその中にメソッドを記述しました。
前回メソッドの作成方法を説明したときにはWindowsフォームアプリケーションに最初から存在するForm1クラスの中にメソッドを追加しました。
Form1にもメソッドを追加できるのに、なぜ先ほどはSampleClassというクラスを新規作成したのでしょうか?
メソッドをどこのクラスに追加するかは場合によります。 初心者は何でもかんでも Form1 にプログラムしてしまうことがよくあり、複数のクラスを連携させて動作させるプログラムの醍醐味に気づくのが遅い傾向があるからです。学習のはじめの方でクラスを新規作成してForm1以外の場所にメソッドが作れることを知っておけばこの罠にはまることが少しは少なくなりそうです。
From1とは、フォームを表すオブジェクトです。つまり、1つの画面です。Form1にプログラムすべきことはこの画面を制御するプログラムで、それ以外のプログラムを書くべきではありません。どこにプログラムしても動くことは動くので、何をすべきで何をすべきでないかは個人が好みで決めてよいという側面もあります。しかし、プログラムの上級者はクラスをうまく組み合わせて信じられないような相乗効果をもたらすことができます。そのような上級プログラマーはみな同じことを言うと思いますよ。初心者のうちはまだいろいろ理解できずにわけがわからないこともあると思いますが、ひとまず上級者の言うことを聞いておくのが無難でしょう。
とは言え、簡単な練習やお試し・サンプルではForm1にすべてをプログラムしてしまうことも良くあります。そのような例を見かけたら、練習やサンプルだと割り切ってください。
簡単なクラスを作って肩慣らししてみましょう。
Webからファイルをダウンロードしてファイルとして保存する DownloadCommander というクラスを作ってみます。
このクラスには単にファイルをダウンロードするだけではなく、ダウンロードしたものの履歴を取っておく機能を持たせます。
Webからファイルをダウンロードする機能を持つクラスは .NET のフレームワークにもあります。たとえばWebClientクラス(読み方:WebClient = ウェブクライアント)です。DownloadCommanderクラスも内部ではこのWebClientクラスの機能を使います。 |
まずは、Windows フォームアプリケーションで新しいプロジェクトを作成してください。名前は DownloadCommanderSample としましょう。
プロジェクトの作成が完了したら、新しくクラスを作成してください。名前は「DownloadCommander」、つまりダウンロード司令官としましょう。
その操作方法は上述していますが念のために繰り返すと、ソリューションエクスプローラでプロジェクトを右クリックして[追加] - [クラス]を選択してから、クラスが選択されているのを確認し、名前に DownloadCommander と入力し、「追加」を押すことです。
DownalodCommanderクラスに次のようにプログラムしてください。
このプログラムはDownloadメソッドを定義しています。Downloadメソッドでは WebClientクラスの機能を使ってファイルをダウンロードして保存します。
Public
Class DownloadCommander Dim wc As New Net.WebClient() ''' <summary> ''' url からファイルをダウンロードし、folderNameに保存します。 ''' </summary> Public Sub Download(url As String, folderName As String) Dim localPath As String localPath = IO.Path.Combine(folderName, IO.Path.GetFileName(url)) wc.DownloadFile(url, localPath) End Sub End Class |
ダウンロードしてローカルに保存する機能はWebClient.DownloadFileメソッドをそのまま使用します。IO.Path.GetFileNameはURLやフルパスからファイル名だけを抜き出します。IO.Path.Combineはフォルダーやファイル名を結合して1つのパスにします。たとえば、IO.Path.Combine("C:", "temp", "test.txt) は "C:\temp\test.txt"を返します。 Combineメソッドを使うとMacやLinuxで実行した場合には、それぞれの環境に応じた区切り文字を使用してくれるので、複数のOSで動作する可能性がある場合に役に立ちます。.NET Coreまたは.NET 5以上の場合は、IO.Path.Joinメソッドも使用できます。
この時点で、このようになっています。
これだけでダウンロード機能は完成です。このクラスを利用するプログラム書いて見ましょう。
ソリューションエクスプローラーで Form1.vb をダブルクリックして画面のデザイナーを開いてください。
そして、フォームにボタンを1つ配置して、次のようにDownloadCommander.Downloadメソッドを呼び出すとあなたのパソコンのCドライブ C:\temp\souri01.jpg に初代総理大臣である伊藤博文の写真がコピーされます。
C:\temp フォルダーが存在しなかったりアクセス権がなかったりするとエラーになりますのでフォルダーは都合が良いように変えてください。
Private
Sub Button1_Click(sender As
Object,
e As
EventArgs) Handles Button1.Click Dim commander As New DownloadCommander Dim url As String = "http://www.kantei.go.jp/jp/rekidai/souri/images/souri01.jpg" Dim folder As String = "C:\temp" commander.Download(url, folder) 'エクスプローラーでフォルダーを開きます。 Process.Start(folder) End Sub |
これで、簡単なクラスを作ってメソッドを実装し、それをフォームから呼び出して利用するということができました。
次はダウンロード履歴をログファイルとして作成する部分をプログラムしてみます。このためにとりあえずDownloadメソッドにログファイル名を指定する引数 logFileName を追加して次のように改造します。
Public
Class DownloadCommander Dim wc As New Net.WebClient() ''' <summary> ''' url からファイルをダウンロードし、folderNameに保存します。 ''' </summary> Public Sub Download(url As String, folderName As String, logFileName As String) '▼ファイルのダウンロード Dim localPath As String localPath = IO.Path.Combine(folderName, IO.Path.GetFileName(url)) wc.DownloadFile(url, localPath) '▼履歴をログに書き込む Dim description As String description = Now.ToString("yyyy\/MM\/dd hh\:mm\:ss") & ", " description &= Environment.UserName & ", " description &= url & ", " description &= localPath description &= Environment.NewLine '最後に改行 IO.File.AppendAllText(logFileName, description) End Sub End Class |
■リスト:DownloadCommanderクラスのプログラム。Downloadメソッドを改造してログの記録を実現したもの。
呼び出し側も新しく追加された引数に対応しなければならないので次のようになります。
Private
Sub Button1_Click(sender As
Object,
e As
EventArgs) Handles Button1.Click Dim commander As New DownloadCommander Dim url As String = "http://www.kantei.go.jp/jp/rekidai/souri/images/souri01.jpg" Dim folder As String = "C:\temp" Dim logFileName As String = "C:\temp\dllog.txt" commander.Download(url, folder, logFileName) 'エクスプローラーでフォルダーを開きます。 Process.Start(folder) End Sub |
■リスト:フォーム側のプログラム。ログ記録機能つきのDownloadメソッドの呼び出し。
これで指定したファイルをダウンロードしてログに履歴を記録してくれるメソッドを持ったクラスが完成しました。
これで一応ファイルをダウンロードして履歴を保存する機能は完成しましたが、使いにくい点があります。
複数のファイルをダウンロードしたい場合、次のようなプログラムになってしまいます。
Dim commander
As New
DownloadCommander Dim url1 As String = "http://www.kantei.go.jp/jp/rekidai/souri/images/souri01.jpg" Dim url2 As String = "http://www.kantei.go.jp/jp/rekidai/souri/images/souri02.jpg" Dim folder As String = "C:\temp" Dim logFileName As String = "C:\temp\dllog.txt" commander.Download(url1, folder, logFileName) commander.Download(url2, folder, logFileName) |
この例では伊藤博文と黒田清隆(読み方:黒田清隆 = くろだきよたか)の写真がダウンロードされます。
黒田清隆は2代目の内閣総理大臣で帝国憲法が発布されたときの首相でもあります。北海道開拓使官有物払下げ事件で今でも教科書に載っているでしょうか?個人的には明治天皇とは馬が合わず、また自分の妻を殺したという疑惑がいまだにささやかれていて歴史ミステリの1つとなっています。
URLを2個使うのは当然として、保存先のフォルダー名やログファイル名は2回指定しなくてもいいはずです。
この点を改善するために保存先のフォルダー名とログファイル名は DownloadCommanderクラスのプロパティにして見ましょう。
VB2010以上では Propertyキーワード (読み方:Property=プロパティ)を使うことで普通の変数と同じ感覚でプロパティを作成できます。
DownalodCommanderクラス側に次のようにプログラムを追加してください。
Public Class
DownloadCommander Public Property FolderName As String '←追加 Public Property LogFileName As String '←追加 'VB2008以前の場合、プロパティの構文が少し複雑なのでこのサンプルでは下記のようにフィールドを利用します。 'Public FolderName As String 'Public LogFileName As String Dim wc As New Net.WebClient() ''' <summary> ''' url からファイルをダウンロードし、folderNameに保存します。 ''' </summary> Public Sub Download(url As String, folderName As String, logFileName As String) 省略 End Sub '↓追加 Public Sub Download(url As String) Me.Download(url, FolderName, LogFileName) End Sub End Class |
さらに、folderName引数とlogFileName引数がない、Downloadメソッドを追加します。同じ名前のメソッドでも引数が違うものを複数定義できるというのは前回説明していますね。オーバーロードというのでした。
この新しく追加するDownloadメソッドは、引数が違うだけで機能はさきほど作った引数が3つあるものと同じなので、同じプログラムを繰り返す必要はなく、さきほど作った引数が3つあるほうの Downloadメソッドを呼び出すプログラムだけ書いておけばよいです。
ここで1つポイントがあります。
引数が1つの方のDownloadメソッド内では FolderName や LogFileName という名前(識別子)は、プロパティを表しています。一方、引数が3つあるほうのDownloadメソッド内では folderName や logFileName はプロパティではなく引数を表しています。VBでは大文字と小文字を区別しないで、大文字と小文字の違いからこの区別が発生しているわけではありません。
この区別は以前説明した 名前の衝突 によるものです。
引数が3つあるほうのDownloadメソッド内では、folderName という名前(識別子)に合致するものが、folderName引数とFolderNameプロパティの2つあるという状態になっています。これが名前の衝突です。名前が衝突した場合、近くにある方が優先的に使用されます。近くというのは物理的な行数ではなく、意味的な近さを指しています。メソッド内のプログラムにとっては自分のメソッドの引数の方が、クラスのプロパティよりも近い存在です。そのため、引数が3つあるほうのDownloadメソッド内では単に folderName、logFileName と書いた場合、引数の方を指しているとみなされます。なお、今回は不要ですが、このケースでプロパティの方を使いたい場合は、Me.FolderName、Me.LogFileのようにクラスのメンバーであることを明示します。
このような面倒なことを考えなくて良いように、引数の名前を違うものにすることもできます。そのようなスタンスを好むプログラマーもおそらくたくさんいるでしょう。たとえば、引数の名前をparamFolderName などにするだけで名前の衝突から開放されます。それでも私がこの2つを同じ名前にする理由は、プロパティや引数の名前にはそのものの意味を表す名前を付けるとプログラムがわかりやすくなる、そして、このプログラムではプロパティと引数は同じ意味を表しているということです。具体的に言うとFolderNameプロパティは保存先のフォルダーという意味であり、folderName引数も保存先のフォルダーという意味です。同じ意味ならば同じ名前を付けたほうがわかりやすいというのが私のスタンスです。ただ、そのために名前の衝突というわかりにくい状況が発生しているので、このスタンスをすべての人に推奨するものではありません。
さて、 これで、次のようにすることで、複数のファイルをダウンロードする場合でも、保存先のフォルダーや、ログファイル名は1回だけ指定すればよいようになりました。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim commander As New DownloadCommander commander.FolderName = "C:\temp" commander.LogFileName = "C:\temp\dllog.txt" commander.Download("http://www.kantei.go.jp/jp/rekidai/souri/images/souri01.jpg") '伊藤博文 commander.Download("http://www.kantei.go.jp/jp/rekidai/souri/images/souri02.jpg") '黒田清隆 commander.Download("http://www.kantei.go.jp/jp/rekidai/souri/images/souri03.jpg") '山県有朋 'エクスプローラーでフォルダーを開きます。 Process.Start(commander.FolderName) End Sub |
しかも、Downloadメソッドには、保存先とログファイル名を引数で指定できるオーバーロードもあるので、特別に保存先を替えたい場合があれば、そのオーバーロードを呼び出すこともできます。
たとえば、2代目の総理大臣 黒田清隆だけは kurodaフォルダーに保存するには次のようになります。kurodaフォルダーが存在しないとエラーになるのでご注意ください。
commander.Download("http://www.kantei.go.jp/jp/rekidai/souri/images/souri01.jpg")
'伊藤博文 commander.Download("http://www.kantei.go.jp/jp/rekidai/souri/images/souri02.jpg", "C\kuroda", commander.LogFileName) '黒田清隆 commander.Download("http://www.kantei.go.jp/jp/rekidai/souri/images/souri03.jpg") '山県有朋 |
これで、なかなか良いクラスができました。
でも、まだ改良できます。
今の状態だと、引数が1個の方のDownloadメソッドを呼び出す前に FolderNameプロパティとLogFileNameプロパティを設定しておく必要があります。
そうするとこのクラスを誰か別の人に使ってもらうときに注意しないといけないですよね。「最初にFolderNameプロパティとLogFileNameプロパティをセットしておいてくださいね」と。
大勢の人に配ったりする場合は、直接口で伝えることはできないので、この注意はどこかに書いておくことになります。でも、使う人はその注意をちゃんと読むでしょうか?
あなたははじめてのクラスを使うときに、クラスのドキュメントを見て注意事項を全部確認してから使いますか?答えはもちろんノーですよね。
だから、読まないとわからない注意事項がある という状態はそれだけで使いにくいんです。
どうすればよいでしょうか?
間違った使い方をされた場合に、親切で具体的なエラーメッセージを表示するか、そもそも間違った使い方をできないようにするというのが一般的なアプローチです。
まずはエラーメッセージを表示するように改良してみましょう。エラーの扱い方はまだ初級講座では説明していないので少し先取りになってしまいます。そのため詳細は割愛しますが、引数が1つの方のDownloadメソッドを次のようにすると親切なメッセージを表示できます。
Public Sub Download(url
As String) If String.IsNullOrEmpty(Me.FolderName) Then Throw New InvalidOperationException("Downloadメソッドを呼び出す前にFolderNameプロパティに存在するフォルダーを指定してください。このフォルダーにダウンロードしたファイルを保存します。") End If If String.IsNullOrEmpty(Me.LogFileName) Then Throw New InvalidOperationException("Downloadメソッドを呼び出す前にLogFileNameプロパティに書き込み可能なファイルのフルパスを指定してください。このファイルに履歴を書き込みます。") End If Me.Download(url, FolderName, LogFileName) End Sub |
これでよく知らない人が、FolderNameプロパティの設定をしないで、引数1個のDownloadメソッドを呼び出してもちゃんとエラーメッセージに何がダメでどうすればよいのか表示されるようになります。
エラーについては別の回に説明しますが、簡単に説明しとくと Throw は例外(エラー)を発生させる命令です。例外にも種類があり、今回は InvalidOperationException という名前の例外を発生させています。これは、きまったやり方があるのにその通りにやっていない 場合に発生する例外なので今回の例にぴったりです。
そもそも間違った使い方をできないような作り変えも紹介しておきましょう。
つまり、Downloadメソッドを呼び出す前に、必ずプロパティが設定されるようにするのです。
これにはいくつか方法があります。1つはプロパティに初期値を設定する方法です。
プロパティやフィールドに初期値を設定するには = 初期値 を記述するだけなので簡単です。
Public Class
DownloadCommander Public Property FolderName As String = "C:\temp" Public Property LogFileName As String = "C:\temp\dllog.txt" 'VB2008以前の場合、プロパティの構文が少し複雑なのでこのサンプルでは下記のようにフィールドを利用します。 'Public FolderName As String = "C:\temp" 'Public LogFileName As String = "C:\temp\dllog.txt" 省略 End Class |
コンストラクターを使って初期値を設定する方法もあります。
コンストラクターはクラスのインスタンスを生成する機能をもったメンバーであるということは既に説明しています。呼び出し側で New DownloadCommader のように New を使用するとDownloadCommanderのコンストラクターが実行されます。
コンストラクターを何も定義していない場合はインスタンスを生成するだけで他には何も処理をしないコンストラクターが自動的に作成されます。コンストラクターに追加の処理をさせたい場合は、自分でコンストラクターを定義することもできます。コンストラクターの定義は見かけ上は名前が New であるメソッドを定義するのと同じです。(見かけ上メソッドの定義と同じなので、コンストラクターをメソッドの一種だと誤解してしまう方がいますが、この2つは異なるものです。名前が New であるメソッドの定義はコンストラクターです。)
次の例は何もしないコンストラクターです。このコンストラクターであれば、記述を省略できます。(コンストラクターをオーバーロードしている場合は、このような空のコンストラクターを省略せずに明示的に記述することに意味があります。)
Public Class
DownloadCommander Public Property FolderName As String Public Property LogFileName As String 'VB2008以前の場合、プロパティの構文が少し複雑なのでこのサンプルでは下記のようにフィールドを利用します。 'Public FolderName As String 'Public LogFileName As String Public Sub New() End Sub 省略 End Class |
コンストラクターの中に、プロパティに値を設定するプログラムを記述することができます。
Public Class
DownloadCommander Public Property FolderName As String Public Property LogFileName As String 'VB2008以前の場合、プロパティの構文が少し複雑なのでこのサンプルでは下記のようにフィールドを利用します。 'Public FolderName As String 'Public LogFileName As String Public Sub New() FolderName = "C:\temp" LogFileName = "C:\temp\dllog.txt" End Sub 省略 End Class/span> |
コンストラクターには引数を追加することもできます。これを使うと、固定値の値を設定するのではなく、ユーザーが指定した値を設定するようにできます。
コンストラクターの中に、プロパティに値を設定するプログラムを記述することができます。
Public Class
DownloadCommander Public Property FolderName As String Public Property LogFileName As String 'VB2008以前の場合、プロパティの構文が少し複雑なのでこのサンプルでは下記のようにフィールドを利用します。 'Public FolderName As String 'Public LogFileName As String Public Sub New(folderName As String, logFileName As String) Me.FolderName = folderName Me.LogFileName = logFileName End Sub 省略 End Class |
この例ではコンストラクターの引数の名前とプロパティの名前が同じなので、コンストラクターの中でプロパティにアクセスするには Me. をつける必要があります。つまり、Me.FolderName = folderName というプログラムはFolderNameプロパティに引数folderNameの値を設定するという意味になり、 = の左側はプロパティ、右側は引数を意味しています。
プログラムをこのように改造すると、Form1の呼び出し側のプログラムはエラーになります。
New を使っている部分でコンストラクターが呼び出されるのに、引数が指定されていないからです。
エラーを解消するにはコンストラクターの呼び出し時に次のように引数を指定しましょう。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Dim commander As New DownloadCommander("C:\temp", "C:\temp\dllog.txt") commander.Download("http://www.kantei.go.jp/jp/rekidai/souri/images/souri01.jpg") '伊藤博文 commander.Download("http://www.kantei.go.jp/jp/rekidai/souri/images/souri02.jpg") '黒田清隆 commander.Download("http://www.kantei.go.jp/jp/rekidai/souri/images/souri03.jpg") '山県有朋 'エクスプローラーでフォルダーを開きます。 Process.Start(commander.FolderName) End Sub |
コンストラクターを呼び出すだけでプロパティに初期値が設定されるので、個別にプロパティに設定する必要はありません。
New 使用時に必ず引数を必要であるプログラムにすると、便利な反面、そのクラスを使う側のプログラマーには不便に感じられることもあります。みなさんも何かクラスを使いたいのに、New のやり方がわからなくて苦労したという経験がおそらくあるでしょう。
今回のDownloadCommanderクラスはコンストラクターで引数を強制するよりも、プロパティを設定し忘れてDownloadメソッドを呼び出すとエラーメッセージを表示するという作りの方がひょっとすると親切だったかもしれません。
もう1つの選択肢として、コンストラクターもオーバーロードできるので引数なしのコンストラクターを追加で定義することで、この両方の便利さをあわせ持つということもできます。コンストラクターをオーバーロードするにはもう1つSub New ~ End Sub を記述するだけです。シグネチャ(引数の数と型と順番)が異なっていれば、3つでも4つでもコンストラクターを追加することができます。
※一般のメソッドでは名前もシグネチャの一部ですが、コンストラクターには名前がないので、名前はシグネチャを構成しません。
コンストラクターのオーバーロードまで含めて、この時点でのプログラムの全体像を示します。XMLコメントも補っておきました。
Public Class
DownloadCommander ''' <summary> ''' ダウンロードしたファイルの既定の保存先フォルダー ''' </summary> Public Property FolderName As String ''' <summary> ''' ダウンロードの履歴を記録するログファイルのフルパス ''' </summary> Public Property LogFileName As String 'VB2008以前の場合、プロパティの構文が少し複雑なのでこのサンプルでは下記のようにフィールドを利用します。 'Public FolderName As String 'Public LogFileName As String ''' <summary> ''' DownloadCommanderクラスのインスタンスを生成します。 ''' </summary> Public Sub New() '引数なしのコンストラクター End Sub ''' <summary> ''' DownloadCommanderクラスのインスタンスを生成します。 ''' </summary> ''' <param name="folderName">ダウンロードしたファイルの既定の保存先フォルダー</param> ''' <param name="logFileName">ダウンロードの履歴を記録するログファイルのフルパス</param> Public Sub New(folderName As String, logFileName As String) Me.FolderName = folderName Me.LogFileName = logFileName End Sub Dim wc As New Net.WebClient() ''' <summary> ''' url からファイルをダウンロードし、folderNameに保存します。 ''' </summary> ''' <param name="folderName">ダウンロードしたファイルの既定の保存先フォルダー</param> ''' <param name="logFileName">ダウンロードの履歴を記録するログファイルのフルパス</param> Public Sub Download(url As String, folderName As String, logFileName As String) '▼ファイルのダウンロード Dim localPath As String localPath = IO.Path.Combine(folderName, IO.Path.GetFileName(url)) wc.DownloadFile(url, localPath) '▼履歴をログに書き込む Dim description As String description = Now.ToString("yyyy\/MM\/dd hh\:mm\:ss") & ", " description &= Environment.UserName & ", " description &= url & ", " description &= localPath description &= Environment.NewLine '最後に改行 IO.File.AppendAllText(logFileName, description) End Sub ''' <summary> ''' url からファイルをダウンロードし、<see cref="FolderName"/> プロパティに設定しているフォルダーに保存します。 ''' ダウンロードの履歴は <see cref="LogFileName"/> プロパティのファイルに記録します。 ''' </summary> Public Sub Download(url As String) If String.IsNullOrEmpty(Me.FolderName) Then Throw New InvalidOperationException("Downloadメソッドを呼び出す前にFolderNameプロパティに存在するフォルダーを指定してください。このフォルダーにダウンロードしたファイルを保存します。") End If If String.IsNullOrEmpty(Me.LogFileName) Then Throw New InvalidOperationException("Downloadメソッドを呼び出す前にLogFileNameプロパティに書き込み可能なファイルのフルパスを指定してください。このファイルに履歴を書き込みます。") End If Me.Download(url, FolderName, LogFileName) End Sub End Class |
DownloadCommanderクラスの完成まであともう一工夫必要です。
DownloadCommanderクラスが内部で使用しているWebClientクラスは使用が終了したら Dispose メソッド(読み方:Dispose = ディスポーズ)を呼び出さなければいけないというルールになっています。このDisposeメソッドの呼び出しを追加しましょう。
Disposeという名前のメソッドを持っているクラスはとてもたくさんあります。このメソッドは最後に実行すべき処理を実行します。たとえば、大量のメモリを使うクラスの場合、一番最後に使っていたメモリを開放する必要があるかもしれません。ファイルを読み書きする機能があるクラスは、一番最後にファイルを閉じておく必要があるかもしれません。どこかのネットワークに接続して何か処理する機能を持つクラスは、最後にネットワーク接続を切断する必要があるかもしれません。
このように実行すべき処理は状況によって異なります。WebClientクラスの場合は、インターネットのセッションを使用するので最後にセッションを切断する処理を実行しているのではないかと思います。(公式ドキュメントにはWebClientクラスのDisposeメソッドが何をやっているか記述されていないので私の推測です。)
.NETのルールで、このような最後に必要な処理の呼び出しが必要な場合、その処理は Dispose という名前のメソッドで実装することになっています。だから、Disposeという名前のメソッドを持っているクラスがたくさんあるのです。あなたが何かクラスを利用する場合、そのクラスにDisposeという名前のメソッドがあるかどうか見てみてください。もし、Disposeという名前のメソッドがあるのならば、必ず使い終わったときにそのメソッドを呼び出す必要があります。
Disposeはメソッドなので、他のメソッドと同じように xxxx.Dispose() と記述して呼び出すことができます。
たとえば、ファイルを読み込むStreamReaderの場合、次のようになります。
Dim reader
As New IO.StreamReader("C:\temp\test.txt") 'readerを使用してファイルを読み込む処理 reader.Dispose() |
ただ、どこかプログラムの途中でエラーが発生した場合など、Disposeが呼び出されない可能性があるので、この方法でDisposeを「必ず」呼び出すのはかなり難しいです。だからこの方法は非推奨です。
それで、もう1つ特別な呼び出し方法が用意されています。Using ~ End Using という構文(読み方:Using = ユージング)を使用すると、VBはそのクラスの使用が終わったときに「必ず」自動的にDisposeメソッドを呼んでくれます。
この方法だと上記のStreamReaderのプログラムは次のようになります。
Using reader
As New
IO.StreamReader("C:\temp\test.txt") 'readerを使用してファイルを読み込む処理 End Using |
変数 reader の宣言が Dim ではなく Using になっているのポイントです。そして、変数が使い終わる場所に End Using を記述します。End Usingという目印があるので、ここでこのクラスが不要になるということがVBに伝わるようになります。それで、VBが自動的に Disposeメソッドを予備だしてくれます。自分で Disposeメソッドを呼び出す必要はありません。
Visual Studio 2019では、Disposeが必要なのに Using を使っていない場合、クイックヒントを使って自動的にUsingに書き換えてくれる機能があります。
それでは、DownloadCommanderクラスを改造して、WebClientクラスのDisposeメソッドを呼び出すようにしてみましょう。
さて、どこに記述すればよいのでしょうか?
DownloadCommanderクラスが不要になったタイミングでWebClientクラスのDisposeメソッドを呼び出せばよいのですが、呼び出し側がいくつファイルをDownloadするのか予測できないため、「不要になったタイミング」というものが判断できません。
このような時は、DownloadCommanderクラス自身にDisposeメソッドを実装します。そして、DownloadCommanderクラスのDisposeメソッドからWebClientクラスのDisposeメソッドを呼び出します。
自作のクラスにDisposeメソッドを実装する場合、重要なルールがあります。それは、IDisposableインターフェース(読み方:IDisposable = アイディスポーザブル)としてDisposeメソッドを実装するということです。
「インターフェース」というのは初級講座では説明する予定のない機能です。簡単に説明しておくと、このDisposeメソッドのようにいろいろなクラスで使うメソッドだけど、そのメソッドが呼び出されたときに何を実行するかはクラスによって違う というものを定義しておく機能です。インターフェースという定義がないと、名前だけが手がかりになってしまい、いろいろ不便なのです。
インターフェースの名前は I から始めるという伝統があり、Ixxxxxx という名前のものを見かけたらたいていは何かのインターフェースです。(これは単なる伝統なので I から始まらない名前のインターフェースを作成することは可能です。)
IDisposableインターフェースを実装するには、Public Class DownloadCommander の下に Implements IDisposable という1行を追加します。(読み方:Implements = インプリメンツ)
Public Class
DownloadCommander Implements IDisposable '←追加。【重要】一番最後に Enter キーで改行してください。 |
最後に Enter を押すことが重要です。Implementsの行の最後で Enter を押すと、Visual Studio がそのインターフェースの定義を読み取って最低限のプログラムを自動的に生成してくれます。
IDisposableインターフェースは重要なインターフェースなのでVisual Studioの支援が手厚く、かなりの量のプログラムを自動生成してくれます。Enterを押したらDownloadCommanderクラスの下の方を見てみてください。Visual Studioによって次のようなプログラムが自動的に追加されているはずです。
(これはVisual Studio 2019から採取したプログラムです。バージョンが異なる場合、追加される内容が異なる場合があります。)
Protected Overridable Sub
Dispose(disposing As Boolean) If Not disposedValue Then If disposing Then ' TODO: マネージド状態を破棄します (マネージド オブジェクト) End If ' TODO: アンマネージド リソース (アンマネージド オブジェクト) を解放し、ファイナライザーをオーバーライドします ' TODO: 大きなフィールドを null に設定します disposedValue = True End If End Sub ' ' TODO: 'Dispose(disposing As Boolean)' にアンマネージド リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします ' Protected Overrides Sub Finalize() ' ' このコードを変更しないでください。クリーンアップ コードを 'Dispose(disposing As Boolean)' メソッドに記述します ' Dispose(disposing:=False) ' MyBase.Finalize() ' End Sub Public Sub Dispose() Implements IDisposable.Dispose ' このコードを変更しないでください。クリーンアップ コードを 'Dispose(disposing As Boolean)' メソッドに記述します Dispose(disposing:=True) GC.SuppressFinalize(Me) End Sub |
この自動生成されたプログラムは、Disposeが複数呼び出された場合や、Disposeが呼び出されないまま、インスタンスがメモリ上から消滅しそうな状況など、いろいろな状況を考慮しています。
そのため、なかなか小難しいプログラムになっており初心者には理解できないでしょう。2002年にDisposeが導入されてから数年の間、いろいろな機能とDisposeをどううまく使い分けたらよいのか混乱があり、たどり着いた結論がこの自動生成されたプログラムなのです。プログラムの世界ではこういうものに出会うことがしばしばあります。こういうときはおとなしく先人の知恵に従っておきましょう。詳しくないうちはあまりいじらないようにしましょうね。
この中に1行 WebClientクラスの Disposeメソッドを呼び出しを追加する必要があります。下記の場所に追加してください。
Protected Overridable Sub
Dispose(disposing As Boolean) If Not disposedValue Then If disposing Then ' TODO: マネージド状態を破棄します (マネージド オブジェクト) wc.Dispose() '←ここに追加 End If ' TODO: アンマネージド リソース (アンマネージド オブジェクト) を解放し、ファイナライザーをオーバーライドします ' TODO: 大きなフィールドを null に設定します disposedValue = True End If End Sub ' ' TODO: 'Dispose(disposing As Boolean)' にアンマネージド リソースを解放するコードが含まれる場合にのみ、ファイナライザーをオーバーライドします ' Protected Overrides Sub Finalize() ' ' このコードを変更しないでください。クリーンアップ コードを 'Dispose(disposing As Boolean)' メソッドに記述します ' Dispose(disposing:=False) ' MyBase.Finalize() ' End Sub Public Sub Dispose() Implements IDisposable.Dispose ' このコードを変更しないでください。クリーンアップ コードを 'Dispose(disposing As Boolean)' メソッドに記述します Dispose(disposing:=True) GC.SuppressFinalize(Me) End Sub |
今回の例のように、他のクラスのDisposeを呼び出す必要がある場合は、すべてこの位置に追加してください。Disposeを呼び出す対象が2つ、3つある場合は、ここに2行、3行と記述してください。
DownloadCommanderクラスにDisposeメソッドが実装されたので、Form1の呼び出し側では、このDisposeメソッドを呼び出す必要があります。どうすればよいのでしょうか?もちろんUsing ~ End Using を使うのです。
Private Sub Button1_Click(sender
As Object, e
As EventArgs)
Handles Button1.Click Using commander As New DownloadCommander("C:\temp", "C:\temp\dllog.txt") commander.Download("http://www.kantei.go.jp/jp/rekidai/souri/images/souri01.jpg") '伊藤博文 commander.Download("http://www.kantei.go.jp/jp/rekidai/souri/images/souri02.jpg") '黒田清隆 commander.Download("http://www.kantei.go.jp/jp/rekidai/souri/images/souri03.jpg") '山県有朋 End Using 'エクスプローラーでフォルダーを開きます。 Process.Start(commander.FolderName) End Sub |
このDisposeメソッドのように、クラスの使用終了時に呼び出す処理をデストラクターと呼びます。クラスを生成するコンストラクターの反対の機能です。
デストラクターはDisposeの他に、Finalize(読み方:ファイナライズ)というものがあります。
よくみると、IDisposableインターフェースを実装して自動生成されたプログラムの中に、コメントの形で Protected Overrides Sub Finalize という定義が埋め込まれており、これがそれです。
このFinalizeは .NET ではない、機能を利用している場合の終了処理を行うことが主眼であり、通常は出番はありません。FinalizeはDisposeと違って、プログラマーが明示的に呼び出すものではなく、VB(.NET)が自動的に呼び出します。そのため、いつ呼び出されるかという制御が難しく、また、まちがった使い方をするとパフォーマンスが劣化したり、面倒な存在です。
ということで、Finalizeはどうしてもそれが必要だという場合以外使用しないことをお勧めします。私の20年近くの.NETの経験ではこれが必要になったことは数えるほどしかありません。
メソッドの冒頭に記述している Public (読み方:Public = パブリック) について説明しておきます。
Publicはこのメソッドがどこからでも呼び出せることを定義しています。この考え方を「アクセスレベル」と呼びます。実際Form1からDownloadメソッドを呼び出すことができています。
Publicの他に Private, Protected, Friend, Protected Friend, Private Friend というキーワードを指定することもできます。
たとえば、Publicの代わりに Private を使用すると自分のクラスからしか呼び出せなくなります。
以下の例のTestメソッドはPublic Subではなく、Private Subで定義されている点に注目してください。
Public Class
SampleClass Private Sub Test() Debug.WriteLine("Hello!") End Sub End Class |
このようにするとTestメソッドはSampleClass内からしか呼び出せなくなります。
実例でPublicとPrivateの違いを説明しましょう。
次のプログラムはInvokeメソッドからTestメソッドを呼び出しています。この呼び出しは同じクラス内なのでOKです。
Public Class
SampleClass Private Sub Test() Debug.WriteLine("Hello!") End Sub Public Sub Invoke() Me.Test() End Sub End Class |
この状態でForm1に次のようにInvokeの呼び出しをプログラムしてみましょう。これも大丈夫です。InvokeがPublicで定義されているからです。
Public Class
Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Dim sample As New SampleClass sample.Invoke() End Sub End Class |
実行すると、Button1_Click → Invoke → Test の順に呼び出され、出力ウィンドウのデバッグには Hello! と出力されます。
しかし、次の記述はエラーとなりそもそも実行できません。
Public Class
Form1 Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click Dim sample As New SampleClass sample.Test() '←この例はエラーです。実行できません。 End Sub End Class |
TestメソッドはPrivateで宣言されているため、外部のクラス(この場合Form1)から直接呼び出すことができないのです。
このとき、Visual Studioは次のメッセージを表示します。このメッセージはエラーを示す赤い波線の上にマウスカーソルをホバーさせたり、[表示]メニューからエラー一覧を表示したりすると確認できます。 'SampleClass.Private Sub Test()' は 'Private' であるため、このコンテキストではアクセスできません。 この「コンテキスト」という言葉は、今後も何かにつけて目にすると思います。簡単に言うと「状況」や「シチュエーション」という意味です。つまり、Testメソッドは呼び出せることもあるけど、この状況では呼び出せないという意味で、この場合、「この状況」とは外部のクラスから呼び出そうとしているという状況を指しています。 プログラムの世界では同じプログラムを記述しても、うまく実行できる場合とできない場合があることは珍しくなく、このような「状況」を表現するために「コンテキスト」という言葉は便利に使われます。「コンテキストが違います。」、「予期されたコンテキストとは異なります。」。『状況』自体が抽象化されてオブジェクト化されている場合もあり、「コンテキストが生成できません。」などと表現する場合もあります。こちらは使い方はちょっと難しいです。 |
InvokeはPublicなので外部のクラスからでも呼び出せます。TestはPrivateなので外部のクラスからは呼び出せません。InvokeとTestは同じクラス内にあるのでお互い呼び出せます。図にすると次のようなイメージです。
アクセスレベルにはPublicとPrivateのほか、下記のものがあります。この表に挙げているものがすべてです。
アクセスレベル | 読み方 | 意味 |
---|---|---|
Public | パブリック | どこからでも呼び出せます。 |
Friend | フレンド | 同じアセンブリ内から呼び出せます。 |
Protected | プロテクテッド | 同じクラス または 派生クラスから呼び出せます。 |
Protected Friend | プロテクテッドフレンド | 同じクラス または 同じアセンブリ内 または 派生クラスから呼び出せます。 |
Private | プライベート | 同じクラスからのみ呼び出せます。 |
Private Protected | プライベートプロテクテッド | 同じクラス または 同じアセンブリ内の派生クラスから呼び出せます。 |
Private ProtectedはVisual Basic 2017から使用できます。
この表にはまだ初級講座で説明していない「派生クラス」や「アセンブリ」という言葉が登場します。
アセンブリとは簡単に言うとプロジェクトのことです。厳密には一致しませんがほとんどの場合一致します。ある程度複雑なアプリケーションを作成すると複数のプロジェクトを使って、プロジェクト間で呼び出しを行う場合があります。これができるのはPublicで宣言されている場合のみということです。
「派生クラス」の説明は長くなってしまうので、別の回で詳しく取り上げることにします。
なお、メソッドのアクセスレベルの記述を省略すると Public とみなされます。わかりにくくなるので省略することはお勧めしません。
ところで、アクセスレベルはどのような用途で使用することが想定されているのでしょうか?どいういうものをPublicにして、どういうものをPrivateにすべきでしょうか?
よくある誤解は、何か秘密の機能をプログラムするときはPrivateで宣言して外部からアクセスできないようにするというものです。これは完全に誤解です。まず、.NETでプログラムされたものは、解析可能であり、ほとんどのプログラムはexeやdllファイルとして入手したとしても、元のプログラムがどのようなものであったのか知る手段があります。だから、プログラムの中に秘密の機能やデータを埋め込んでいる場合、それをPrivateで宣言しても隠すことにはなりません。
それに、PublicやPrivateで呼び出せたり呼び出せなかったりするのはVisual Basic(やC#)などの言語の機能であり、フレームワークの機能を使えば、コンテキスト(状況)を無視して、Privateなメソッドを外部から呼び出すことも可能です。このようなフレームワークの機能をリフレクションと呼びます。
アクセスレベルが想定しているのは簡単に言うと 間違いの防止 や 修正のしやすさ です。
数千個のメソッドをすべてPublicで宣言している大きなアプリケーションを想像してみましょう。ここに新しい機能を追加する場合どうでしょうか?既に数千個もメソッドがあるので、役に立ちそうなものがきっとあるはずです。でも、数千個の候補の中から何か役に立ちそうなものを探すのはなかなか大変です。もしかしたら間違って外部から呼び出されることを想定しないメソッドを呼び出してしまうかもしれません。それだったら、はじめから、外部から呼び出すと役に立つものだけをPublicにして、外部から呼び出すことを想定していないものをPrivateにしておくほうがわかりやすいですよね。
それから、プロジェクトには複数のプログラマーが参加しているの普通です。あなたが作ることになっている機能は Public で宣言して他のプログラマーも呼び出せるようにすべきです。しかし、その機能を実現するために裏方で実行される細かい複数の処理はPublicにするべきではありません。間違ってPublicにしてしまって、他のプログラマーがそれを呼び出してしまったらどうでしょう。そうすると、もうあなたの都合だけでそのメソッドを修正することはできなくなるのです。そのメソッドの戻り値がちょっと変わっただけで、他のプログラマーが作成している機能に影響が出るかもしれないからです。
以上はある程度大きなチーム開発を想定しているので、あなた一人がそれほど大きくないプログラムを作っているのならばすべてをPublicにしたほうがどこからでも呼び出せて便利かもしれません。でも、プログラムというのは予想外に成長したり、他人の手に渡ったりするものです。小さいうちからPublic・Privateをメリハリを付けて多くことをお勧めします。
まとめ
Public → 外部に公開する機能。この機能を変更すると他の箇所に影響が出ます。
Private → 裏方の機能。この機能を変更しても影響範囲は同じクラス内だけです。
今回は実際のクラス作りを通して、クラスを作成するのに必要な基本的な事柄を説明しました。
最後に、クラスとは何かもう1回振り返って見ます。
クラスとは意味のある機能のあつまりです。プログラムをしているとファイルシステムやデータベース、ネットワーク、グラフィックスやサウンドなどと実にいろいろな機能を使用することになります。それらの機能を意味のある単位ごとにわけて管理しているのがクラス(や構造体)です。
そして、クラス(や構造体)はただ単に機能をまとめているだけではなく、人間にとって扱いやすいように現実世界に存在する「物」をモデルとして設計されているのが特徴です。
たとえば、ファイルを操作するFileInfoクラスは現実の「ファイル(書類)」のように設計されています。このことはメソッドやプロパティの意味を考えてみると明らかです。
メソッド・プロパティ | 英語の意味 | プログラム上の機能 |
Open | 開け | 開く |
AppendText | 文字を追加せよ | 文字を追加する |
Length | 長さ | ファイルサイズ |
Name | 名前 | 名前 |
現実世界には存在しないような物をクラス化する場合でもできるだけ人間の視点でわかりやすく、できるだけ「物」のように設計されています。
そして、クラスを利用する側の立場に立てば、必要な物を実体化し、その物の性質を定義したり、物に対して命令したりして目的の機能を実現していくのがVBのプログラミングスタイルです。
このスタイルは人間にとってとてもわかりやすいプログラミングスタイルであると同時に、意味ごとに機能が分かれていて管理・設計がしやすいなどの利点がありVB以外にも多くのプログラミング言語で採用されています。
「物(オブジェクト)」視点でのプログラミングということで、このスタイルは「オブジェクト指向」と呼ばれています。
今回は、WebからファイルをダウンロードするDownloadCommanderクラスを作成しました。このような物は現実には存在しないので、現実にあるような感覚だと次のようなメソッドやプロパティがあるかなというところを想像しています。
メソッド・プロパティ | 英語の意味 | プログラム上の機能 |
Download | ダウンロードしろ | ダウンロードする |
FolderName | フォルダーの名前 | ダウンロード先のフォルダー名 |
LogFileName | ログファイル名 | ダウンロードの履歴を記録するファイル名 |
他の人が設計するともっと違うメソッドやプロパティになるかもしれません。それは構わないのです。
ただ、これらの機能をもし、Form1 に作ってしまうようだと、「画面」を現すクラスであるForm1がダウンロード機能まで持ってしまうことになり、オブジェクト指向的ではありません。