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

§1 属性の概要

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

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

§1.1 属性の具体例 (DllImport)

.NET Frameworkには様々な機能を持った属性が用意されています。 代表的なものとしてDllImportAttribute(DllImport属性)があります。 この属性はC#やVBからプラットフォームAPIの呼び出しを行うために使うもので、VB6以前に存在したDeclare Sub/Functionによる定義に変わるものです。 この属性を使うことで、呼び出したいAPIが含まれるDLLを指定する他、呼び出す際の呼び出し規約、文字列を渡す際の文字セットの指定などを行うことができます。 実行時にはこの属性で指定されたとおりにDLLがロードされ、APIを呼び出す際の文字セットの変換等といった処理が自動的に行われます。 これにより複雑な処理を記述することなく、APIの呼び出しが行えるようになります。

次のコードではAPI関数のLockWorkStationを呼び出しています。 DllImport属性に続けて通常のメソッドと似た形式で宣言している箇所が、API呼び出しの宣言部です。

using System;
using System.Runtime.InteropServices;

class Sample {
  [DllImport("user32.dll")]
  private static extern bool LockWorkStation();

  static void Main()
  {
    // 画面をロックする
    LockWorkStation();
  }
}

DllImport属性を使ってWin32 APIを呼び出す他の例については以下のページをご覧ください。

§1.2 .NET Frameworkで用意されている属性

DllImport属性の他にも、.NET Frameworkには様々な属性が存在します。 よく使われるものとして次のような属性があります。 属性の具体的な使い道を理解するには以下のページが参考になるかもしれません。



§2 カスタム属性

ほとんどの場合、既に.NET Frameworkに存在する属性を使用するだけでも十分ですが、何か独自の機能を追加する場合やメタデータの埋め込みを行いたい場合も出てきます。 そういった場合は、独自の属性(カスタム属性)を定義する必要があります。 しかし、カスタム属性を定義すると言っても、実際に行う必要があるのは新たに属性を表すクラス型を宣言するだけです。 これといって難しいことはありません。

§2.1 カスタム属性の宣言

カスタム属性となるクラスには、いくつかの要件があります。 カスタム属性としてクラスを宣言するために必要な要件を次にまとめます。

  1. 属性となるクラスは、Attributeクラスを(直接または間接的に)継承している必要がある。 (Attributeクラスの派生クラスでなければならない)
  2. 属性となるクラスは、publicとして宣言されている必要がある。
  3. 属性となるクラスは、その属性がどのプログラム要素(クラス、メソッド、プロパティ等)に対して使用するものかを指定するため、AttributeUsage属性を適用することができる。 (ただし、VB.NETでは必須)
  4. 属性のパラメータは、単純型、文字列型、オブジェクト型、列挙型、これらいずれかの一次元配列型、System.Typeの定数値でなければならない。

また、必須ではありませんが、属性となるクラスの名前はAttributeで終わるようにすべきです。 これは、クラスが属性であることがわかりやすくなる他、クラス名がAttributeで終わる場合、その属性を適用する際にAttributeを省略することができるようになるためです。

早速、上記の要件を満たすクラスを宣言し、簡単なカスタム属性を作成してみます。

カスタム属性クラスの宣言
[AttributeUsage(AttributeTargets.All)]
public class CustomAttribute : Attribute {}

ここでは、Attributeクラスを直接継承したカスタム属性CustomAttributeを宣言しています。 AttributeUsageとAttributeTargets.Allの詳細については後述します。 このようにして定義したカスタム属性を使用するには、通常の属性と同じようにメソッドやクラスの前に付与するだけです。

カスタム属性の適用
using System;

[AttributeUsage(AttributeTargets.All)]
public class CustomAttribute : Attribute {}

[Custom]
class Sample {
  [CustomAttribute]
  static void Main()
  {
    Console.WriteLine("Hello, world!");
  }
}

この例にあるとおり、属性クラスの名前がAttributeで終わっている場合は、属性を適用する際に~Attributeを省略することができます。 もちろん、省略せずに完全な名前で適用することもできます。

この例のCustomAttributeでは、値や文字列などのパラメータを持っていないため属性が適用されているということ以外、何のメタデータも持っていません。 カスタム属性に具体的な意味を持たせるには、属性でパラメータを持たせるようにし、かつそのパラメータを取得できるようにする必要があります。

§2.2 カスタム属性のパラメータ

カスタム属性にパラメータを持たせるには、引数のあるコンストラクタを用意するか、パブリックなフィールドを用意するか、二種類の方法があります。 これらの違いは後述するとし、まずは引数のあるコンストラクタをもつカスタム属性クラスを作成し、それを適用してみます。

