C#の開発において、クラスやメソッドに付加情報を与える「属性 (Attribute)」は非常に強力なツールです。
メタデータを活用することで、実行時の動作を動的に変更したり、フレームワークに対して特定の指示を与えたりすることが可能になります。
本記事では、C#で属性を取得するための基本的な手法から、リフレクションを用いた実践的なコード例までを詳しく解説します。
開発現場で即座に役立つテクニックを網羅しているため、プログラムの柔軟性を高めたいと考えている方はぜひ参考にしてください。
C#における属性(Attribute)の役割と重要性
属性とは、プログラム要素に対して追加の情報を付与するための宣言的なタグのことです。
クラス、メソッド、プロパティ、引数など、ほぼすべてのプログラム要素に対して属性を定義することができます。
属性自体は単なるデータですが、これをリフレクション(Reflection)によって読み取ることで、プログラムの振る舞いを制御できます。
例えば、データベースのテーブル名とクラス名を紐付けたり、APIのバリデーションルールを定義したりする際に多用されます。
モダンなC#開発において、属性を使いこなすことはライブラリ作成や共通基盤の構築において必須のスキルと言えるでしょう。
カスタム属性の定義方法
属性を取得する前に、まずは独自のカスタム属性を作成する方法を確認しておきましょう。
カスタム属性を作成するには、System.Attributeクラスを継承したクラスを定義します。
また、その属性がどこに適用できるかを制限するためにAttributeUsage属性を使用するのが一般的です。
// 属性の適用範囲をクラスとプロパティに限定する
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
public class DeveloperInfoAttribute : Attribute
{
public string Name { get; }
public string Version { get; set; }
public DeveloperInfoAttribute(string name)
{
Name = name;
}
}
このコードでは、開発者名(Name)を必須とし、バージョン(Version)を任意とする属性を定義しています。
このように、カスタム属性を定義することで独自のメタデータをプログラムに埋め込むことが可能になります。
リフレクションを使用した属性の取得手順
属性を取得するためには、対象となる型(Type)やメンバーの情報(MemberInfo)を取得する必要があります。
主に利用されるのはSystem.Reflection名前空間に含まれるメソッド群です。
属性取得の基本的な流れは、まず対象の型情報を取得し、その後に属性検索メソッドを呼び出すという形式になります。
1. クラスに付与された属性を取得する
クラス全体に対して付与された属性を取得するコードを見てみましょう。
GetCustomAttributeメソッドを使用することで、特定の型に合致する属性を1つ取得できます。
[DeveloperInfo("山田太郎", Version = "1.0")]
public class MyService
{
// クラスの中身
}
public class Program
{
public static void Main()
{
var type = typeof(MyService);
var attribute = (DeveloperInfoAttribute)Attribute.GetCustomAttribute(type, typeof(DeveloperInfoAttribute));
if (attribute != null)
{
Console.WriteLine($"開発者: {attribute.Name}");
Console.WriteLine($"バージョン: {attribute.Version}");
}
}
}
開発者: 山田太郎
バージョン: 1.0
この例では、typeof(MyService)を使用して型情報を取得し、その型に付与されているDeveloperInfoAttributeを取り出しています。
Attribute.GetCustomAttributeは、対象が見つからない場合にnullを返すため、必ずnullチェックを行うようにしましょう。
2. ジェネリック版メソッドによるスマートな取得
C#では、より型安全に属性を取得するためにジェネリック版のメソッドが用意されています。
System.Reflection.CustomAttributeExtensions名前空間にある拡張メソッドを使用するのが現在の主流です。
using System.Reflection;
var type = typeof(MyService);
var attribute = type.GetCustomAttribute<DeveloperInfoAttribute>();
if (attribute != null)
{
Console.WriteLine($"取得成功: {attribute.Name}");
}
キャストの手間が省けるだけでなく、コードの可読性が大幅に向上するため、可能な限りジェネリック版を使用することを推奨します。
プロパティやメソッドから属性を取得する方法
クラス単位ではなく、個別のプロパティやメソッドに付与された属性を取得したいケースも多いでしょう。
この場合、まずはプロパティ情報(PropertyInfo)やメソッド情報(MethodInfo)を取得する必要があります。
public class UserProfile
{
[DeveloperInfo("佐藤次郎")]
public string DisplayName { get; set; }
}
public class Program
{
public static void Main()
{
var type = typeof(UserProfile);
var property = type.GetProperty("DisplayName");
if (property != null)
{
var attr = property.GetCustomAttribute<DeveloperInfoAttribute>();
Console.WriteLine($"プロパティの担当者: {attr?.Name}");
}
}
}
GetPropertyメソッドで特定のプロパティを取得し、そのインスタンスに対してGetCustomAttributeを呼び出しています。
リフレクションを使用する際は、プロパティ名が文字列指定になるため、タイポによる実行時エラーに注意してください。
複数の属性を取得する場合(GetCustomAttributes)
同じ要素に複数の属性が付与されている場合、あるいは同じ種類の属性が複数許可されている(AllowMultiple = true)場合は、配列として取得する必要があります。
このときはGetCustomAttributesという複数形のメソッドを使用します。
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class LogTagAttribute : Attribute
{
public string Tag { get; }
public LogTagAttribute(string tag) => Tag = tag;
}
public class Logger
{
[LogTag("Network")]
[LogTag("Security")]
public void Connect() { }
}
// 取得側のコード
var method = typeof(Logger).GetMethod("Connect");
var tags = method.GetCustomAttributes<LogTagAttribute>();
foreach (var t in tags)
{
Console.WriteLine($"Tag: {t.Tag}");
}
GetCustomAttributesは、指定した属性が見つからない場合でも空の列挙を返すため、foreachループを安全に回すことができます。
継承関係にある属性の取得制御
属性の取得において重要なポイントが「継承」の扱いです。
親クラスで定義された属性を子クラスでも取得したい場合、メソッドの第2引数にinheritパラメータ(bool値)を指定します。
| 引数値 | 動作 |
|---|---|
| true | 継承チェーンを遡って属性を検索します。 |
| false | 指定された型そのものに付与されている属性のみを検索します。 |
デフォルトではtrueに設定されていることが多いですが、意図しない属性取得を防ぐために明示的に指定することをお勧めします。
特にインターフェース経由での属性取得は、通常の継承とは挙動が異なる場合があるため、実装時は十分なテストが必要です。
属性の存在確認(IsDefined)
属性の具体的な中身(プロパティ値)が必要なく、単に「その属性が付与されているかどうか」だけを知りたい場合があります。
その場合は、IsDefinedメソッドを使用するのが最も効率的です。
var type = typeof(MyService);
bool hasAttribute = type.IsDefined(typeof(DeveloperInfoAttribute), false);
if (hasAttribute)
{
Console.WriteLine("DeveloperInfo属性が設定されています。");
}
IsDefinedは属性オブジェクトのインスタンスを生成しないため、大量のメタデータをスキャンする際のパフォーマンス向上に寄与します。
パフォーマンスに関する注意点と最適化
リフレクションによる属性の取得は、通常のメソッド呼び出しに比べて計算コストが高い処理です。
高頻度で呼び出されるループ内や、レスポンス速度が求められるAPIのホットパスでリフレクションを多用するのは避けましょう。
パフォーマンスを最適化するためには、取得した属性情報をConcurrentDictionaryなどのキャッシュに保持しておく手法が一般的です。
private static readonly ConcurrentDictionary<Type, string> _nameCache = new();
public string GetCachedName(Type type)
{
return _nameCache.GetOrAdd(type, t => {
var attr = t.GetCustomAttribute<DeveloperInfoAttribute>();
return attr?.Name ?? "Unknown";
});
}
このように一度取得した情報を再利用することで、リフレクションのオーバーヘッドを最小限に抑えることが可能です。
実戦的な応用例:バリデーションエンジンの簡易実装
属性取得の具体的な活用例として、プロパティの値を検証する簡易的なバリデーション機能を考えてみましょう。
以下は、文字列の長さをチェックするカスタム属性とその検証ロジックの例です。
[AttributeUsage(AttributeTargets.Property)]
public class StringLengthAttribute : Attribute
{
public int Max { get; }
public StringLengthAttribute(int max) => Max = max;
}
public class UserInput
{
[StringLength(10)]
public string Username { get; set; }
}
public static class Validator
{
public static bool Validate(object obj)
{
var type = obj.GetType();
foreach (var prop in type.GetProperties())
{
var attr = prop.GetCustomAttribute<StringLengthAttribute>();
if (attr != null)
{
var value = prop.GetValue(obj) as string;
if (value?.Length > attr.Max) return false;
}
}
return true;
}
}
この仕組みを利用することで、ビジネスロジックと検証ルールを分離し、コードの保守性を高めることができます。
まとめ
C#における属性の取得は、プログラムに柔軟性と拡張性をもたらす非常に重要な機能です。
GetCustomAttributeやGetCustomAttributesといったリフレクションAPIを使い分けることで、実行時にメタデータを自在に操ることができます。
基本的にはジェネリック版の拡張メソッドを使用し、型安全なコードを心がけましょう。
また、属性取得は低速な処理であるため、必要に応じてキャッシュ戦略を導入することがプロフェッショナルな開発には不可欠です。
属性とリフレクションをマスターして、より高度なC#プログラミングを実現してください。
