属性

目次

キーワード

概要

属性(attribute)とはクラスやメンバに追加情報を与えるものです。 例えば、publicprivate などといったC#のキーワードもある種の属性と考えることが出来ます。 public ならば「このメンバはクラス外からも参照可能」、 private ならば「このメンバはクラス内のみから参照可能」という追加情報が与えられます。

C++ などの既存の言語では、このような追加情報を定義する場合、 言語仕様自体を拡張し、新たにコンパイラを作り直す必要がありました。 それに対し、C# では自分で属性を定義し、クラスやメンバに付加することが出来ます。 すなわち、ライブラリで提供されている属性や自作した属性を用いることで、 コンバイラに対する指示を行ったり、クラスの利用者に対する情報を残すことが出来ます。

属性の使用

属性は以下のように [] でくくり、 クラスやメンバの前に付けて使います。

[属性名(属性パラメータ)]
メンバの定義

属性名は語尾に Attribute を付けることになっています。 例えば、標準で用意されている属性には ObsoleteAttributeConditionalAttribute などといった名前のものがあります。 また、これらを C# から利用する場合、語尾の Attribute は省略してもかまいません。 したがって、前者は Obsolete、 後者は Conditional という名前で使用できます。

例として、Conditional 属性を使用してみましょう。 Conditional 属性とは、 特定の条件下でのみ実行されるメソッドを定義するために使用する属性です。 例えば、以下のようにして使用します。

using System;
using System.Diagnostics;

class AttributeTest
{
  static void Main()
  {
    double[] array = new double[]{9, 4, 5, 2, 7, 1, 6, 3, 8};
    BubbleSort(array);
    Output(array);
  }

  /// <summary>
  /// バブルソートを行う。
  /// </summary>
  static void BubbleSort(double[] array)
  {
    int n = array.Length - 1;

    for(int i=0; i<n; ++i)
    {
      for(int j=n; j>i; --j)
        if(array[j-1] > array[j])
          Swap(ref array[j-1], ref array[j]);

      IntermediateOutput(array); // ソートの途中段階のデータを表示。
    }
  }

  static void Swap(ref double x, ref double y)
  {
    double tmp = x;
    x = y;
    y = tmp;
  }

  /// <summary>
  /// 配列の内容をコンソールに表示する。
  /// </summary>
  static void Output(double[] array)
  {
    foreach(double x in array)
    {
      Console.Write("{0} ", x);
    }
    Console.Write("\n");
  }

  /// <summary>
  /// SHOW_INTERMEDIATE というシンボルが定義されているときのみ
  /// 配列の内容をコンソールに表示する。
  /// </summary>
  [Conditional("SHOW_INTERMEDIATE")]
  static void IntermediateOutput(double[] array)
  {
    Output(array);
  }
}

SHOW_INTERMEDIATE という名前のシンボルが定義されている場合、 以下のように、ソートの途中段階のデータが表示されます。

1 9 4 5 2 7 3 6 8
1 2 9 4 5 3 7 6 8
1 2 3 9 4 5 6 7 8
1 2 3 4 9 5 6 7 8
1 2 3 4 5 9 6 7 8
1 2 3 4 5 6 9 7 8
1 2 3 4 5 6 7 9 8
1 2 3 4 5 6 7 8 9
1 2 3 4 5 6 7 8 9

一方、SHOW_INTERMEDIATE という名前のシンボルが定義されていない場合、 以下のように、結果のみが表示されます。

1 2 3 4 5 6 7 8 9

ちなみに、以下のように , で区切るか、複数の [] を並べることで複数の属性を指定することが出来ます。

[Conditional("DEBUG"), Conditional("TEST")]
void DebugOutput(string message)
[Conditional("DEBUG")]
[Conditional("TEST")]
void DebugOutput(string message)

定義済み属性

Conditional 以外にも、標準ライブラリによって提供されている定義済み属性がいくつかあります。 そのうちのいくつかを以下に挙げます。

属性名効果
System.AttributeUsageAttribute属性の用途を指定します。属性クラスを自作する場合(詳細は後述)に使用します。
System.ObsoleteAttribute時代遅れな(次期バージョンで削除される可能性のある)コードであることを示します。
System.SerializableAttributeオブジェクトがシリアル化(アプリケーションが終了してもオブジェクトの状態が残るように、ファイルなどにデータを書き出すこと)可能であることを示します。
System.Diagnostics.ConditionalAttribute 特定の条件下でのみ実行されるメソッドを定義するために使用します。 Ver. 2.0 C# 2.0 では、メソッドだけでなく、属性に対しても Conditional 属性を付ける事が可能になりました。
System.ComponentModel.CategoryAttribute
System.ComponentModel.DefaultValueAttribute
System.ComponentModel.DescriptionAttribute
System.ComponentModel.BrowsableAttribute
コンポーネントクラス(簡単に言うと Windows アプリケーションのボタンやテキストボックス等のこと)のプロパティに対してこれらの属性を指定することで、Visual Studio .net のプロパティエディタで値を編集することが出来るようになります。
System.Runtime.InteropServices.DllImportAttribute
System.Runtime.InteropServices.ComImportAttribute
Unmanaged な DLL や COM からメソッドやクラスをインポートします。
System.Web.Services.WebMethodAttributeXML Web Service を使用してリモートにあるメソッドを呼び出すことが出来ます。