using System;

[AttributeUsage(AttributeTargets.All)]
public class CustomAttribute : Attribute {
  public CustomAttribute(int intValue, string stringValue)
  {
  }
}

[Custom(16, "test")]
class Sample {
  [Custom(42, "sample")]
  static void Main()
  {
    Console.WriteLine("Hello, world!");
  }
}

このように、カスタム属性クラスに引数のあるコンストラクタを用意することで、属性を適用する際にパラメータを渡すことができます(実際には渡されたパラメータを保持し、取得することができないと意味がないのですが、その点については後述します)。

先に書いたとおり、カスタム属性にパラメータを持たせるには二種類あり、コンストラクタの引数を用いた位置パラメータと、もう一つパブリックなフィールドを用いた名前付きパラメータです。 それぞれの違いは、

位置パラメータ
省略できないパラメータで、かつ、パラメータの順序によってその意味が変わる
名前付きパラメータ
省略が可能なパラメータで、また、パラメータの順序が変わっても意味は変わらない

となります。 上記の例で示した位置パラメータを、名前付きパラメータに置き換えると次のようになります。

using System;

[AttributeUsage(AttributeTargets.All)]
public class CustomAttribute : Attribute {
  public int IntParam;
  public string StringParam;
}

[Custom(StringParam = "test", IntParam = 16)]
class Sample {
  [Custom(StringParam = "sample")]
  static void Main()
  {
    Console.WriteLine("Hello, world!");
  }
}

このように、名前付きパラメータでは、好きな順で指定することができ、必要なパラメータのみを指定することが出来ます。 もちろん、通常のクラスと同様に複数のコンストラクタを用意することもフィールドに初期値を設定することもでき、また位置パラメータと名前付きパラメータを同時に指定することも出来ます。 ただ、位置パラメータと名前付きパラメータを同時に指定する場合は、先に位置パラメータを指定し、後ろに名前付きパラメータを指定しなければなりません。

using System;

[AttributeUsage(AttributeTargets.All)]
public class CustomAttribute : Attribute {
  public int IntParam = 42;
  public string StringParam = "default";

  public CustomAttribute(int x)
  {
  }

  public CustomAttribute(int x, int y)
  {
  }
}

[Custom(72, IntParam = 16)]
class Sample {
  [Custom(42, 16, StringParam = "sample")]
  // [Custom(StringParam = "sample", 42, 16)] <- 名前付きパラメータを先に指定することは出来ない
  static void Main()
  {
    Console.WriteLine("Hello, world!");
  }
}

§3 属性の取得

属性を適用できる対象には型・メソッド・アセンブリなどがありますが、そこから属性を取得するする方法は統一されています。 具体的には、TypeクラスMethodInfoクラスAssemblyクラスなどのICustomAttributeProviderインターフェイスを実装したクラスを使って、GetCustomAttributes()メソッドを呼びします。

§3.1 型からの属性の取得

型から属性を取得するには、まず属性を取得したい型のTypeインスタンスを取得し、そこから取得したい属性のTypeを指定してGetCustomAttributes()メソッドを呼びします。

using System;

[AttributeUsage(AttributeTargets.All)]
public class CustomAttribute : Attribute {
  public int Param = 0;
}

[Custom(Param = 16)]
class TestClass {}

class Sample {
  static void Main()
  {
    Type t = typeof(TestClass);
    object[] attrs = t.GetCustomAttributes(typeof(CustomAttribute), false);

    if (0 < attrs.Length) {
      CustomAttribute c = attrs[0] as CustomAttribute;

      if (c != null) Console.WriteLine("Param = {0}", c.Param);
    }
  }
}

GetCustomAttributes()メソッドはobjectの配列を返すので、まずは返された配列の長さをチェックします。 0より大きい場合は、実際に取得したい属性の型にキャストし、キャストできたら属性のパラメータの値を取得するという手順をとります。 これによって、型に適用された属性のパラメータの値を取得することが出来ます。

実行結果
Param = 16

なお、属性が適用されているかどうかだけを判断できればいい場合は、次の例のようにIsDefined()メソッドを使用することが出来ます。

using System;

[AttributeUsage(AttributeTargets.All)]
public class CustomAttribute : Attribute {}

// 属性を適用したクラス
[Custom]
class Class1 {}

// 属性を適用していないクラス
class Class2 {}

class Sample {
  static void Main()
  {
    Console.WriteLine("Class1: {0}", typeof(Class1).IsDefined(typeof(CustomAttribute), false));
    Console.WriteLine("Class2: {0}", typeof(Class2).IsDefined(typeof(CustomAttribute), false));
  }
}
実行結果
Class1: True
Class2: False

