属性(Attribute)はデリゲートなどと同様、.NET Frameworkで採用されたプログラミングにおける新しい概念です。 といっても、デリゲートのように理解するのに多少の苦労を要するようなものではなく、属性は比較的簡単に理解することができると思います。 というのも、属性を指定する構文が単純で、ちょっとしたコメントをつけるのと同じ感覚で利用できるからです。

Sponsored Link

属性の概要

属性は様々な場所で、いろいろなプログラム要素に対して使用されます。 一般的に、いままで言語仕様の範囲を超えたオプションなどは、特別な構文やコンパイルオプションなどとして言語から切り離されていることがありました。 しかし、属性として存在する機能、例えば SerializableAttribute, ObsoleteAttribute, DebuggerStepThroughAttributeなどの機能は言語仕様の範囲を越えていますが、プログラムと非常に密接に関わり合っています。

つまり、プログラムと深い関係がある情報(メタデータ)を、言語の機能を拡張することなく記述することができるのが属性です。 また、属性を採用することにより言語仕様の拡張をする必要がなくなるので、Visual C++とBorland C++などのように、拡張した機能によって言語間の互換性が失われると言う心配もありません。

属性クラスの仕様

.NET Frameworkには様々な機能を持った属性が存在します。 これだけでも十分と言えるのですが、何か独自の機能を追加するためには独自に属性を定義する必要があります。 しかし、独自に属性を定義すると言っても、実際に行うのは新たにクラス型を宣言するのと大差ありません。 というのは全ての属性は System.Attribute クラスの派生クラスであるからです。 実際にはそれだけではないので、独自の属性としてクラスを宣言するために必要な要件を次にまとめます。

  1. 属性となるクラスはSystem.Attributeを継承している必要がある。
  2. 属性となるクラスはpublicとして宣言されている必要がある。
  3. 属性となるクラスの名前はAttributeで終わるべきである。 Attributeで終わる場合は、属性を適用する際にAttributeを省略することができる。
  4. 属性となるクラスは、その属性がどのプログラム要素に対して使用するものかを指定するため、AttributeUsage属性を適用することができる。 (ただし、VB.NETでは必須)
  5. 属性となるクラスは、最低でも一つのパブリックコンストラクタを持っている必要がある。
  6. 属性のパラメータは、単純型、文字列型、オブジェクト型、列挙型、これらいずれかの一次元配列型、System.Typeの定数値でなければならない。

さらに、属性クラスの適用先などを指定するAttributeUsage属性のパラメータの詳細を次にまとめます。

ValidOn
属性の適用対象を指定する。 アセンブリ、クラス、フィールド、メソッド、戻り値、パラメータなど。
AllowMultiple
一つの要素に対して複数回同じ属性を指定することができるかを指定する。
Inherited
その属性を適用されたクラスから派生したクラスが、その属性を継承できるかどうかを指定する。

また、属性のパラメータの指定方法には二種類あり、一種類はコンストラクタの引数に対して使用する位置パラメータと、パブリックなフィールドに対して指定する名前付きパラメータです。 位置パラメータでは省略する事ができず、また、パラメータの位置によってその意味が変わります。 名前付きパラメータでは、省略が可能で、また位置も関係ありません。 次にその二種類のパラメータの例を示します。

// 位置パラメータを使用した例
[AttributeUsage( AttributeTargets.All )]
class UsingPositionalParameterAttribute : Attribute
{
}

// 位置パラメータと名前付きパラメータを使用した例
[AttributeUsage( AttributeTargets.All, AllowMultiple = true )]
class UsingNamedParameterAttribute : Attribute
{
}

AttributeUsage属性では引数をとらないコンストラクタがなく、先ほど説明したValidOnはコンストラクタの引数、つまり位置パラメータとなっているので必ず指定する必要があります。 しかし、AllowMultipleはプロパティとして存在し、名前付きパラメータとして指定することができるので省略することができます。 名前付きパラメータは必ず名前を指定しなければなりません。

独自の属性の宣言