属性の対象

属性を付ける場所によって属性の対象は変わります。 例えば、クラスの直前に属性を付ければクラスに属性が適用されますし、 メソッド定義の直前に属性を付ければメソッドに属性が適用されます。 以下にその例を挙げます。

[assembly: AssemblyTitle("Test Attribute")] // プログラムそのものが対象

[Serializable] // クラスが対象
public class SampleClass
{
  [Obsolete("時期版で削除します。使わないでください。")] // メソッドが対象
  public void Test([In, Out] ref int n) // 引数が対象
  {
    n *= 2;
  }
}

しかし、属性を付ける位置によっては属性の対象が曖昧になることがあります。 メソッドそのものとメソッドの戻り値に属性を適用したい場合がその典型例です。 以下にその例を挙げます。

[DllImport("msvcrt.dll")] 
[MarshalAs(UnmanagedType.I4)] // メソッドの戻り値に属性を適用したいんだけど、
                              // コンパイラはそう解釈してくれない。
public static extern int puts(
  [MarshalAs(UnmanagedType.LPStr)]
  string m);

このような曖昧さを解決するため、 明示的に属性の対象を指定する構文があります。

[属性の対象 : 属性名(属性のオプション)]

先ほどの例を属性の対象を明示的に指定して書き直すと以下のようになります。

[method : DllImport("msvcrt.dll")] 
[return : MarshalAs(UnmanagedType.I4)]
public static extern int puts(
  [param : MarshalAs(UnmanagedType.LPStr)]
  string m);

属性の対象には以下のようなものがあります。

対象名説明
assemblyアセンブリ(簡単に言うと、プログラムの実行に必要なファイルをひとまとめにした物のこと)が対象になります。
moduleモジュール(1つの実行ファイルやDLLファイルのこと)が対象になります。
typeクラスや構造体、列挙型やデリゲート(後述)等の型が対象になります。
fieldフィールド(要するにメンバ変数のこと)が対象になります。
methodメソッドが対象になります。
eventイベント(後述)が対象になります。
propertyプロパティが対象になります。
paramメソッドの引数が対象になります。
returnメソッドの戻り値が対象になります。

このうち、return は先ほど説明したとおり、 メソッドそのものに対する属性と区別するために必ず付ける必要があります。 また、assembly および module は省略した際の規定値がないため、省略することが出来ません。

属性の自作

属性の実態は System.Attribute クラスの派生クラスですSystem.Attribute クラスを継承したクラスを作成することで、 新しい属性を自作することが出来ます。

ここでは例として、クラスの作者を記録しておくための属性 Author を作成します。 まずは最も基本的な部分を作成します。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class AuthorAttribute : Attribute
{
  private string name;       // 作者名
  public string affiliation; // 作者所属
  public AuthorAttribute(string name){this.name = name;}
}

見てのとおり、何の変哲もないクラスです。 ただ、System.Attribute を継承していて、 AttributeUsage 属性が付いています。 AttributeUsage により、その属性の用途を指定することが出来ます。 この例の場合、Author 属性は対象が指定されていて、 クラスまたは構造体にのみ適用できる属性になります。

次に使用する側の例を挙げます。

[Author("Andrei Hejilsberg")]
class Test
{
  // 中身は省略
}

属性パラメータで指定した引数は属性クラスのコンストラクタに渡されます。 したがって、この例の場合、AuthorAttribute クラスのコンストラクタに文字列 "Andrei Hejilsberg" が渡されます。 その結果生成された AuthorAttribute クラスのインスタンス情報がこのクラスのメタデータとして残されます。

また、属性クラスの public なフィールドやプロパティは名前付きパラメータと呼ばれる方法で設定することが出来ます。 例として、先ほど作成した Author 属性の affiliation フィールドを設定してみましょう。

[Author("Andrei Hejilsberg", affiliation="Microsoft")]
class Test
{
  // 中身は省略
}

この例の affiliation="Microsoft" の部分が名前付きパラメータです。 こように、通常の属性パラメータの後ろに , で区切って「フィールド名 = 値」と書くことでフィールドの値を設定できます。 (プロパティの場合もまったく同様にして値を設定できます。)

Attribute にも AllowMultipleInherited という2つの名前付きパラメータがあります。