また、属性の型に関わらずすべての属性を取得する場合は、属性のTypeを指定せずにGetCustomAttributes()メソッドを呼びします。

using System;

[AttributeUsage(AttributeTargets.All)]
public class FooAttribute : Attribute {}

[AttributeUsage(AttributeTargets.All)]
public class BarAttribute : Attribute {}

[AttributeUsage(AttributeTargets.All)]
public class BazAttribute : Attribute {}

[Foo, Bar, Baz]
class TestClass {}

class Sample {
  static void Main()
  {
    Type t = typeof(TestClass);

    foreach (object attr in t.GetCustomAttributes(false)) {
      Console.WriteLine(attr.GetType().Name);
    }
  }
}
実行結果
BazAttribute
BarAttribute
FooAttribute

§3.2 メソッドからの属性の取得

メソッドから属性を取得する場合は、対象となるメソッドのMethodInfoを取得しておく必要がありますが、それ以外は型から属性を取得する場合と何ら変わりありません。

using System;
using System.Reflection;

[AttributeUsage(AttributeTargets.All)]
public class CustomAttribute : Attribute {
  public int Param = 0;
}

class TestClass {
  [Custom(Param = 72)]
  public void TestMethod()
  {
  }
}

class Sample {
  static void Main()
  {
    Type t = typeof(TestClass);
    MethodInfo m = t.GetMethod("TestMethod");
    object[] attrs = m.GetCustomAttributes(typeof(CustomAttribute), false);

    if (0 < attrs.Length) {
      CustomAttribute c = attrs[0] as CustomAttribute;

      if (c != null) Console.WriteLine("Param = {0}", c.Param);
    }
  }
}
実行結果
Param = 72

メソッド以外にも、コンストラクタやプロパティ、イベント、フィールドから属性を取得する場合も、これとほぼ同じ方法で取得することができます。

MethodInfoなどのメンバ情報の取得方法についてはリフレクション §.メンバ情報の取得 (MemberInfo)を参照してください。

§3.3 アセンブリからの属性の取得

アセンブリから属性を取得する場合も、型やメソッドなどから取得する場合とほぼ同じです。 例として、現在実行中のアセンブリから属性を取得する場合は次のようになります。

using System;
using System.Reflection;

// アセンブリに属性を適用
[assembly: Custom(Param = 16)]

[AttributeUsage(AttributeTargets.All)]
public class CustomAttribute : Attribute {
  public int Param = 0;
}

class Sample {
  static void Main()
  {
    // 現在実行しているアセンブリを取得
    Assembly a = Assembly.GetExecutingAssembly();
    object[] attrs = a.GetCustomAttributes(typeof(CustomAttribute), false);

    if (0 < attrs.Length) {
      CustomAttribute c = attrs[0] as CustomAttribute;

      if (c != null) Console.WriteLine("Param = {0}", c.Param);
    }
  }
}
実行結果
Param = 16

アセンブリに属性を適用する場合はコードの最初に記述する必要があるため、この例では属性クラスの宣言より前で属性の適用を行っています。 通常、アセンブリへの属性の適用はアセンブリ情報ファイル(AssebmlyInfo.csやAssebmlyInfo.vb)に記述します。

この例では現在実行中のアセンブリから属性を取得していますが、ロード済みの他のアセンブリから取得する場合なども同じ方法で取得できます。

§4 属性の指定対象 (AttributeUsage属性)

AttributeUsage属性は属性の指定対象などを指定する属性です。 カスタム属性にこの属性を適用することで、カスタム属性の指定対象、継承時の動作などを定義することが出来ます。

§4.1 属性の指定対象 (ValidOn, AttributeTargets)

AttributeUsage属性には省略できない位置パラメータとして、ValidOnプロパティがあります。 このパラメータは、属性の指定対象をAttributeTargets列挙型で指定します。 例えば、クラスのみに適用する属性、メソッドとプロパティのみに適用する属性、といった指定をするには、このパラメータで指定します。 ここまでの例ではAttributeTargets.Allを指定していましたが、これはどこにでも適用できる属性となります。

次の例では、クラス型のみに適用できる属性ClassTypeAttributeと、メソッドとプロパティのみに適用できるMethodOrPropertyAttributeを宣言し、さまざまな箇所にしています。

AttributeTargetsによって属性の指定対象が設定された属性を適用する
using System;

// クラス型のみに適用できる属性
[AttributeUsage(AttributeTargets.Class)]
public class ClassTypeAttribute : Attribute {}

// メソッドとプロパティのみに適用できる属性
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property)]
public class MethodOrPropertyAttribute : Attribute {}

