連載

C#入門

第20回 実行時に参照可能な属性

(株)ピーデー
川俣 晶
2002/01/23


属性(アトリビュート)とは何か?

 通常、プログラムのソースコードは、プログラム言語の言語仕様の範囲内で記述する。しかし、しばしば、言語仕様の範囲内だけでは不自由であるという状況が発生し、言語仕様を拡張しようという動きが出てくる。例えばVisual C++において、COMサポートのために言語仕様を拡張した例などがある。しかし、さまざまな企業が個々の事情で勝手に拡張しては、プログラム言語に非互換性が生じてしまう。これに対処する1つの方法が、C#の「属性(アトリビュート)」である。属性は、クラスやメソッドに対して付加情報を付け加える機能を提供するが、具体的な属性は自由に定義することができ、プログラム言語の仕様を拡張することなく、独自の情報をクラスやメソッドに与えることができる。例えばWebサービスを記述する際には、通常のメソッドと、Webサービスで外部公開するメソッドを区別する必要があるが、そのために言語仕様を拡張するのではなく、属性を用いて対処するわけである(Webサービスの具体的なソースコード例は後で紹介する)。

 では、具体的に属性が使われている例を見てみよう。以下は、デバッグ・ビルドの場合のみメッセージを表示するというサンプル・ソースである。前回解説したプリプロセッサを用いても実現可能であるが、今回は属性を用いた方法をご紹介しよう。

 1: using System;
 2: using System.Diagnostics;
 3:
 4: namespace ConsoleApplication15
 5: {
 6:   class Class1
 7:   {
 8:     [Conditional("DEBUG")]
 9:     private void test()
10:     {
11:       Console.WriteLine("test called");
12:     }
13:     static void Main(string[] args)
14:     {
15:       Class1 instance = new Class1();
16:       Console.WriteLine("call start");
17:       instance.test();
18:       Console.WriteLine("call end");
19:     }
20:   }
21: }
デバッグ・ビルドの場合のみメッセージを表示するサンプル・プログラム1
プリプロセッサを用いても実現可能であるが、今回は「Conditional属性」を用いている。

 これを実行すると以下のようになる。

サンプル・プログラム1の実行結果
デバッグ・ビルドとリリース・ビルドでは結果が異なっているのが分かる。

 実行結果を見てのとおり、デバッグ・ビルドとリリース・ビルドでは結果が異なっている。その秘密は、8行目に記述された「Conditional属性」にある。Conditional属性は、引数で指定されたシンボル名が定義されている場合のみ、その属性が付加されたメソッド、つまりConditional属性の直後に書かれたメソッドを実行するようにシステムに指示する。そのため、シンボル“DEBUG”が定義されている場合のみ9〜12行目のメソッドtestは実行されるのである。

 なお、Conditional属性は、「System.Diagnosticsネームスペース」に属するので、2行目でこれをusing指定している。属性にもネームスペースがあることに注意されたい。

 ここで使用したConditional属性は、C#の言語仕様の一部として定義されているもので、これを使うにあたってネームスペースに配慮する以外、特に何かを定義する必要はない。いきなりメソッドの手前に書けば利用できる。

属性を自作する

 Conditional属性を始め、言語仕様やクラス・ライブラリ側で用意される属性も多いが、必要なら自作することもできる。以下は自分で新しい属性を定義して利用した例である。メソッドを書いたプログラマの名前を明示的にソースコードに書き込めるようにしたサンプル・ソースである。

 1: using System;
 2: using System.Reflection;
 3:
 4: namespace ConsoleApplication16
 5: {
 6:   [AttributeUsage(AttributeTargets.Method)]
 7:   class AuthorAttribute : Attribute
 8:   {
 9:     private string _name;
10:     public AuthorAttribute( string name )
11:     {
12:       _name = name;
13:     }
14:     public string name
15:     {
16:       get { return _name; }
17:     }
18:   }
19:   class Class1
20:   {
21:     [Author("Ichiro")]
22:     public void test1()
23:     {
24:     }
25:     [Author("Jiro")]
26:     public void test2()
27:     {
28:     }
29:     [Author("Saburo")]
30:     public static void dumpAuthor( string methodName )
31:     {
32:       MethodInfo info = typeof(Class1).GetMethod(methodName);
33:       object [] list = info.GetCustomAttributes(typeof(AuthorAttribute),false);
34:       foreach( AuthorAttribute item in list )
35:       {
36:         Console.WriteLine( "method {0} is written by {1}", methodName, item.name );
37:       }
38:     }
39:     static void Main(string[] args)
40:     {
41:       dumpAuthor( "test1" );
42:       dumpAuthor( "test2" );
43:       dumpAuthor( "dumpAuthor" );
44:     }
45:   }
46: }
新しい属性を定義して利用したサンプル・プログラム2
属性は自作することができる。メソッドを書いたプログラマの名前を属性としてソースコードに書き込めるようにしている。

 これを実行すると以下のようになる。