[AttributeUsage(
   属性の対象,
   AllowMultiple=複数回適用の可否,
   Inherited=継承の有無
)]

AllowMultiple には同じ属性を同じ対象に複数回適用できるかどうかを指定します。 true の場合は適用可能、false の場合は適用不可になります。 Inherited には属性が継承されるかどうかを指定します。 true の場合はクラスの継承時に属性も一緒に継承され、 false の場合には属性は継承されません。

先ほどの Author 属性の場合、 1つのクラスを複数人で開発することもありえますし、 AllowMultiple は true にすべきでしょう。 また、派生クラスと基底クラスの作者が同じとは限りませんから、 Inherited は false とすべきです。 以上のことを踏まえ、Author 属性を書き直すと以下のようになります。

[AttributeUsage(
  AttributeTargets.Class | AttributeTargets.Struct,
  AllowMultiple = true,
  Inherited = false)]
public class AuthorAttribute : Attribute
{
  private string name;       // 作者名
  public string affiliation; // 作者所属
  public AuthorAttribute(string name){this.name = name;}
}

属性情報の取得

リフレクション機能を用いて属性情報を出得することが出来ます。 具体的には、 Attribule クラスの GetCustomAttribute メソッドや GetCustomAttributes メソッドを用いて属性を取得します。 取得したい属性の AllowMultiple パラメータが false の場合は GetCustomAttribute メソッドを、 AllowMultiple パラメータが true の場合や、 全ての属性を取得したい場合には GetCustomAttributes メソッドを使用します。

例として、クラス及びそのクラス中の public メソッドに適用された全ての Author 属性を取得するプログラムを以下に示します。

using System;
using System.Reflection;

/// <summary>
/// 作者情報を残すための属性。
/// </summary>
[AttributeUsage(
   AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method,
   AllowMultiple = true,
   Inherited = false)]
public class AuthorAttribute : Attribute
{
  private string name;
  public AuthorAttribute(string name){this.name = name;}
  public string Name{get{return this.name;}}
}

/// <summary>
/// テスト用のクラス。
/// メソッドごとに違う人が開発するなんてほとんどありえないけど、
/// その辺は目をつぶってください。
/// </summary>
[Author("Stephanie McMahon")]
[Author("Hunter Herst Helmsly")]
class AuthorTest
{
  [Author("Kurt Angle")]    public static void A(){}
  [Author("Rocky Mavia")]   public static void B(){}
  [Author("Chris Jericho")] public static void C(){}
  [Author("Glen Jacobs")]   public static void D(){}
}

/// <summary>
/// テストプログラム。
/// </summary>
class AttributeTest
{
  public static void Main()
  {
    GetAllAuthors(typeof(AuthorTest));
  }

  /// <summary>
  /// クラス自体とクラス中の public メソッドの作者情報を取得する。
  /// </summary>
  /// <param name="t">クラスの Type</param>
  static void GetAllAuthors(Type t)
  {
    Console.Write("type name: {0}\n", t.Name);
    GetAuthors(t);

    foreach(MethodInfo info in t.GetMethods())
    {
      Console.Write("  method name: {0}\n", info.Name);
      GetAuthors(info);
    }
  }

  /// <summary>
  /// クラスやメソッドの作者情報を取得する。
  /// </summary>
  /// <param name="info">クラスやメソッドの MemberInfo</param>
  static void GetAuthors(MemberInfo info)
  {
    Attribute[] authors = Attribute.GetCustomAttributes(info, typeof(AuthorAttribute));
    foreach(Attribute att in authors)
    {
      AuthorAttribute author = att as AuthorAttribute;
      if(author != null)
      {
        Console.Write("    author name: {0}\n", author.Name);
      }
    }
  }
}
type name: AuthorTest
    author name: Hunter Herst Helmsly
    author name: Stephanie McMahon
  method name: GetHashCode
  method name: Equals
  method name: ToString
  method name: A
    author name: Kurt Angle
  method name: B
    author name: Rocky Mavia
  method name: C
    author name: Chris Jericho
  method name: D
    author name: Glen Jacobs
  method name: GetType

ちなみに、一部の属性は、実行ファイルのプロパティに表示されます。 例えば、以下のようなプログラムにより、 AssemblyDescription という属性をアセンブリに付けたとします。

using System.Reflection;
using System.Runtime.CompilerServices;

[assembly: AssemblyDescription("assembly 属性のサンプルコードです。")]

class TestAttribute
{
  public static void Main()
  {
  }
}

AssemblyDescription に与えた文字列は、このプログラムのコメントとして、 explorer から参照することができます。 このソースコードをコンパイルした結果の実行ファイルのプロパティを開くと、 以下のようになります。

実行ファイルのプロパティ

図1: 実行ファイルのプロパティ