これで独自の属性の宣言をする準備ができました。 そこで数種類の独自定義の属性クラスのサンプルを作ってみます。

独自に定義した属性と、その属性を適用した例
using System;



// 全ての要素を対象とした属性
[AttributeUsage( AttributeTargets.All )]
class UseForAllTargetsAttribute : Attribute
{
}

// メソッドに対して複数回指定できる属性
[AttributeUsage( AttributeTargets.Method, AllowMultiple = true )]
class UseForMethodAllowMultipleAttribute : Attribute
{
}

// 派生クラス・構造体に対して指定できる属性
[AttributeUsage( AttributeTargets.Struct | AttributeTargets.Class, Inherited = true )]
class UseForStructAndDerivedClassAttribute : Attribute
{
}

// これらの属性を適用したクラス
[UseForAllTargetsAttribute, UseForStructAndDerivedClass]
class UsingAttributeClass
{
    [UseForMethodAllowMultiple, UseForMethodAllowMultiple]
    public void UsingAttributeMethod()
    {
    }
}






// 位置パラメータとしてint型の値を三つとる属性  (AttributeUsage属性の適用を省略)
class ThreeArgumentsAttribute : Attribute
{
    public ThreeArgumentsAttribute( int x, int y, int z)
    {
    }
}

// ThreeArgumentsAttribute を適用したクラス
[ThreeArguments( 1, 2, 3 )]
class UsingThreeArgumentClass
{
}





// 名前付きパラメータで指定する属性 (AttributeUsage属性の適用を省略)
class NamedParameterAttribute : Attribute
{
    public string StringValue = null;
    public int    IntValue    = 0;
    public Type   TypeValue   = null;
}

// NamedParameterAttribute を適用したクラス
[NamedParameter( StringValue = "String value", TypeValue = typeof( UsingNamedParameterClass ) )]
class UsingNamedParameterClass
{
}

この例では属性となるクラスの名前には全てAttributeをつけているので、どれが属性かはすぐわかると思います(そうでなくても、 Attributeを継承しているか否かでわかるはずです)。 まず、三つの属性クラスUseForAllTargetsAttribute、 UseForMethodAllowMultipleAttribute、UseForStructAndDerivedClassAttributeを宣言しています。 これらの詳細はコメントにあるとおりですが、実装は全くしていません。 また、これらの属性を適用したクラスが、 UsingAttributeClassです。

このクラスを見てわかるとおり、属性を複数指定する場合には、「,(カンマ)」で区切ることで行えます。 また、かぎ括弧[]を複数回利用するという手段もとれます。 言うまでもありませんが、属性が対象としていない要素に属性を指定しようとするとコンパイルエラーになります。 さらに、この例ではその有効性を発揮していませんが、AllowMultipleにtrueを指定した属性は複数回指定することができます。

続いて、38行目から50行目は位置パラメータを持つ属性とそれを適用したクラスの宣言です。 これも詳細な機能は実装はされていません。 位置パラメータではコンストラクタの引数として値を受け取るので、省略することができないので、全ての引数に対して値を設定する必要があります。 対して名前付きパラメータで指定する属性では省略することが可能です。 しかし、その代わりに指定するパラメータの名前を記述する必要があります。

56行目から68行目は名前付きパラメータを持つ属性とそれを適用したクラスの宣言ですが、この属性は、string、int、 Type型のパブリックメンバを持ちますが、コンストラクタは存在しません。 これらのメンバが名前付きパラメータとして値を指定できるメンバとなります。 65行目で実際にこの属性を適用していますが、IntValueのパラメータの指定を省略しています。 ここで、パラメータとなり得るメンバには、省略される可能性があるので必ず初期値を代入しておきます。

最後に、今まで説明してきませんでしたが、任意の属性クラスから派生したクラスを適用することもできます(全ての属性はAttributeクラスから派生しているので言うまでもありませんが、要するに直接または間接のいずれの継承でかまわないということです)。

アセンブリに適用された属性の取得