サンプル・プログラム2の実行結果
メソッド名とメソッドに付けたAuthor属性の引数が表示される。

 このソースコードのおおまかな構成から説明しよう。6〜18行目では、新しい属性を定義している。21〜38行目は、定義した属性を使ってプログラマの名前を書き込んでみた例である。30〜38行目は、書き込まれた情報を表示するメソッドでもある。そして、39〜45行目はメイン・メソッドである。

 まず、属性の定義から見ていこう。属性は一種のクラスとして定義される。クラスを属性にするには、7行目のように「System.Attirbuteクラス」を継承する。それに加えて、やるべきことが2つある。1つは、クラスの名前を“Attribute”で終わるようにすることと、6行目のように「AttributeUsage属性」を付加して、それが何に付く属性であるかを示すことである。ここでは、AttributeTargets.Methodが指定されているが、これはメソッドに付く属性であることを指定している。AttributeTargetsは列挙型で、他にもクラスなどのバリエーションが定義されている。必要なものを選んで、指定すればよい。“|”で区切って、[AttributeUsage(AttributeTargets.Method|AttributeTargets.Class)]というように複数を並べて指定することもできる。

 6: [AttributeUsage(AttributeTargets.Method)]
 7: class AuthorAttribute : Attribute

 属性に引数を付けるには、属性クラスに引数を持ったコンストラクタを用意すればよい。ここでは、10行目で文字列を1つ引数に取るコンストラクタを定義している。これにより、この属性は1つの文字列を引数に取るようになる。

10: public AuthorAttribute( string name )

 さて、こうして定義した属性は、21、25、29行目のように記述する。6行目でメソッドをターゲットに指定しているので、メソッドの先頭以外に記述するとエラーになる。そして、1個の文字列引数を取るコンストラクタが用意されているので、それぞれの属性には引数として文字列が記述されている。

21: [Author("Ichiro")]
22: public void test1()
23: {
24: }
25: [Author("Jiro")]
26: public void test2()
27: {
28: }
29: [Author("Saburo")]
30: public static void dumpAuthor( string methodName )
31: {

 もう1つ注意してほしいことがある。属性クラスの名前は“AuthorAttribute”であったが、実際に記述されている属性名は“Author”であり、AuthorAttributeではない。通常、属性クラスを定義するときはAttributeを付けた名前を使うが、利用する場合は付けない名前で記述する。このルールは頭に入れておく価値がある。なぜなら、ある属性の仕様を知りたくなったとき、その属性名にAttributeの文字を付け加えた名前のクラスをリファレンス・マニュアルから探せばよいからだ。

 説明が長くなっているが、もう1つ超えるべき山場がある。それは、記述した属性の参照方法だ。属性で記述した情報は実行時に参照できる。といっても、属性はフィールドやメソッドではないので、普通の方法では参照できない。これを参照するには、「リフレクション」という機能を使わねばならない。この連載は、言語仕様を中心に解説し、クラス・ライブラリには深く立ち入らない方針なので、リフレクションの詳細には踏み込まない。しかし、属性を活用する上で最低限必要な知識は解説したい。

 リフレクションは、実行時に実行中のプログラムに関する情報にアクセスする機能である。例えば、自分自身のプログラムの中に、どんなクラスやメソッドがあるかを調べることができる。この機能の一部として、クラスやメソッドにどんな属性が付いているかを調べる機能も用意されている。

 まず、32行目に注目していただきたい。ここでは、typeofキーワードで、クラスClass1に関する情報を持ったSystem.Type型のインスタンスを取得する。このインスタンスは、GetMethodメソッドを持つ。これは、指定されたクラスの持つPublicなメソッドについての情報を取得する機能を持つ。ここでは引数で指定した名前のメソッドについての情報を取得している。このメソッドが返す値のデータ型は32行目の先頭に書かれたMethodInfoクラスである。このクラスには、メソッドについてのさまざまな情報が含まれている。

32: MethodInfo info = typeof(Class1).GetMethod(methodName);

 ここで知りたいのは、属性についての情報なのだが、今回取り上げる属性を取得するには、33行目のように「GetCustomAttributes」というメソッドを呼び出せばよい。このメソッドには2つの引数を指定する。1つは、取得した属性クラスのデータ型を示すSystem.Type型の値。ここでは、typeof演算子でそれを得ている。もう1つは、属性クラスが継承されたクラスかどうかを指定するものだが、ここでは継承関係が絡み合うような使い方はしていないので、シンプルにfalse(継承されていない)を指定している。さて、このメソッドの戻り値は、object型の配列だが、実際の中身はAuthorAttribute型のインスタンスの配列である。つまり、配列の個々の要素をAuthorAttributeにキャストしてやれば、内容にアクセス可能である。

33: object [] list = info.GetCustomAttributes(typeof(AuthorAttribute), false);

 36行目を見て分かるとおり、AuthorAttributeクラスのnameプロパティを参照している。その結果、コンソールに、個々のメソッドを記述したプログラマの名前が表示されるわけである。Aboutボックスに、すべての開発者の名前をリストしたい場合などに利用できる機能かもしれない。

34: foreach( AuthorAttribute item in list )
35: {
36:   Console.WriteLine( "method {0} is written by {1}", methodName, item.name );
37: }

 なお最後になったが、リフレクションの機能はSystem.Reflectionネームスペースにあるので、2行目でこれをusingしている。

 

 INDEX
  第20回 実行時に参照可能な属性
  1.属性(アトリビュート)とは何か?
    2.クラスに付く属性
    3.名前付き引数
    4.廃止予定を事前に告げるObsolete属性
    5.Webサービスで使われる属性
 
「C#入門」


Insider.NET フォーラム 新着記事
  • 第2回 簡潔なコーディングのために (2017/7/26)
     ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている
  • 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
     Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう
  • 第1回 明瞭なコーディングのために (2017/7/19)
     C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える
  • Presentation Translator (2017/7/18)
     Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間