[ClassType] // <- クラス型に属性を適用
class TestClass {
  // [MethodOrProperty] <-AttributeTargets.Method or AttributeTargets.Propertyなのでコンストラクタには適用できない
  TestClass()
  {
  }

  [MethodOrProperty] // <- メソッドに属性を適用
  void TestMethod()
  {
  }
}

// [ClassType] <- AttributeTargets.Classなので構造体には適用できない
struct TestStructure {
  [MethodOrProperty] // <- プロパティに属性を適用
  int TestProperty {
    get {
      return 0;
    }
  }
}

属性の適用対象はコンパイル時にチェックされます。 コメントアウトしてある箇所を有効にすると、コンパイルエラーが発生するようになります。

ValidOnプロパティには、次の値を組み合わせて指定することができます。

ValidOnプロパティに指定できる値と属性の対象
対象
AttributeTargets.Class クラス型
AttributeTargets.Struct 構造体型
AttributeTargets.Interface インターフェイス型
AttributeTargets.Enum 列挙型
AttributeTargets.Delegate デリゲート型
AttributeTargets.GenericParameter ジェネリック型の型パラメータ
型のメンバ
AttributeTargets.Method メソッド
AttributeTargets.Property プロパティ
AttributeTargets.Constructor コンストラクタ
AttributeTargets.Field フィールド
AttributeTargets.Event イベント
AttributeTargets.Parameter メソッド・コンストラクタ等の引数
AttributeTargets.ReturnValue メソッド・プロパティ等の戻り値
その他
AttributeTargets.Assembly アセンブリ
AttributeTargets.Module モジュール (VB.NETのモジュールではなくアセンブリ内のモジュール)
AttributeTargets.All すべて (上記のうち任意の要素、ValidOnプロパティのデフォルト値)

§4.2 複数指定の許可 (AllowMultiple)

AttributeUsage属性のAllowMultipleフィールドは省略可能な名前付きパラメータで、属性を2つ以上適用できるかどうかを指定します。 指定しなかった場合のデフォルトはfalse(1つしか適用できない)です。

次の例では、OriginalAuthorAttributeは1つのみ適用できる属性、ModifierAttributeは複数個適用できる属性として宣言しています。

using System;
using System.Reflection;

// 1つだけ指定できる属性
[AttributeUsage(AttributeTargets.Method)]
public class OriginalAuthorAttribute : Attribute {
  public string Name {
    get; private set;
  }

  public OriginalAuthorAttribute(string name)
  {
    Name = name;
  }
}

// 複数個指定できる属性
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class ModifierAttribute : Attribute {
  public string Name {
    get; private set;
  }

  public ModifierAttribute(string name)
  {
    Name = name;
  }
}

class TestClass {
  [OriginalAuthor("Alice")]
  // [OriginalAuthor("Eve")] <- AllowMultiple = falseなので複数個指定できない
  [Modifier("Bob")]
  [Modifier("Carol")]
  [Modifier("Dave"), Modifier("Eve")]
  public void TestMethod()
  {
  }
}

class Sample {
  static void Main()
  {
    MethodInfo m = typeof(TestClass).GetMethod("TestMethod");

    foreach (object attr in m.GetCustomAttributes(false)) {
      if (attr is OriginalAuthorAttribute) {
        Console.WriteLine("Original Author: {0}", (attr as OriginalAuthorAttribute).Name);
      }
      else if (attr is ModifierAttribute) {
        Console.WriteLine("Modifier: {0}", (attr as ModifierAttribute).Name);
      }
    }
  }
}
実行結果
Modifier: Eve
Modifier: Dave
Modifier: Carol
Modifier: Bob
Original Author: Alice

ValidOn同様、属性の適用回数はコンパイル時にチェックされます。 コメントアウトしてある部分を有効にしてOriginalAuthorAttributeを複数個適用しようとすると、コンパイルエラーとなります。

上記の例にあるように、属性を複数個指定する場合は、個別に括弧でくくる方法と、一つの各個の中に複数の属性を指定する方法の二つがあります。 意味は同じなので、読みやすさなどを考慮し、より適切な記述を選ぶことができます。 また、属性の取得方法は、一つの場合でも複数個の場合でも変わりませんが、実行結果からも分かるように、属性の指定順と取得した結果は必ずしも一致するとは限りません。

§4.3 属性の継承 (Inherited)

AttributeUsage属性のInheritedフィールドは省略可能な名前付きパラメータで、クラスを継承した場合に、基底クラスに適用されている属性を継承するかどうかを指定します。 指定しなかった場合のデフォルトはtrue(継承する)です。

