属性名は、属性クラスの名前から最後のAttributeの部分を除いた名前を使用するのが普通だが、除かない名前を使用することもできる。List 20-6はそれを示した例である。
1: using System;
2: using System.Reflection;
3:
4: namespace Sample006
5: {
6: [AttributeUsage(AttributeTargets.Class)]
7: class SampleAttribute : Attribute
8: {
9: public SampleAttribute()
10: {
11: }
12: }
13: [Sample]
14: class Sample1
15: {
16: }
17: [SampleAttribute]
18: class Sample2
19: {
20: }
21: class Class1
22: {
23: public static void dumpAuthor( string className )
24: {
25: Type type = Type.GetType("Sample006." + className);
26: object [] list = type.GetCustomAttributes(typeof(SampleAttribute),false);
27: if( list.Length > 0 )
28: {
29: Console.WriteLine( "class {0} has SampleAttribute attribute.", className );
30: }
31: }
32: [STAThread]
33: static void Main(string[] args)
34: {
35: dumpAuthor( "Sample1" );
36: dumpAuthor( "Sample2" );
37: dumpAuthor( "Class1" );
38: }
39: }
40: }
これを実行するとFig.20-6のようになる。
このソースでは、13行目と17行目で、それぞれAttributeを含む名前と含まない名前を記述している。しかし、26行目からSampleAttributeクラスの属性を調べれば、両方がSampleAttributeクラスとして認識されていることが分かるだろう。
余談だが、37行目ではClass1クラスを指定しているが、このクラスにはいかなる名前でもSampleAttributeクラスの属性は付いていないので、これに対応する表示は何も出ない。27行目でlist.Lengthが0になるためだ。
C#言語仕様書では、Conditional属性、AttributeUsage属性と並んで、もう1つObsoleteという名前の属性が標準で予約されている。これは、古くなって廃止予定の機能に付加するもので、将来なくなるので使わない方がよいと告げるために使用される。プログラムを開発中に、互換性のためにやむをえず残しているものの、もう新規には使ってほしくない機能というものが生じることがある。そのような状況に対してエレガントに対処するために積極的に使用するとよいだろう。List 20-7はObsolete属性を使用した例である。
1: using System;
2:
3: namespace Sample007
4: {
5: [Obsolete("Sample1は今後サポートされません。Sample2を使ってください。")]
6: class Sample1
7: {
8: }
9: class Sample2
10: {
11: }
12: class Class1
13: {
14: [STAThread]
15: static void Main(string[] args)
16: {
17: Console.WriteLine( new Sample1() );
18: }
19: }
20: }
これをビルドするとFig.20-7のようになる。
1: ------ ビルド開始 : プロジェクト : Sample007, 構成 : Debug .NET ------
2:
3: リソースを準備しています...
4: 参照を更新しています...
5: メイン コンパイルを実行しています...
6: q:\awrite\gh\cs\wk2\samples\chapt20\sample007\class1.cs(17,27): warning CS0618: 'Sample007.Sample1' は古い形式です : 'Sample1は今後サポートされません。Sample2を使ってください。'
7:
8: ビルドの完了 -- エラー 0、警告 1
9: サテライト アセンブリをビルドしています...
10:
11:
12:
13: ---------------------- 終了 ----------------------
14:
15: ビルド : 1 正常終了、0 失敗、0 スキップ
List 20-7の5行目に記述されているのがObsolete属性である。引数には文字列を指定する。引数に指定された文字列が、ビルド結果に表示される。ビルド結果の6行目の後半は、Obsolete属性の引数に記述した内容がそのまま表示されたものだ。
プリプロセッサの#warningでも同じような機能が実現できそうに思えるが、動作は同じではない。#warningは処理されれば必ず警告を発するが、Obsolete属性はその属性が付いた機能が利用されないかぎりメッセージは表示されない。ソース上にあっても利用しなければ警告されないので、互換のために残しながら使うのに適している。
本節および次節のサンプル・ソースはAPIやDLL呼び出しに興味のある読者向けの話題である。すべての読者が理解する必要はない。
Win32 APIなどの外部のDLLを呼び出すためにも属性が使われる。Win32 APIだけでなくCOMオブジェクトの呼び出しにも使用できるが、Visual Studio .NETにはいちいち手動で属性を書かなくても簡単に呼び出す方法があるので、解説は割愛する。
まずは、シンプルに整数1つだけを引数に取るAPIを呼び出してみよう。List 20-8を見てほしい。
1: using System;
2: using System.Runtime.InteropServices;
3:
4: namespace Sample008
5: {
6: class Class1
7: {
8: [DllImport("user32.dll")]
9: public static extern int MessageBeep( uint uType );
10: [STAThread]
11: static void Main(string[] args)
12: {
13: MessageBeep(0);
14: }
15: }
16: }
これを実行するとスピーカーから音が鳴るはずである。
ここでポイントになるのは、もちろん、8行目のDllImport属性である。この属性は、externキーワードの付いたメソッド宣言(9行目)に付く。externキーワードは、処理の実体が外部にあることを示すキーワードである。8〜9行目の指定により、外部にあるメソッドを呼び出す指定がなされている。具体的に、どこにあるものを呼び出しているかというと、DllImport属性の引数で指定されたuser32.dllというシステムDLLである。その中にあるMessageBeepという関数が呼び出される。
このように引数が整数に限られれば、API呼び出しもさほど難しいものではない。.NET Frameworkは機能が充実しているので、めったにAPIを呼び出す必要はないが、まれに、存在しない機能に遭遇することがあるので、知っておく価値があるだろう。
なお、これは、ポインタなどが使用できる「安全でないコード」とは異なるものであることに注意が必要である。このDLL呼び出しは、システムによる安全なメモリ管理下で動作するものである。
Visual BasicからAPI呼び出しを利用した経験者なら、文字列などを受け渡すようになると、急に事態がややこしくなる状況に遭遇したことがあるだろう。これは、文字列の表現方法がプログラミング言語によって異なっていたり、文字のエンコード方法にバリエーションがあったりすることにより発生する。これらの問題に対処するために、DllImport属性には多くの名前付きパラメータが用意されている。List 20-9はそれを駆使して、ANSIバージョンのMessageBox APIと、UnicodeバージョンのMessageBox APIを呼び出してみた例である。
1: using System;
2: using System.Runtime.InteropServices;
3:
4: namespace Sample009
5: {
6: class Class1
7: {
8: [DllImport("user32.dll", CallingConvention=CallingConvention.Winapi,
9: CharSet=CharSet.Ansi, EntryPoint="MessageBoxA",
10: ExactSpelling=false, PreserveSig=true, SetLastError=false )]
11: public static extern int MyMessageBox1(
12: uint hWnd,
13: [MarshalAs(UnmanagedType.LPStr)] string lpText,
14: [MarshalAs(UnmanagedType.LPStr)] string lpCaption,
15: uint uType
16: );
17:
18: [DllImport("user32.dll", CallingConvention=CallingConvention.Winapi,
19: CharSet=CharSet.Unicode, EntryPoint="MessageBoxW",
20: ExactSpelling=false, PreserveSig=true, SetLastError=false )]
21: public static extern int MyMessageBox2(
22: uint hWnd,
23: [MarshalAs(UnmanagedType.LPWStr)] string lpText,
24: [MarshalAs(UnmanagedType.LPWStr)] string lpCaption,
25: uint uType
26: );
27: [STAThread]
28: static void Main(string[] args)
29: {
30: MyMessageBox1(0,"by ANSI","Hello!",0);
31: MyMessageBox2(0,"by Unicode","Hello!",0);
32: }
33: }
34: }
これを実行するとFig.20-8のようになる。
DllImport属性に付いている名前付き属性は、それぞれ、以下のような意味を持つ。CallingConventionは呼び出し方法(stdcallなど)の種類を指定する。CharSetはANSIかUnicodeかを指定する。EntryPointはAPIの真の名前を指定する。ここで呼び出したいAPIは、ANSIバージョンがMessageBoxA、UnicodeバージョンがMessageBoxWである。ExactSpellingは名前の文字列が完全に一致する場合のみ利用可能とするかどうかを指定する。PreserveSigはメソッドのシグネチャを保存することを指定する。SetLastErrorは、そのAPIがエラーコードをSetLastError APIで設定するかどうかを指定する。これらにより、異なる文字コードを使用する2つのMessageBox APIを別個に呼び出すようにコードを記述している。
このサンプル・ソースでは文字列の変換も明示的に指定してみた。13〜14行目、23〜24行目のMarshalAs属性は、引数に付く属性である。これはC#ソース側からstring型の引数に見える文字列を実際の呼び出し時にどんなデータ型に変換するかを、明示的に指定するものである。引数のUnmanagedType.LPStrはWin32 APIのLPSTRを、UnmanagedType.LPWStrはLPWSTRをそれぞれ意味する。
Copyright© Digital Advantage Corp. All Rights Reserved.