属性(attribute)とはクラスやメンバーに追加情報を与えるものです。
例えば、public や private などといったC#のキーワードもある種の属性と考えることが出来ます。
public ならば「このメンバーはクラス外からも参照可能」、
private ならば「このメンバーはクラス内のみから参照可能」という追加情報が与えられます。
C++ などの既存の言語では、このような追加情報を定義する場合、 言語仕様自体を拡張し、新たにコンパイラを作り直す必要がありました。 それに対し、C# では自分で属性を定義し、クラスやメンバーに付加することが出来ます。 すなわち、ライブラリで提供されている属性や自作した属性を用いることで、 コンバイラに対する指示を行ったり、クラスの利用者に対する情報を残すことが出来ます。
属性の情報は、以下のような場面で使われます。
属性は以下のように [] でくくり、
クラスやメンバーの前に付けて使います。
[属性名(属性パラメータ)] メンバーの定義
たとえば以下のような感じ。
[DataContract] class User { public int Id { get; set; } public string Name { get; set; } }
属性名は語尾に Attribute を付けることになっています。
例えば、標準で用意されている属性には ObsoleteAttribute や
ConditionalAttribute などといった名前のものがあります。
また、これらを 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.Diagnostics.ConditionalAttribute
| 特定の条件下でのみ実行されるメソッドを定義するために使用します。 Ver. 2.0 C# 2.0 では、メソッドだけでなく、属性に対しても Conditional 属性を付ける事が可能になりました。 |
Visual Studio などの開発ツールが利用します。
いずれも、System.ComponentModel 名前空間です。
| 属性名 | 効果 |
|---|---|
CategoryAttribute
DefaultValueAttribute
DescriptionAttribute
BrowsableAttribute
| コンポーネントクラス(簡単に言うと Windows アプリケーションのボタンやテキストボックス等のこと)のプロパティに対してこれらの属性を指定することで、 Visual Studio のプロパティ エディタで値を編集することが出来るようになります。 |
.NET Framework の IL 実行エンジンが利用します。
いずれも、System.Runtime.InteropServices 名前空間です。
| 属性名 | 効果 |
|---|---|
DllImportAttribute
| ネイティブ な DLL からメソッドをインポートします。 (DllImport 属性を付けたメソッドを宣言するだけで、ネイティブ な DLL のメソッドを利用できます。 ネイティブ DLL 側に特別な処理を書く必要は全くありません。) |
ComImportAttribute
| Unmanaged な DLL から COM クラスをインポートします。 |
ライブラリが利用します。 各ライブラリ内部で、リフレクションを使った動的コード生成などを行っています。
| 属性名 | 効果 |
|---|---|
System.Web.Services.WebMethodAttribute
| XML Web Service を使用してリモートにあるメソッドを呼び出すことが出来ます。 |
| 属性名 | 効果 |
|---|---|
System.ServiceModel.OperationContractAttribute
System.ServiceModel.ServiceContractAttribute
System.Runtime.Serialization.DataContractAttribute
| WCF のサービスや、サービスで使うデータに付けます。 |
System.ComponentModel.DataAnnotations.Validator クラスを使って、
データが満たすべき条件(null であってはいけないとか、値の範囲とか)を検証します。
いずれも、System.ComponentModel.DataAnnotations 名前空間です。
| 属性名 | 効果 |
|---|---|
RequiredAttribute
| 必須である(null や空文字を認めない)ことを示します。 |
RangeAttribute
| 値の範囲を指定します。 |
StringLengthAttribute
| 文字列の最大長/最小長を指定します。 |
Visual Studio 組み込みの単体テスト機能で利用します。
いずれも、Microsoft.VisualStudio.TestTools.UnitTesting 名前空間です。
| 属性名 | 効果 |
|---|---|
TestClassAttribute
| テスト メソッドを含むクラスを識別するために使用されます。 |
TestMethodAttribute
| テスト メソッドの識別に使用します。 |
一部の属性は、実行ファイルのプロパティに表示されます。 例えば、以下のようなプログラムにより、 AssemblyDescription という属性をアセンブリに付けたとします。
using System.Reflection; using System.Runtime.CompilerServices; [assembly: AssemblyDescription("assembly 属性のサンプルコードです。")] class TestAttribute { static void Main() { } }
AssemblyDescription に与えた文字列は、このプログラムのコメントとして、 explorer から参照することができます。 このソースコードをコンパイルした結果の実行ファイルのプロパティを開くと、 以下のようになります。
属性を付ける場所によって属性の対象は変わります。 例えば、クラスの直前に属性を付ければクラスに属性が適用されますし、 メソッド定義の直前に属性を付ければメソッドに属性が適用されます。 以下にその例を挙げます。
[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 にも AllowMultiple と Inherited という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 { 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