C# でカスタム属性を自作する
.NET には多くの組み込み属性が用意されているが、プロジェクト固有の要件に合った属性が欲しくなることもある。C# では System.Attribute を継承したクラスを定義するだけで、独自の属性を作れる。
最小のカスタム属性
カスタム属性は Attribute クラスを継承して作る。クラス名の末尾は慣例として Attribute を付ける。
using System;
public class AuthorAttribute : Attribute
{
public string Name { get; }
public AuthorAttribute(string name)
{
Name = name;
}
}使うときは末尾の Attribute を省略できる。
[Author("田中太郎")]
public class ReportGenerator
{
public void Generate()
{
Console.WriteLine("レポート生成中...");
}
}これでクラスに「誰が作ったか」というメタデータを付与できた。属性自体はプログラムの動作を変えないが、リフレクションで読み取ることで実行時に活用できる。
AttributeUsage で適用先を制限する
デフォルトではカスタム属性はあらゆる要素に付けられるが、AttributeUsage 属性を使えば適用先を限定できる。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorAttribute : Attribute
{
public string Name { get; }
public AuthorAttribute(string name)
{
Name = name;
}
}この定義では Author 属性はクラスとメソッドにしか付けられない。フィールドやプロパティに付けるとコンパイルエラーになる。
クラスに適用可能にする。
メソッドに適用可能にする。
プロパティに適用可能にする。
すべての要素に適用可能にする(デフォルト)。
複数の対象を許可したい場合はビット OR 演算子 | で組み合わせる。適用先を明確にしておくと、意図しない場所に属性が付けられるミスを防げる。
複数回の適用を許可する
デフォルトでは同じ属性を 1 つの要素に複数回付けられない。AllowMultiple = true を指定すると、この制限が解除される。
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class AuthorAttribute : Attribute
{
public string Name { get; }
public AuthorAttribute(string name)
{
Name = name;
}
}
[Author("田中太郎")]
[Author("鈴木花子")]
public class CollaborativeReport
{
// 共同作成したクラス
}共同執筆のような場面では AllowMultiple = true が自然な選択になる。
名前付きプロパティを追加する
コンストラクタ引数だけでなく、省略可能な名前付きプロパティも定義できる。
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class AuthorAttribute : Attribute
{
public string Name { get; }
public string Email { get; set; } = "";
public string Version { get; set; } = "1.0";
public AuthorAttribute(string name)
{
Name = name;
}
}コンストラクタ引数は必須パラメータ、公開プロパティは省略可能なパラメータとして機能する。
[Author("田中太郎", Email = "tanaka@example.com", Version = "2.1")]
public class UserService
{
[Author("鈴木花子")]
public void CreateUser() { }
}必須パラメータとして扱われる。属性を付ける際に必ず指定しなければならない。
省略可能なパラメータとして扱われる。指定しなければデフォルト値が使われる。
リフレクションで属性を読み取る
付与した属性はリフレクションで取得できる。GetCustomAttributes メソッドや GetCustomAttribute<T> メソッドを使う。
using System;
using System.Reflection;
var type = typeof(UserService);
var attrs = type.GetCustomAttributes<AuthorAttribute>();
foreach (var attr in attrs)
{
Console.WriteLine($"作成者: {attr.Name}");
if (!string.IsNullOrEmpty(attr.Email))
{
Console.WriteLine($"連絡先: {attr.Email}");
}
}メソッドに付けた属性も同様に取得できる。
var method = typeof(UserService).GetMethod("CreateUser");
var methodAttr = method?.GetCustomAttribute<AuthorAttribute>();
if (methodAttr != null)
{
Console.WriteLine($"メソッド作成者: {methodAttr.Name}");
}実用的なカスタム属性の例
バリデーション用のカスタム属性を作ってみよう。プロパティに付けて、値の範囲を検証する。
[AttributeUsage(AttributeTargets.Property)]
public class RangeValidationAttribute : Attribute
{
public int Min { get; }
public int Max { get; }
public RangeValidationAttribute(int min, int max)
{
Min = min;
Max = max;
}
public bool IsValid(int value) => value >= Min && value <= Max;
}
public class Student
{
public string Name { get; set; } = "";
[RangeValidation(0, 100)]
public int Score { get; set; }
}リフレクションと組み合わせて汎用的なバリデーションを実装できる。
public static bool Validate(object obj)
{
var properties = obj.GetType().GetProperties();
foreach (var prop in properties)
{
var attr = prop.GetCustomAttribute<RangeValidationAttribute>();
if (attr != null && prop.PropertyType == typeof(int))
{
int value = (int)prop.GetValue(obj)!;
if (!attr.IsValid(value))
{
Console.WriteLine(
$"{prop.Name} の値 {value} は {attr.Min}〜{attr.Max} の範囲外です");
return false;
}
}
}
return true;
}カスタム属性は宣言的にメタデータを付与し、リフレクションで読み取って処理するという設計パターンの基盤になっている。ASP.NET のルーティング属性やバリデーション属性、Entity Framework のマッピング属性など、多くのフレームワークがこの仕組みの上に構築されている。自前のフレームワークや規約を作る際にも、カスタム属性は強力な武器となるだろう。