これまでは、独自の属性(カスタム属性)の宣言とその適用のサンプルを見てきたわけですが、属性自体にそのコードが実装されていませんでした。 また、属性にソースコードが記述されているからと言ってもそのまま属性が動作するわけではありません。 属性が特定の要素に対して適用されているかを調べ、適用されているならばその属性を取得し、適切なコードを実行する必要があります。 今までの例では、宣言されているだけで属性としての機能を果たしていません。

そこで、ここからは適用された属性を取得し、適切なコードを実行するための方法を考えていきます。 といっても、先程述べたとおり、実際には対象から属性を取得するだけなのでそれほど複雑なコーディングなどは必要ありません。 次に示すサンプルではカスタム属性 CopyrightAttributeを宣言し、アセンブリにその属性を適用、さらに現在実行中のアセンブリに適用されている全ての属性を列挙するという、属性に関する基本的な流れを記述しています。 今まで特に説明しませんでしたが、属性はアセンブリ自体にも指定できますが、その場合は [assembly: 属性名]と指定します。 ちなみに、VB.NETではモジュールに対しても属性を指定できます。

アセンブリへの属性の適用と取得
using Attributes;
using System;
using System.Reflection;

// アセンブリに対する属性の適用
[assembly: Copyright( "santa marta", 2003 )]

namespace Attributes
{
    // カスタム属性
    [AttributeUsage(AttributeTargets.Assembly | 
                    AttributeTargets.Class |
                    AttributeTargets.Method,
                    Inherited = true)]
    public class CopyrightAttribute : Attribute
    {
        private string m_Author;
        private int    m_Year;

        public CopyrightAttribute( string author, int year )
        {
            m_Author = author;
            m_Year   = year;
        }

        public string Author
        {
            get { return m_Author; }
        }

        public int Year
        {
            get { return m_Year; }
        }

        public override string ToString()
        {
            return "CopyrightAttribute: Copyright(C) " + m_Year.ToString() + " " + m_Author;
        }
    }

    // アプリケーションのエントリーポイントを提供するクラス
    class ApplicationEntrance
    {
        [STAThread]
        static void Main(string[] args)
        {

            // 現在実行中のアセンブリを取得する
            Assembly assm = Assembly.GetExecutingAssembly();

            // アセンブリに指定されている属性を取得する
            object[] attrs = assm.GetCustomAttributes( typeof( Attribute ), true );

            // 属性を列挙
            foreach ( Attribute attr in attrs )
            {
                Console.WriteLine( attr );
            }

        }
    }

}
実行結果
System.Reflection.AssemblyCopyrightAttribute
System.Reflection.AssemblyProductAttribute
System.Diagnostics.DebuggableAttribute
System.Reflection.AssemblyKeyFileAttribute
System.Reflection.AssemblyDelaySignAttribute
System.Reflection.AssemblyTrademarkAttribute
System.Reflection.AssemblyConfigurationAttribute
CopyrightAttribute: Copyright(C) 2003 santa marta
System.Reflection.AssemblyCompanyAttribute
System.Reflection.AssemblyKeyNameAttribute
System.Reflection.AssemblyDescriptionAttribute
System.Reflection.AssemblyTitleAttribute

実行結果の検証は後にして、ひとまずコードの説明をしていきます。 属性の宣言自体は特に問題ないと思います。 この属性はアセンブリ、クラス、メソッドに対して使用し、その対象の著作権を記述するための属性です。 プロパティが読み取り専用になっているのは、コンストラクタで全ての値を指定させるためと、指定された後にその値を参照するためです。

実際に属性を取得するコードは49行目から58行目に記述されています。 まず、現在実行中のアセンブリを取得します。 次にこのアセンブリから指定されている全ての属性を取得します。 それがGetCustomAttributes()メソッドです。 このメソッドは二つのバージョンがあり、今回は引数を二つとるバージョンを使用します。 一つ目の引数には取得したい属性の型情報を指定します。 二つ目の引数は一つ目の引数で Attributeクラスとその派生クラスを指定した場合には無視されます。 また、このメソッドの戻り値はobject型の配列となり、ここに適用されている属性の一覧が格納されます。 これでアセンブリから属性を取得することができます。

