|
|
連載
改訂版
プロフェッショナルVB.NETプログラミング
Chapter 14 属性
株式会社ピーデー
川俣 晶
2004/09/02 |
|
|
固定長文字列、ランダム・ファイルとVBFixedString属性、属性とは何か?を読み比べて、「あれ?」と思った読者もいるのではないだろうか。というのは、VBFixedStringAttribute属性を、VBFixedString属性と記述しているケースも存在するからである。さらに、名前空間を明示した名前を記述しても、エラーにならない。以下のサンプル・プログラムは、Serializableを例にして、これらのバリエーションを並べて書いてみたものである(リスト14-7)。
1: <Serializable()> Private Class Sample1
2: Public a As Integer
3: Public b As Integer
4: End Class
5:
6: <SerializableAttribute()> Private Class Sample2
7: Public a As Integer
8: Public b As Integer
9: End Class
10:
11: <System.SerializableAttribute()> Private Class Sample3
12: Public a As Integer
13: Public b As Integer
14: End Class
|
|
リスト14-7 属性を記述する場合の3つバリエーション
|
このサンプル・プログラムで示した3つのクラスは、まったく同じ内容を定義していることになる。これらのクラスに付いた属性名、Serializable、SerializableAttribute、System.SerializableAttributeは、すべて同じものを示している。これは、2つの事実を知ることで容易に理解することができる。
第1の事実は、属性の実体はクラスであるということだ。そのため、記述方法もクラスと何ら変わることはなく、System.SerializableAttributeとフルネームで書いてもよく、省略可能な名前空間名を省略して、SerializableAttributeと書いてもよい。また、Imports文でデフォルトの名前空間名を指定すれば、属性の名前空間名の部分を省略して書くこともできる。属性の機能について調べたいときも、クラス・ライブラリのリファレンスを開き、クラス名として属性名を調べることで、必要な情報を得ることができる。
第2の事実は、属性を実現しているクラスの名前は、必ずAttributeという文字列で終わり、この文字列は属性としてソース・コードに記述する際に省略可能ということである。つまり、SerializableAttributeという属性名を記述する際、最後の“Attribute”を省略して、Serializableとだけ書いてもよいということである。この機能により、VBFixedStringAttributeとVBFixedStringは、どちらの名前を使っても結果は同じである。また、リファレンス・マニュアルを引くときに、「VBFixedString」というキーワードでは出てこない場合があったとしても、それに省略された文字列Attributeを補い、「VBFixedStringAttribute」というキーワードで引いてみるというテクニックも可能である。しかし、この省略は属性として記述する場合にのみ有効なものであり、クラス名として記述する場合はAttributeという文字列を省略できない。
なお、リファレンス・マニュアルの古い版では、VBFixedStringAttributeを引いても、これが属する名前空間が明記されていないが、これはオブジェクトブラウザで見ればすぐに確認できる。VBFixedStringAttribute属性の名前空間は、Microsoft.Visual Basicである。つまり、そのフルネームは、Microsoft.Visual Basic.VBFixedStringAttributeとなる。
|
●図14-8 オブジェクトブラウザで表示させたVBFixedStringAttribute属性(VBFixedStringAttributeクラス) |
VB.NETは.NET Frameworkの幅広いクラス・ライブラリにアクセスできるため、Win32 APIを呼び出すDeclare文を使用する機会はVB 6と比べて減少したのではないかと思う。しかし、Win32 APIを呼び出す機会がゼロになったわけではなく、依然としてDeclare文は存在している。このとき問題になるのが、文字列などのデータ型がVB.NETとWin32 APIで互換性がまったくないことだ。VB.NETの文字列は、System.Stringクラスの文字列クラスにより表現されるが、Win32 APIでは文字型の配列として表現される。この相違を乗り越えるには、API呼び出し時に適切なデータ型の変換が必要となる。このときの変換方法を指定する手段として、MarshalAs属性が使用される。リスト14-9は、実際にMarshalAs属性を使用したサンプル・プログラムである。
1: Imports System.Runtime.InteropServices
2:
3: Public Class Form1
4: Inherits System.Windows.Forms.Form
5:
6: …Windows フォーム デザイナで生成されたコード…
7:
8: Declare Auto Sub MessageBox Lib "user32.dll" ( _
9: ByVal hWnd As Integer, _
10: <MarshalAs(UnmanagedType.LPTStr)> ByVal lpText As String, _
11: <MarshalAs(UnmanagedType.LPTStr)> ByVal lpCaption As String, _
12: ByVal uType As Integer)
13:
14: Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
15: MessageBox(0, "Hello!", "Sample Program", 0)
16: End Sub
17: End Class
|
|
リスト14-9 Declare文の使用時に、データ型の変換方法を指定するMarshalAs属性を使用したプログラム
|
これを実行すると以下のようになる。
|
●図14-10 図14-9の実行結果 |
ここで注目すべき点は、8〜12行目のDeclare文の中で使用されているMarshalAs属性である。10行目と11行目の文字列は、そのままWin32 APIに渡すことができないので、変換する必要がある。そこで、MarshalAs属性はその必要性を指定している。具体的にどのようなデータ型に変換するかは、UnmanagedType列挙型の値で指定している。MarshalAs属性とUnmanagedType列挙型は、System.Runtime.InteropServices名前空間に属しているので、1行目のImports文でこの名前空間を指定している。
さて、Win32 API呼び出し時の文字列に関するもう1つの問題は、Win32 APIには文字コードの相違によって同じ機能を持ったAPIが実際には2つ存在している場合があることだ。つまり、その2つのどちらを呼び出すかの明示的な指定と、どちらの文字コードを指定して引数を変換させるかという指定が必要とされる。リスト14-9の場合、Declareキーワードの次のAutoキーワードが、OSの種類に応じて、自動的に2つのWin32 APIの中から選択することを指定している。Windows 9X系ではANSI系を、Windows NT系ではUnicode系のAPIが呼ばれることになると思う。引数の変換に指定したUnmanagedType.LPTStrという値は、状況に応じて適切な文字コードを選択するという機能を持つ。しかし、自動判定に頼らず、明示的に指定することもできる。以下は、明示的にUnicode系のWin32 APIを選ぶように記述したソースである。
1: Imports System.Runtime.InteropServices
2:
3: Public Class Form1
4: Inherits System.Windows.Forms.Form
5:
6: …Windows フォーム デザイナで生成されたコード…
7:
8: Declare Auto Sub MessageBoxW Lib "user32.dll" ( _
9: ByVal hWnd As Integer, _
10: <MarshalAs(UnmanagedType.LPWStr)> ByVal lpText As String, _
11: <MarshalAs(UnmanagedType.LPWStr)> ByVal lpCaption As String, _
12: ByVal uType As Integer)
13:
14: Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
15: MessageBoxW(0, "Hello!", "Sample Program", 0)
16: End Sub
17: End Class
|
|
リスト14-11 Unicode系のWin32 APIを呼び出すように図14-9を修正したプログラム
|
ソース・コードの変更点は2つある。1つは、8行目のメソッド名をMessageBoxからMessageBoxWに変更し、Unicode系のMessageBox APIの本当の名前を指定したことだ。もう1つは、10〜11行目のMarshalAs属性で、Unicode文字列を意味するUnmanagedType.LPWStrという値を指定したことである。なお、いうまでもなく、メソッドの名前はDeclare文のAliasキーワードを活用すれば変更可能なので、MessageBoxという名前で呼び出すようにもできる。
逆に、ANSI系(日本ではシフトJIS)を用いて文字列を受け渡すように変更したサンプル・プログラムをリスト14-12に示す。
1: Imports System.Runtime.InteropServices
2:
3: Public Class Form1
4: Inherits System.Windows.Forms.Form
5:
6: …Windows フォーム デザイナで生成されたコード…
7:
8: Declare Auto Sub MessageBoxA Lib "user32.dll" ( _
9: ByVal hWnd As Integer, _
10: <MarshalAs(UnmanagedType.LPStr)> ByVal lpText As String, _
11: <MarshalAs(UnmanagedType.LPStr)> ByVal lpCaption As String, _
12: ByVal uType As Integer)
13:
14: Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
15: MessageBoxA(0, "Hello!", "Sample Program", 0)
16: End Sub
17: End Class
|
|
リスト14-12 ANSI系のWin32 APIを呼び出すように図14-9を修正したプログラム
|
ここでは、Win32 APIの名前はANSI系のWin32 APIとしての本名であるMessageBoxAに変更し、変換するデータ型をUnmanagedType.LPStrとしている。UnmanagedType.LPStrは、ANSI系の文字列を示す値である。
メソッドを呼び出す際、呼び出されるメソッドにSystem.Diagnostics.ConditionalAttribute属性が設定されている場合、それは条件付きメソッドと呼ばれるものになる。リスト14-13は、条件付きメソッドを使用したサンプル・プログラムである。
1: Public Class Form1
2: Inherits System.Windows.Forms.Form
3:
4: …Windows フォーム デザイナで生成されたコード…
5:
6: <Conditional("Debug")> Private Sub sample()
7: Trace.WriteLine("Hello")
8: End Sub
9:
10: Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
11: Trace.WriteLine("call sample")
12: sample()
13: Trace.WriteLine("sample done")
14: End Sub
15: End Class
|
|
リスト14-13 条件付きメソッドを使用したプログラム
|
これをDebugビルドで実行すると以下のような結果になる。
1: call sample
2: Hello
3: sample done
|
|
リスト14-14 リスト14-13をDebugビルドで実行した場合の実行結果
|
また、Releaseビルドで実行すると以下のような結果になる。
1: call sample
2: sample done
|
|
リスト14-15 リスト14-13をReleaseビルドで実行した場合の実行結果
|
見てのとおり、ビルドの設定によって結果が異なっていることが分かるだろう。その点で条件付きコンパイルと似ているが、メソッド単位で効力を発揮するという点で異なるものである。
メソッド呼び出しが可能かどうかの判断は、System.Diagnostics.ConditionalAttribute属性の引数に指定された条件付き定数(条件付きコンパイル定数)によって判断される。このあたりは、条件付きコンパイルと同様である(条件付きコンパイルを参照)。