このパラメータはGetCustomAttributes()メソッド等で属性を取得しようとした場合の動作に影響します。 GetCustomAttributes()等のメソッドでは、引数に継承した属性も含むかどうかを指定することができ、この引数の値と属性のInheritedの設定により、戻り値に含まれるかどうかが変わってきます。

次の例では、継承される属性InheritedAttributeと継承されない属性NotInheritedAttributeを宣言し、基底クラスBaseClassに適用しています。 BaseClassと、BaseClassを継承したDerivedClassのそれぞれの型に対してGetCustomAttributes()メソッドを呼び出し、結果の違いを見てみます。

using System;

// 継承される属性
[AttributeUsage(AttributeTargets.Class, Inherited = true)]
public class InheritedAttribute : Attribute {}

// 継承されない属性
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class NotInheritedAttribute : Attribute {}

// 基底クラス
[Inherited, NotInherited]
class BaseClass {}

[AttributeUsage(AttributeTargets.Class)]
public class CustomAttribute : Attribute {}

// 派生クラス
[Custom]
class DerivedClass : BaseClass {}

class Sample {
  static void Main()
  {
    // 基底クラスのすべての属性を取得
    Console.WriteLine("BaseClass");

    foreach (object attr in typeof(BaseClass).GetCustomAttributes(false)) {
      Console.WriteLine("  {0}", attr.GetType().Name);
    }

    // 派生クラスのすべての属性を取得 (継承した属性は含めない)
    Console.WriteLine("DerivedClass (inherited = false)");

    foreach (object attr in typeof(DerivedClass).GetCustomAttributes(false)) {
      Console.WriteLine("  {0}", attr.GetType().Name);
    }

    // 派生クラスのすべての属性を取得 (継承した属性も含める)
    Console.WriteLine("DerivedClass (inherited = true)");

    foreach (object attr in typeof(DerivedClass).GetCustomAttributes(true)) {
      Console.WriteLine("  {0}", attr.GetType().Name);
    }
  }
}
実行結果
BaseClass
  NotInheritedAttribute
  InheritedAttribute
DerivedClass (inherited = false)
  CustomAttribute
DerivedClass (inherited = true)
  CustomAttribute
  InheritedAttribute

BaseClassでは適用した二つの属性が取得できるのに対し、DerivedClassではGetCustomAttributes()メソッドの引数inheritedによって結果が異なります。 引数inheritedがfalseの場合は、その型で適用されている属性のみが取得されるのに対し、trueの場合はその型で適用されている属性と継承された属性の両方が取得されます。

§4.3.1 InheritedとAllowMultiple

AllowMultipleがfalseの場合、基底クラスでInheritedがtrueの属性を適用し、かつ派生クラスでも同じ属性を適用すると、基底クラスの属性が上書きされます。 AllowMultipleがtrueの場合は上書きされず、複数個指定した場合と同じように扱われます。

using System;

// 継承される属性
[AttributeUsage(AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
public class InheritedAttribute : Attribute {
  public string Value;

  public override string ToString()
  {
    return Value;
  }
}

// 継承されない属性
[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
public class NotInheritedAttribute : Attribute {
  public string Value;

  public override string ToString()
  {
    return Value;
  }
}

// 基底クラス
[Inherited(Value = "Base-Inherited"), NotInherited(Value = "Base-NotInherited")]
class BaseClass {}

// 派生クラス
[Inherited(Value = "Derived-Inherited"), NotInherited(Value = "Derived-NotInherited")]
class DerivedClass : BaseClass {}

class Sample {
  static void Main()
  {
    // 基底クラスのすべての属性を取得
    Console.WriteLine("BaseClass");

    foreach (object attr in typeof(BaseClass).GetCustomAttributes(false)) {
      Console.WriteLine("  {0}", attr.ToString());
    }

    // 派生クラスのすべての属性を取得 (継承した属性は含めない)
    Console.WriteLine("DerivedClass (inherited = false)");

    foreach (object attr in typeof(DerivedClass).GetCustomAttributes(false)) {
      Console.WriteLine("  {0}", attr.ToString());
    }

    // 派生クラスのすべての属性を取得 (継承した属性も含める)
    Console.WriteLine("DerivedClass (inherited = true)");

    foreach (object attr in typeof(DerivedClass).GetCustomAttributes(true)) {
      Console.WriteLine("  {0}", attr.ToString());
    }
  }
}
実行結果
BaseClass
  Base-NotInherited
  Base-Inherited
DerivedClass (inherited = false)
  Derived-NotInherited
  Derived-Inherited
DerivedClass (inherited = true)
  Derived-NotInherited
  Derived-Inherited
  Base-Inherited