概要
属性(attribute)とはクラスやメンバーに追加情報を与えるものです。
例えば、public
や private
などといったC#のキーワードもある種の属性と考えることが出来ます。
public
ならば「このメンバーはクラス外からも参照可能」、
private
ならば「このメンバーはクラス内のみから参照可能」という追加情報が与えられます。
C++ などの既存の言語では、このような追加情報を定義する場合、 言語仕様自体を拡張し、新たにコンパイラを作り直す必要がありました。 それに対し、C# では自分で属性を定義し、クラスやメンバーに付加することが出来ます。 すなわち、ライブラリで提供されている属性や自作した属性を用いることで、 コンパイラに対する指示を行ったり、クラスの利用者に対する情報を残すことが出来ます。
属性の情報は、以下のような場面で使われます。
-
条件コンパイルなどの、コンパイラへの指示に使う(Conditional や Obsolete)。
-
作者情報などをメタデータとしてプログラムに埋め込む(AssemblyTitle など)。
-
「リフレクション」を利用して、プログラム実行時に属性情報を取り出して利用する。
ポイント
-
C# では、クラスやメンバーに対して、ユーザーが自分で定義した属性を自由に付けられます。
-
一部の属性は、コンパイラや Visual Studio に対する指示として利用します。 例:
-
[Obsolete] class OldClass {} … 古いバージョンとの互換性のためだけに残してるけど、このクラスはもう使わないで。
-
[EditorBrowsable] T Property; … Visual Studio の IntelliSense(などの、開発ツールの補完機能)で表示するかどうかを設定します。
-
属性の使用
属性は以下のように []
でくくり、
クラスやメンバーの前に付けて使います。
[属性名(属性パラメータ)]
メンバーの定義
たとえば以下のような感じ。
[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.0C# 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 クラスをインポートします。 |
ライブラリ
ライブラリが利用します。 各ライブラリ内部で、「リフレクション」を使った動的コード生成などを行っています。
ASP.NET
属性名 | 効果 |
---|---|
System.Web.Services.WebMethodAttribute
|
XML Web Service を使用してリモートにあるメソッドを呼び出すことが出来ます。 |
WCF(Windows Communication Foundation)
属性名 | 効果 |
---|---|
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
も指定が必須です。
プロパティ、イベントと属性の対象
プロパティやイベントは、 内部的にはフィールドやメソッドも作られます。 その結果、属性の指定先もいろいろと増えます。
- プロパティやイベント自身
get
/set
や、add
/remove
アクセサーに対応するメソッドset
,add
,remove
が受け取っているvalue
引数- 自動実装の場合、バック フィールドも生成されるので、そのフィールド
これらに対して、以下のような書き方で属性を付けることができます。
using System;
class XAttribute : Attribute { }
class Sample
{
[X] // プロパティ自体
public int Property
{
[method:X] // get に対応するメソッド
get => 0;
[method: X] // set に対応するメソッド
[param: X] // set が受け取っている value 引数
set { }
}
[field:X] // (C# 7.3 から) 自動で生成されるフィールド
public int AutoProperty { get; }
[X] // イベント自体
public event Action Event
{
[method: X] // add に対応するメソッド
[param: X] // add が受け取っている value 引数
add { }
[method: X] // remove に対応するメソッド
[param: X] // remove が受け取っている value 引数
remove { }
}
[field: X] // 自動で生成されるフィールド
public event Action AutoEvent;
}
Ver. 7.3
※この中で、自動プロパティから生成されるフィールドに対する属性付けは、C# 7.3からしかできません。
C# 7.2 以前では自動プロパティでフィールドに対して属性指定する方法がなく、 もし必要なら、自動実装をやめて手動でプロパティを実装しなおさなければなりませんでした。
ちなみに、この「修正」は一応、破壊的変更になります。 この問題を踏むことはほとんどないとは思いますが、以下のコードは、C# 7.2まではコンパイルできて、7.3ではコンパイルできなくなります。
using System;
// 本来フィールドには付けれない属性
[AttributeUsage(AttributeTargets.Class)]
class XAttribute : Attribute { }
class Sample
{
// C# 7.2 の挙動:
// そもそもこの field 指定が無効。
// 無効なので警告は出しているけども、エラーにはしていなかった。
// 一方で、「フィールドに付けれる属性かどうか」のチェックはしていなかった。
//
// C# 7.3 の挙動:
// field 指定が有効になったことで、チェックが働くように。
// フィールドに付けれる属性ではないのでエラーになる。
[field:X]
public int AutoProperty { get; }
}
プライマリ コンストラクター
Ver. 9
Ver. 12
C# 12 からは普通のクラスや構造体にも使えるようになったプライマリ コンストラクターという構文があります。 (ただし、レコード型では、 C# 9 で導入された当初から先行してプライマリ コンストラクターを使えました。)
// クラスの直後に () や引数リストを書ける。 class A(); class B(int x);
通常、クラスの直前に書く属性はクラス自体に対して付与されますが、
method
指定を付けることでプライマリ コンストラクターに対する属性にできます。
(ただし、レコード型が対象であっても、この機能が使えるのは C# 12 からです。)
[X] // これはクラスに対する属性。 [method: X] // これはプライマリ コンストラクターに対する属性。 class A(); class XAttribute : Attribute;
また、レコード型の場合はプライマリ コンストラクターの引数からプロパティが自動生成されることになりますが、
以下のように、property
や field
を付けることで属性の指定先を選べます。
record A( [X] // これはプライマリ コンストラクターの引数に付く。 // [param: X] 省略せずに書くならこう。 [property: X] // これは生成されるプロパティに付く。 [field: X] // これは生成されるプロパティのバッキング フィールドに付く。 int X ); class XAttribute : Attribute;
属性の自作
属性の実態は 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
ジェネリックな属性
Ver. 11
C# 11.0 で、属性をジェネリック クラスにできるようになりました。
これまでだと、以下のように引数で typeof
を使って型を渡すことになっていました。
// 属性は非ジェネリックでないとダメ。
class TypeConverter : Attribute
{
public TypeConverter(Type type) { }
}
// これまでだとこんな感じで引数で typeof を指定する。
[TypeConverter(typeof(MyConverter))]
class MyClass { }
C# 11.0 以降は以下のようにも書けます。
// ジェネリックにできるように。
class TypeConverter<T> : Attribute { }
// <> で型引数を指定できる。
[TypeConverter<MyConverter>]
class MyClass { }
ただし、型引数は具象型(仮引数が残っていない状態)でなければなりません。
// ただし、型引数は具象型出ないとダメ。
// 型仮引数を仮引数のままにはできない。
// CS8968 エラーになる。
[TypeConverter<T>]
class MyClass<T> { }