このサンプルではアセンブリに指定された属性を列挙していますが、アセンブリに対する属性の適用は6行目だけで記述されているはずなのに、実行結果を見るとこのアセンブリには複数の属性が指定されています。 というのも、アセンブリに対する属性の適用はこのファイルに記述されているコードでは6行目だけで適用していますが、このプロジェクトにはAssemblyInfo.csというアセンブリ情報ファイルが含まれていて、そこでも属性が指定されているためです。

アセンブリ情報ファイル
using System.Reflection;
using System.Runtime.CompilerServices;

//
// アセンブリに関する一般情報は以下の 
// 属性セットを通して制御されます。アセンブリに関連付けられている 
// 情報を変更するには、これらの属性値を変更してください。
//
[assembly: AssemblyTitle("")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("")]
[assembly: AssemblyCopyright("")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]        

//
// アセンブリのバージョン情報は、以下の 4 つの属性で構成されます :
//
//      メジャー バージョン
//      マイナ バージョン 
//      ビルド番号
//      リビジョン
//
// 下にあるように、'*' を使って、すべての値を指定するか、 
// ビルドおよびリビジョン番号を既定値にすることができます :

[assembly: AssemblyVersion("1.0.*")]

//
// アセンブリに署名するには、使用するキーを指定しなければなりません。 
// アセンブリ署名に関する詳細については、Microsoft .NET Framework ドキュメントを参照してください。
//
// 下記の属性を使って、署名に使うキーを制御します。 
//
// メモ : 
//   (*) キーが指定されないと、アセンブリは署名されません。
//   (*) KeyName は、コンピュータにインストールされている
//        暗号サービス プロバイダ (CSP) のキーを表します。KeyFile は、
//       キーを含むファイルです。
//   (*) KeyFile および KeyName の値が共に指定されている場合は、 
//       以下の処理が行われます :
//       (1) KeyName が CSP に見つかった場合、そのキーが使われます。
//       (2) KeyName が存在せず、KeyFile が存在する場合、 
//           KeyFile にあるキーが CSP にインストールされ、使われます。
//   (*) KeyFile を作成するには、sn.exe (厳密な名前) ユーティリティを使ってください。
//       KeyFile を指定するとき、KeyFile の場所は、
//       プロジェクト出力 ディレクトリへの相対パスでなければなりません。
//       パスは、%Project Directory%\obj\<configuration> です。たとえば、KeyFile がプロジェクト ディレクトリにある場合、
//       AssemblyKeyFile 属性を 
//       [assembly: AssemblyKeyFile("..\\..\\mykey.snk")] として指定します。
//   (*) 遅延署名は高度なオプションです。
//       詳細については Microsoft .NET Framework ドキュメントを参照してください。
//
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile("")]
[assembly: AssemblyKeyName("")]
 です。たとえば、KeyFile がプロジェクト ディレクトリにある場合、
//       AssemblyKeyFile 属性を 
//       [assembly: AssemblyKeyFile("..\\..\\mykey.snk")] として指定します。
//   (*) 遅延署名は高度なオプションです。
//       詳細については Microsoft .NET Framework ドキュメントを参照してください。
//
[assembly: AssemblyDelaySign(false)]
[assembly: AssemblyKeyFile("")]
[assembly: AssemblyKeyName("")]]]>

このコードを見てわかるとおり、全てのコードが属性からなっています。 これらの属性は全てアセンブリに対して適用されているので、アセンブリに指定されている属性を取得しようとするとこれらの属性も取得されるわけです。

ところで、これらのバージョン情報など情報は、Visual Basic では実行可能ファイルを生成する際のプロパティとして、Visual C++ ではリソースとして埋め込まれていました。 しかし、このアセンブリ情報ファイルのように.NET Frameworkでは同様のことを属性によって実現することができます。 メタデータを属性として任意の場所で記述できることが、属性の特徴であると言えるのではないでしょうか。

