Visual Basic 初級講座 |
Visual Basic 中学校 > 初級講座 >
特殊な クラスであるモジュール・列挙体・構造体について説明します。最後にクラスを宣言する位置についても説明します。
概要 ・モジュールを宣言するにはModule 〜 End Moduleを使う。 ・モジュールとはインスタンス化なしで、クラス名を省略していきなりメンバを参照できる特殊なクラス。 ・列挙体を宣言するにはEnum 〜 End Enumを使う。 ・列挙体はプログラム中に入力候補の一覧が出たりする特殊なクラス。 ・構造体を宣言するにはStructure 〜 End Structureを使う。 ・構造体は値型のクラス。通常のクラスと違って保持している「データ」を重視する。 ・1つのファイルに複数のクラスを記述することができる。 |
VBでは多くのものがクラスでできています。一見クラスに見えないようなものも実はクラスであることがしばしばです。たとえば、例外や属性などがその例です。
今回はクラスの説明の締めくくりとしてこのような特殊なクラスのうち、いままで取り上げなかったりちゃんと説明していなかったモジュール・列挙体・構造体を紹介し、最後にクラスを宣言する場所について説明します。
モジュールとは特殊なクラスの1つです。モジュールで宣言した要素はプロジェクト内のすべてのフォーム・クラスで参照することができ、参照するのにモジュールをインスタンス化する必要もありません。
要するにモジュールとはプロジェクトで共通の変数やメソッドを使用するための簡単・便利なクラスなのです。
ただし、私はVBの学習上の理由でモジュールの使用はお勧めできません。詳しい理由は少し後で説明します。単に学習上の理由なので、モジュールの機能自体に欠陥があったりするわけではありませんからお好みで使用される分には何の問題もありません。
さて、アプリケーションにモジュールを追加するには[プロジェクト]メニューの[モジュールの追加]をクリックします。
■画像1:モジュールの追加
モジュールを追加すると次のような空のモジュールが追加されます。
Module
Module1 End Module |
■リスト1:空のモジュール
このModule 〜 End Module(読み方:Module = モジュール)の間に記述する変数やメソッドはすべてアプリケーション共有となり、アプリケーションのどこからでも参照できるようになります。
例として文字列型のフィールドを1つと、メソッドを1つ記述してみましょう。
Module
Module1 Public ThisName As String 'フィールドの宣言 '■Diagonal ''' <summary>n角形の対角線の数を求めます。</summary> ''' <param name="n">対象の図形の角の数を指定します。</param> ''' <returns>n角形の対角線の数を返します。</returns> Public Function Diagonal(ByVal n As Integer) As Integer If n < 2 Then Throw New ArgumentException("nには3以上の整数を指定して下さい。") End If Return n * (n - 1) / 2 - n End Function End Module |
■リスト2:モジュールのプログラム例
これをフォームから参照するコードは次のようになります。
Private
Sub Button1_Click(ByVal
sender As System.Object,
ByVal e As
System.EventArgs) Handles Button1.Click ThisName = "Viva VB" Dim Count As Integer Count = Diagonal(8) '8角形の対角線の数を求める。 MsgBox("8角形の対角線の数は" & Count & "です。", MsgBoxStyle.Information, ThisName) End Sub |
■リスト3:フォーム側のプログラム。モジュールの利用例
ちょっと見ていただくとわかるのですが、ThisNameもDiagonalもモジュールで定義されているのに、あたかもフォーム自身で定義されているかのように直接呼び出すことができます。もちろんインスタンスを作成したりImportsを記述する必要もありません。
これがモジュールの威力です。
このようにしてForm1に限らずアプリケーションに含まれるすべてのフォーム・すべてのクラスがモジュールで定義されているものを参照することができます。
なお、必要であればモジュール名をつけて参照することももちろん可能です。たとえば、Count = Diagonal(8)と書くべきところをCount = Module1.Diagonal(8)と書くこともできます。
これらモジュールの機能はまったく便利なものです。通常のクラスを使ってこれと同じ事を実現することはできません。
しかし、これとできるだけ似たようなことを通常のクラスを使って実現することを考えて見ましょう。どのようにすればよいでしょうか?
もう一度モジュールの便利さを整理してみると、モジュールの便利さはモジュール名を省略していきなりメンバを参照できる点とクラスのようにインスタンス化しないでいきなり使用できる点にあります。
クラスの場合クラス名を省略していきなりメンバを参照できるようにするにはImportsを使います。またインスタンス化しないでいきなり使用できるようにするにはSharedを使ってメンバを共有メンバとして宣言します。
そこでクラスを使ってできるだけモジュールと似たようなことをやると次のようになります。
まず、クラス側の例です。クラス側ではメンバをSharedで宣言するようにします。
Public
Class Class1 Public Shared Age As Integer '■Factorial ''' <summary>階乗を求めます。</summary> ''' <param name="x">数値を指定します。</param> ''' <returns>xの階乗を返します。</returns> ''' <remarks>xの階乗とはx * (x-1) * (x-2) * ... * 1のことです。たとえば、5の階乗は5*4*3*2*1=120です。</remarks> Public Shared Function Factorial(ByVal x As Long) As Long If x = 1 Then Return 1 Else Return x * factorial(x - 1) End If End Function End Class |
■リスト4:モジュールのようなクラスのプログラム例
これを呼び出すフォーム側ではクラスの名前を省略して呼び出せるように先頭にImportsを記述します。
Imports WindowsApplication1.Class1 |
■リスト5:クラス名を省略するためのImports
このようにすれば後はモジュールと変わりません。次のように使用することができます。
'注意!先頭にImports
WindowsApplication1.Class1を記述すること Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click Age = 123 Dim Result As Long Result = Factorial(5) MsgBox("5の階乗は" & Result & "です。", MsgBoxStyle.Information, Age) End Sub |
■リスト6:フォーム側のプログラム。モジュールのようにクラスを使用する例
先頭にImportsを記述したくない場合でも逃げ道があります。1つはいちいちクラス名を記述する方法です。
たとえば、Result = Factorial(5)と書く代わりにResult = Class1.Factorial(5)と書けばImportsを記述する必要はありません。このような記述の方がクラス名を省略して書くよりプログラムの可読性に貢献することは少なくありません。
またはプロジェクトの設定としてインポートしてしまえば個別のフォームやクラスでImportする必要がなくなります。
■画像2:インポートの設定(VB2005) | ■画像3:インポートの設定(VB.NET2002, VB.NET2003) |
実はVBにはあらかじめモジュールが組み込まれていていつでも使用可能な状態になっています。何のことだかわかりますか?
われわれが普段何気なく使っているMsgBox関数やLen関数はVBに標準で組み込まれているモジュールのメソッドなのです。だからこれらの関数はImportsもインスタンス化もいらずにいきなり使用することができたのです。
どのようなモジュールが用意されているかはオブジェクトブラウザを使うと確認できます。ざっと概観してみます。
モジュール名 | 読み方 | 概要 |
Constants | コンスタンツ | さまざまな定数。改行コードvbNewLineやOKボタンのvbOKなど。 |
Conversion | コンバージョン | Hex, Oct, Valなど変換用の関数。 |
DateAndTime | デイトアンドタイム | DateAdd, DateDiffなど日付用の関数。 |
FileSystem | ファイルシステム | FileCopy, FileOpen, MkDirなどのファイル操作用の関数。 |
Financial | フィナンシャル | 金融系の関数。 |
Globals | グローバル | GetTypeなどのその他の関数。 |
Imformation | インフォメーション | IsNumeric, TypeNameなどの情報・検証・テスト用の関数。 |
Interaction | インタラクション | AppActive, Command, MsgBoxなどのシステム対話系の関数。 |
Strings | ストリングス | Left, Lenなどの文字列操作系の関数。 |
VBMath | ブイビーマス | Rnd, Randomizeなどの乱数系の関数。 |
これらのモジュールはVBでは必ず組み込まれているのですぐにでも使用することができます。たとえば、次のコードはGlobalsモジュールの機能を利用して実行エンジンの名前を表示します。
MsgBox(ScriptEngine) |
■リスト7:Globalsモジュールの関数の呼び出し
何が表示されるかは実際に動かして確かめてみてください。
組み込みモジュールの機能の多くはVB6以前のバージョンとの互換性のために用意されているので実際にはあまり使いませんし、参考書やWebなどで紹介されることもほとんどありません。組み込みモジュールの機能のほとんどは.NET Frameworkの巨大なクラスライブラリを使って実現可能なものです。
たとえば、FileSystemモジュールのFileCopy関数はファイルをコピーする関数ですが、通常我々はこれを使わずにFileクラスのCopyメソッドを使います。
組み込みモジュールの機能でよく使うのはStringsクラスのLeft関数やLen関数、それにConstantsモジュールのいくつかの定数。あとはいくつかの便利関数をつまみ食いする程度です。
それすらもオブジェクト指向の観点から使用しないスタンスの人がいるほどです。
この.NET Frameworkのクラスの機能を使うべきか組み込みモジュールの機能を使うべきかの議論は実は第5回 メソッドと関数でも登場しています。そのときはメソッド(.NET Frameworkのクラスの機能) 対 関数(組み込みモジュールの機能)という構図でした。
モジュールに関する説明はこのくらいです。冒頭で私はモジュールの使用をお勧めしないと書きましたが最後にその理由を説明します。
理由は「学習上よくないから」です。これだけです。学習上のことに気を配る必要がなければじゃんじゃんモジュールを使用して下さい。
モジュールがなぜ学習上よくないかと言うと、簡単で便利すぎるからです。VBはオブジェクト指向という思想の基にプログラムが組めるように設計されています。オブジェクト指向については中級講座で説明しますが、簡単に言うと上手にクラスを使ってプログラムしていきましょうねという考え方です。
VBがオブジェクト指向で設計されている以上、VBを極めると言うことはオブジェクト指向を極めることでもあるのです。また、オブジェクト指向の知識があればVBの理解も格段に向上します。
ですから、私は普段プログラムを行うときもできるだけオブジェクト指向の勉強・鍛錬としてプログラムを行っていただきたいのです。Visual Basic 中学校でもそのように配慮して説明をしています。
ところが、この「モジュール」はオブジェクト指向という思想とはちょっと異なる存在なのです。真っ向から反しているとまでは言えませんがオブジェクト指向が目指す方向とはずれがあります。
そんなに大きなずれとも思いませんが私はこのような学習上の理由で「モジュール」ではなく「クラス」を使って欲しいのです。
発展学習 - モジュールの正体 発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。 構造体の正体はMicrosoft.VisualBasic.CompilerServices.StandardModule属性のついたクラスであるとのことですが、通常の開発環境でこの属性を指定してもそのクラスがモジュールのように動作するわけではありませんので意味はありません。 |
列挙体は値を定義するための便利な入れ物として使いますが、その正体は特殊なクラスです。
列挙体が便利なのはプログラム中に入力候補が自動的に表示されるところです。
■画像4:列挙体の入力候補
列挙体を定義するにはEnum 〜 End Enum(読み方:Enum = エナム または エニューマレーション)を使用してます。たとえば、血液型を表す列挙体を宣言する場合は次のようになります。
Public
Enum ABBloodType A B AB O End Enum |
■リスト8:自作の列挙体
これだけでABBloodType型の変数には自動的に入力候補が表示されるようになってとても便利です。
■画像5:自作の列挙体でも入力候補が表示される。
上述の列挙体ではA, B, AB, Oの4つの要素を宣言しており、これらは通常のクラスで言うと定数のような存在です。それぞれの値は省略していますが、上から順に0, 1, 2, 3となります。
ですから、たとえば次のようにプログラムすると1 + 2を実行したことになり3と表示されます。
Dim
X As
Integer X = ABBloodType.B + ABBloodType.AB '= 1 + 2 MsgBox(X) |
■リスト9:列挙体に割り当てられている値の確認
この例は、列挙体の要素に値が割り当てられていることを確認する例であって実用性はありません。
値が重要な意味を持つ場合には、列挙体の宣言の中で自分で値を割り当てることもできます。
Public
Enum ABBloodType A = 0 B = 10 AB = 20 O = 40 End Enum |
■リスト10:値を指定した列挙体
割り当てられる値は整数のみです。文字列などその他の型を割り当てることはできません。
はじめに書いたように列挙体が最も役に立つのはプログラム中に入力候補の一覧が表示される点です。また、単なる数値が意味のある文字として目に見えるようになるため、プログラムの可読性が大幅に向上します。
次の2つのコードはどちらも現在の土曜日か日曜日の場合にはメッセージを表示しますが、どちらが可読性に優れているか一目瞭然です。
まずは、通常通り列挙体を使用するコードです。
If
Now.DayOfWeek = DayOfWeek.Saturday OrElse
Now.DayOfWeek = DayOfWeek.Sunday
Then MsgBox("起動できません。") End If |
■リスト11:列挙体を使用した読みやすいコード
次に列挙体を使用しないコードです。
If
Now.DayOfWeek = 6 OrElse Now.DayOfWeek = 0
Then MsgBox("起動できません。") End If |
■リスト12:マジックナンバーを使用した読みにくいコード
他にも列挙体を使うことによりプログラムで誤った値の代入を防ぐ効果があります。たとえば、Option Strict Onの状態では以下の代入はエラーになります。
Dim
X As ABBloodType X = 10 |
■リスト13:数値から列挙体への型変換
しかし、なぜか0だけは直接代入することができます。この理由は謎です。
なお、VBでは便宜のために列挙体で定義されている値の「名前」を列挙する方法が用意されています。次の例ではDayOfWeek列挙体の値の「名前」をListBoxに表示します。値ではなく「名前」が取得できることがポイントです。
For
Each DayName As
String In
[Enum].GetNames(GetType(DayOfWeek)) ListBox1.Items.Add(DayName) Next |
■リスト14:列挙体の値の名前を列挙する
この例ではEnumクラスのGetNamesメソッド(読み方:GetNames = ゲットネームス)を使用して値の名前を取得しています。Enumクラスの前後を [ ] でくくっているのはクラスの名前とVBのキーワードの「Enum」が同じ名前のため、普通に使うとVBがエラーを判定してしまうためです。このように [ ] でくくるとVBのキーワードと同じ名前の識別子(変数名や、メンバ名・クラス名)を使用することができます。たとえば、Dim [Dim]とするとDimという名前の変数を宣言することもできます。もちろん、この変数をプログラム中で使うときはいちいち [ ] でくくらないとエラーになってしまいます。
この例では列挙している雰囲気がわかりやすくなるようにFor Eachを使いましたが、ListBoxに表示するのが目的ならもっと簡単に書くこともできます。
ListBox1.Items.AddRange([Enum].GetNames(GetType(DayOfWeek))) |
■リスト15:列挙体の値の名前をListBoxに表示する
なお、列挙体以外のクラスでもメンバの名前を取得することはそれほど難しくありません。次のコードはTextBoxの全プロパティ名をListBoxに表示します。
For
Each Prop As
Reflection.PropertyInfo In
GetType(TextBox).GetProperties() ListBox1.Items.Add(Prop.Name) Next |
■リスト16:TextBoxの全プロパティ名をListBoxに表示する
EnumクラスのGetNameメソッド(読み方:GetName = ゲットネーム)を使うと、単純に値の名前を取得することもできます。
Dim
Name As
String Name = [Enum].GetName(GetType(DayOfWeek), DayOfWeek.Thursday) MsgBox(Name) 'Thursday |
■リスト17:列挙体の値の名前を表示する
発展学習 - 列挙体の正体 発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。 列挙体の正体はSystem.Enum構造体を継承したクラスです。System.Enum構造体はMSDNライブラリにも記載されていますがSystem.ValueTypeクラスを継承した構造体です。 |
構造体はかなり通常のクラスに似ています。メソッドやプロパティを持つこともできます。通常のクラスとの違いは通常のクラスが参照型であるのに対し、構造体が値型であることです。
値型と参照型については第34回 値型と参照型で説明しています。
構造体は値型であるためにNewなどを使って明示的にインスタンス化する必要はありません。もちろんコンストラクタを呼び出すときにはNewを使用しますが、この場合でもインスタンスを作成すると言うよりはコンストラクタを呼び出すと言う意味合いになります。
構造体はStructureステートメント(読み方:Structure = ストラクチャー)を使って宣言します。以下に構造体の例を示します。
Public
Structure Person Private m_Name As String Private m_Age As Integer |
Public Sub
New(ByVal name
As String,
ByVal age As
Integer) Me.Name = name Me.Age = age End Sub |
Public Property
Name() As
String Get Return m_Name End Get Set(ByVal value As String) m_Name = value End Set End Property |
Public
Property Age() As
Integer Get Return m_Age End Get Set(ByVal value As Integer) m_Age = value End Set End Property End Structure |
■リスト18:構造体の例
この構造体には2つのフィールドと2つのプロパティと1つのコンストラクタがあります。このように構造体のメンバはクラスのメンバと同様です。
この構造体を利用する例を次に示します。
Dim
Taro As New
Person("太郎", 12) Dim Hanako As Person Hanako.Name = "花子" Hanako.Age = 11 MsgBox(Taro.Name & " " & Taro.Age & "才") MsgBox(Hanako.Name & " " & Hanako.Age & "才") |
■リスト19:フォーム側のプログラム。構造体の使用例。
この例ではPerson構造体のインスタンスを2つ使用しています。1つ目のインスタンスTaroでは、コンストラクタを呼び出しているのでクラスのインスタンス化と構文上もまったく同じです。
2つ目のインスタンスHanakoではコンストラクタを使用していないのでNewを使用していませんが、エラーにならずに正常に実行することができます。
ただし、これだとぱっと見るとHanakoでNullReferenceExceptionが発生するように思ってしまうかもしれないので、可読性のためにNewをつけることもできます。マイクロソフトも必要がなくてもNewをつけることを推奨しているようです。
このように見てみるとクラスと構造体の違いがほとんどないように思えてくるかもしれません。実際には上述のインスタンス化の方法の違いの他にメモリ管理方法の違いや継承やイベント処理の違いなどがあります。
これらの違いをひとつひとつ挙げて説明することも可能ですが、あまり有意義とは思えないので割愛します。詳しく知りたい方はMSDNライブラリを参照して下さい。
とりあえずは構造体はクラスの軽量版のような存在だと思っておけば問題ありません。
先ほどのPerson構造体をそのままクラスにした場合と構造体である場合とで次のコードの実行時間を比較するとクラスの方が処理に時間がかかるのがわかります。
Dim
Taro As Person Dim Start As Long Start = Now.Ticks For i As Integer = 0 To 10000000 Taro = New Person("太郎", i) Next ListBox1.Items.Add(Now.Ticks - Start) |
■リスト20;構造体とクラスの性能測定
Personを構造体にしたときは私のパソコンではこの処理に約0.4秒かかりますが、Personをクラスにするとまったく同じコードで約1.3秒かかります。ただし、この例がNewを試すものであることに注意して下さい。その他の通常の動作はクラスと構造体は同等です。
しかしながら構造体にすべきかクラスにすべきか迷ったらクラスにしてください。クラスの方が機能に優れ他のVBの機能との親和性にも優れているからです。
イメージ的には構造体はそれが持つデータに主眼があるのに対し、クラスはその振る舞いに主眼があります。
上述のPerson構造体は名前と年齢と言う2つのデータに主眼があるので構造体がふさわしいです。ひょっとすると生年月日から年齢をもとめるようなメソッドが必要になるかもしれませんが、メソッドを装備したとしてもデータに主眼があることに変わりありません。実際に構造体には必ず1つ以上のフィールドを含めなければならないという制約があります。(フィールドの代わりにイベントを含めることもできます)。
一方TextBoxクラスやFileInfoクラスの場合は、データではなく振る舞いに主眼があります。これらは特定の処理をするために必要なのであってプロパティやフィールドはその処理を定義したり補助したりするのに使用するものがほとんどです。
なお、構造体は厳密にはクラスの一種なのですが、通常の会話や説明の中では単に「クラス」と言った場合構造体は含まれていないことが多いので注意して下さい。
発展学習 - 構造体の正体 発展学習では意欲的な方のために現段階では特に理解する必要はない項目を解説します。 構造体の正体はSystem.ValueTypeクラスを継承したクラスです。 |
今までクラスを作成する場合はクラス用にファイルを1つ作成する方法を説明してきました。たとえば、Testクラスを作成するにはTest.vbファイルを用意していました。しかし、別に1ファイル1クラスという決まりがあるわけではないので同じファイルに複数のクラスを定義することもできます。
ここでいうクラスにはモジュール・構造体・列挙体を含みます。
細かいクラスがたくさんある場合などにはいちいちクラスをわけないで同じファイル中に記述してしまいます。フォームと同じファイルにクラスを定義することもできます。
Public Class Form1 … End Class Public Class TestClass … End Class |
■リスト21:複数のクラスを1つのファイルに宣言する例
ただし、フォームと同じファイルに別のクラスを定義する場合はフォームより下に書くようにして下さい。そうしないとビジュアルにマウスでボタンやテキストボックスを貼り付けるあの便利なデザイン画面が表示できなくなってしまいます。
また、1つのファイルにあまりたくさんクラスがあっても見にくくなるのでそのあたりの加減はプログラマのセンスです。
クラスの中にクラスを定義することもできます。このようなクラスを「入れ子クラス」や「内部クラス」などと呼びます。
Public Class
Class1 … Public Class InnerClass1 … End Class End Class |
■リスト22:クラスの中にクラスを宣言する例
内部クラスをPrivateで定義することで、あるクラスに専属のクラスを定義することができます。このようなクラスの例としてはArrayListクラスの内部で定義されているReadOnlyArrayListクラスがあります。
1つのクラスを複数のファイルに分けて定義することもできます。1つのファイル内で複数個所に定義することもできます。これを分割クラスと呼びます。
分割クラスはVB2005から新しく追加された機能です。クラスの宣言を分割するにはPartialキーワード(読み方:Partial = パーシャル)を使います。
Public Class
Form1 Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click Dim X As New BigOne X.Hello() X.Bye() End Sub End Class |
Public Class
BigOne Public Sub Hello() MsgBox("Hello") End Sub End Class |
Partial
Class BigOne Public Sub Bye() MsgBox("Bye") End Sub End Class |
■リスト23:分割クラスの例
このクラスの分割を利用することはまずありません。プログラムが無駄に複雑になり可読性が大いに低下する虞があるので使用しないでください。ただし、この機能を知っておく必要はあります。
なぜなら、我々がもっともよく使うクラスであるフォームがこのPartialを使って宣言されているからです。
ソリューションエクスプローラですべてのファイルを表示するとForm1の下に2つのファイルがぶらさがっているのがわかります。
■画像6:すべてのファイルを表示したソリューションエクスプローラ
つまり、この画像の例だと1つのフォームが実は3つのファイルから成立していることがわかります。このうちForm1.vbは我々がプログラムするファイルです。Form1.Designer.vbは自動的に作成されるプログラムがはいるファイルです。
自動的に作成されるプログラムとはボタンやテキストボックスなどのコントロールの配置やプロパティです。試しにいくつかコントロールを配置してからForm1.Designer.vbの中を見てみてください。
次のようなプログラムが確認できます。
<Global.Microsoft.VisualBasic.CompilerServices.DesignerGenerated()>
_ Partial Class Form1 Inherits System.Windows.Forms.Form … End Class |
■リスト24:Form1.Designer.vbの中
このコードの省略した部分に配置したコントロールやプロパティに関するプログラムが自動的に埋め込まれます。
そして、このクラスがPartialで宣言されていることに注意して下さい。このような仕組みになっているので表に見えるForm1.vbはすっきりと見やすい構造になり、自動生成される裏方のコードはForm1.Designer.vbに任せることができるのです。
Partialはこのためにあると言っても過言ではありません。
なお、VB.NET2002, VB.NET2003にはPartialはないので裏方のコードもForm1.vbに記述されていました。それが、いわゆる「Windows フォーム デザイナで生成されたコード」です。
■画像7:自動生成されるコードの位置