型に適用された属性の取得

アセンブリ以外からの属性の取得も同様の方法で行えます。 実際にはICustomAttributeProviderインターフェイスを実装したクラスのGetCustomAttributes()を呼び出せば取得できます。 といっても実際のサンプルをみてみないと説明にならないので、先ほどの属性をクラスとそのメソッドに適用し、それらを取得するコードを次に示します。

型からの属性の取得
using System;
using System.Diagnostics;
using System.Reflection;

namespace Attributes
{
    // 属性を適用されたクラス
    [Copyright( "santa marta", 2003 ), DebuggerStepThrough()]
    class UsingAttributeClass
    {
        [Copyright( "Some programmer", 2003 )]
        public int ReturnsIntMethod()
        {
            return 0;
        }

        [Copyright( "Old programmer", 1999 )]
        public string ReturnsStringMethod()
        {
            return "";
        }

        [Copyright( "Another programmer", 2001 )]
        public void ReturnsVoidMethod()
        {
        }
    }

    // アプリケーションのエントリーポイントを提供するクラス
    class ApplicationEntrance
    {
        [STAThread]
        static void Main(string[] args)
        {
            // どこかでインスタンスが作成されたとする
            UsingAttributeClass inst = new UsingAttributeClass();

            // そのインスタンスの型情報を取得する
            Type t = inst.GetType();

            // 型に指定されているCopyrightAttribute属性を取得する
            object[] attrs = t.GetCustomAttributes( typeof( CopyrightAttribute ), true );

            // 列挙する
            Console.WriteLine( t.Name + " に指定されている属性" );

            foreach ( CopyrightAttribute attr in attrs )
            {
                Console.WriteLine( attr );
            }

            Console.WriteLine();


            // 型情報からその型のメソッド情報を取得する
            MethodInfo[] methods = t.GetMethods();

            // 全てのメソッドに対して
            foreach ( MethodInfo method in methods )
            {
                // 属性を取得
                attrs = method.GetCustomAttributes( typeof( CopyrightAttribute ), true );

                if ( 0 < attrs.Length )
                {
                    // 列挙する
                    Console.WriteLine( t.Name +"." + method.Name + "() に指定されている属性" );

                    foreach ( CopyrightAttribute attr in attrs )
                    {
                        Console.WriteLine( "\t" + attr );
                    }
                }
                else
                {
                    Console.WriteLine( t.Name +"." + method.Name + "() には属性は指定されていません" );
                }
            }
        }
    }
}
実行結果
UsingAttributeClass に指定されている属性
CopyrightAttribute: Copyright(C) 2003 santa marta

UsingAttributeClass.GetHashCode() には属性は指定されていません
UsingAttributeClass.Equals() には属性は指定されていません
UsingAttributeClass.ToString() には属性は指定されていません
UsingAttributeClass.ReturnsIntMethod() に指定されている属性
        CopyrightAttribute: Copyright(C) 2003 Some programmer
UsingAttributeClass.ReturnsStringMethod() に指定されている属性
        CopyrightAttribute: Copyright(C) 1999 Old programmer
UsingAttributeClass.ReturnsVoidMethod() に指定されている属性
        CopyrightAttribute: Copyright(C) 2001 Another programmer
UsingAttributeClass.GetType() には属性は指定されていません
Press any key to continue

この例を見てわかるとおり、クラス自体にはCopyright属性の他にもDebuggerStepThrough属性を指定していますが、 CopyrightAttributeクラスの属性だけを取得しているので一覧には表示されません。 また、型情報からメソッドの情報を取得すると、その基底クラスのメソッドも含めた全てのメソッドが取得されます。 これらの型情報、メソッド情報からそれぞれGetCustomAttributes()メソッドを呼び出すことで、その対象に指定されている属性を取得することができます。

また、この例ではインスタンスから型情報を取得する方法で属性を取得していますが、インスタンスではなく直接型から型情報を取り出すという方法もとれます。 どちらを用いるかは属性の性質により定まると思います。