C#における属性(Attribute)は、プログラムの要素に対してメタデータを付加するための非常に強力な機能です。

クラス、メソッド、プロパティなどのソースコード要素に、実行時の振る舞いを決定付ける追加情報を付与することで、コードの柔軟性と再利用性を飛躍的に高めることができます。

標準ライブラリでも [Serializable][Obsolete] といった属性が多用されていますが、開発者が独自のロジックを組み込むために属性を自作(カスタム属性の作成)することも可能です。

本記事では、C#でカスタム属性を定義する手順から、リフレクションを用いた属性情報の取得方法、さらには実戦で役立つ活用シーンまでを徹底的に解説します。

属性(Attribute)の基礎知識

属性とは、アセンブリ、モジュール、型、メンバー、パラメーター、または戻り値に関する追加情報を記述するための宣言的なタグです。

属性自体が何か特定の処理を直接実行するわけではなく、「その要素がどのような性質を持っているか」という情報を付与する役割を担います。

付与された情報は、コンパイル時にバイナリデータとしてアセンブリ内に埋め込まれ、実行時に「リフレクション(Reflection)」という機能を通じて読み取られます。

この仕組みにより、例えば「特定の属性が付いているメソッドだけを実行する」といった動的な処理や、「プロパティに設定された制限事項に基づいて入力値を検証する」といった汎用的なロジックの実装が可能になります。

カスタム属性を自作する最大のメリットは、ロジックとデータ(メタデータ)を分離できる点にあります。

ビジネスロジックの本質を汚すことなく、外部から挙動を制御するためのヒントを記述できるため、クリーンなコード設計に寄与します。

カスタム属性を定義する手順

C#でカスタム属性を作成するのは非常に簡単です。

基本的には System.Attribute クラスを継承したクラスを作成するだけですが、いくつかの命名規則や設計上の作法が存在します。

Attributeクラスの継承

カスタム属性を作成する場合、必ず System.Attribute を継承する必要があります。

これにより、C#コンパイラはそのクラスを「属性」として認識します。

C#
using System;

// 1. Attributeクラスを継承する
// クラス名は「~Attribute」という接尾辞を付けるのが慣例です
public class DeveloperInfoAttribute : Attribute
{
    public string Name { get; }
    public string Version { get; set; }

    // コンストラクタを通じて必須情報を定義
    public DeveloperInfoAttribute(string name)
    {
        Name = name;
    }
}

命名規則と省略記法

カスタム属性のクラス名には、末尾に Attribute を付けることが強く推奨されています。

これは、C#の言語仕様として、属性を使用する際に Attribute 部分を省略して記述できる仕組みがあるためです。

例えば、DeveloperInfoAttribute というクラス名であれば、実際に使用する際は [DeveloperInfo] と記述できます。

これにより、コードの可読性が向上します。

コンストラクタ(位置指定パラメーター)とプロパティ(名前付きパラメーター)

属性を使用する際に渡す引数には、2つの種類があります。

位置指定パラメーター (Positional Parameters)

属性クラスのコンストラクタ引数に対応します。

属性を適用する際に必ず指定しなければならない必須の情報を渡すために使用します。

名前付きパラメーター (Named Parameters)

属性クラスの公開された読み書き可能なプロパティフィールドに対応します。

属性を適用する際に任意で指定できるオプションの情報を設定するために使用します。

C#
// 使用例
// "Taro" は位置指定パラメーター(必須)
// Version = "1.1" は名前付きパラメーター(任意)
[DeveloperInfo("Taro", Version = "1.1")]
public class MyClass
{
    // クラスの定義
}

AttributeUsage属性による制御

カスタム属性を定義する際、その属性が「どこに適用できるか」「重複を許可するか」といった制約を設けることができます。

これには、System.AttributeUsageAttribute を自作した属性クラス自体に付与します。

AttributeTargets:適用対象の制限

AttributeTargets 列挙型を使用することで、属性を付与できる対象(クラス、メソッド、プロパティ、引数など)を制限できます。

C#
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class MyCustomAttribute : Attribute
{
    // クラスとメソッドにのみ付与可能
}

指定可能な主なターゲットは以下の通りです。

ターゲット名説明
Classクラスに適用可能
Methodメソッドに適用可能
Propertyプロパティに適用可能
Fieldフィールドに適用可能
Parameterメソッドの引数に適用可能
Allすべての要素に適用可能

AllowMultiple:重複適用の許可

同じ要素に対して、同じ属性を複数回記述することを許可するかどうかを制御します。

デフォルトは false です。

C#
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public class TagAttribute : Attribute
{
    public string TagName { get; }
    public TagAttribute(string tagName) => TagName = tagName;
}

// AllowMultiple = true なので複数付与できる
[Tag("Urgent")]
[Tag("Refactor")]
public class WorkItem { }

Inherited:継承の制御

属性が付与されたクラスを継承した場合、その派生クラスにも属性を引き継ぐかどうかを決定します。

デフォルトは true です。

派生クラスで属性を再定義させたい場合や、特定のクラス固有の情報に限定したい場合は false に設定します。

属性値の取得(リフレクションの活用)

属性を定義してコードに付与しただけでは、プログラムの動作は変わりません。

実行時に リフレクションを用いて属性の情報を読み取り、それに応じた処理を行うコード を記述する必要があります。

基本的な取得方法

属性情報を取得するには、主に System.Reflection 名前空間のメソッドや、Attribute クラスの静的メソッドを使用します。

C#
using System;
using System.Reflection;

[DeveloperInfo("Hanako", Version = "2.0")]
public class SampleService { }

public class Program
{
    public static void Main()
    {
        // 型情報を取得
        Type type = typeof(SampleService);

        // 特定の属性を取得
        DeveloperInfoAttribute attr = type.GetCustomAttribute<DeveloperInfoAttribute>();

        if (attr != null)
        {
            Console.WriteLine($"Developer: {attr.Name}");
            Console.WriteLine($"Version: {attr.Version}");
        }
    }
}
実行結果
Developer: Hanako
Version: 2.0

メソッドやプロパティからの取得

クラス単位だけでなく、メソッドやプロパティに付与された属性も同様に取得できます。

C#
public class UserProfile
{
    [MaxLength(10)]
    public string UserName { get; set; }
}

// 取得コードの例
PropertyInfo prop = typeof(UserProfile).GetProperty("UserName");
MaxLengthAttribute maxLenAttr = prop.GetCustomAttribute<MaxLengthAttribute>();

このように、PropertyInfoMethodInfo といったリフレクションオブジェクトを経由することで、詳細なメタデータにアクセスできます。

実践的な活用例:簡易バリデーションの実装

カスタム属性の最も代表的な活用例の一つが、プロパティに対するバリデーション(入力チェック)です。

属性に検証ルールを記述し、共通の検証エンジンでそのルールをチェックする仕組みを構築してみましょう。

ここでは、文字列の長さをチェックする StringLengthAttribute を自作します。

1. カスタム属性の定義

C#
using System;

[AttributeUsage(AttributeTargets.Property)]
public class StringLengthAttribute : Attribute
{
    public int MaximumLength { get; }
    public string ErrorMessage { get; set; }

    public StringLengthAttribute(int maximumLength)
    {
        MaximumLength = maximumLength;
    }
}

2. 属性の適用

検証対象となるモデルクラスに属性を付加します。

C#
public class UserRegistration
{
    [StringLength(8, ErrorMessage = "ユーザー名は8文字以内で入力してください。")]
    public string UserName { get; set; }

    [StringLength(20, ErrorMessage = "パスワードが長すぎます。")]
    public string Password { get; set; }
}

3. バリデーションエンジンの実装

リフレクションを使用して、オブジェクトの全プロパティをスキャンし、StringLengthAttribute が付いている箇所をチェックします。

C#
using System;
using System.Reflection;
using System.Collections.Generic;

public static class Validator
{
    public static bool Validate(object obj, out List<string> errors)
    {
        errors = new List<string>();
        Type type = obj.GetType();
        
        // プロパティをすべて取得
        PropertyInfo[] properties = type.GetProperties();

        foreach (var prop in properties)
        {
            // プロパティに付与されている StringLengthAttribute を取得
            var attr = prop.GetCustomAttribute<StringLengthAttribute>();
            
            if (attr != null)
            {
                // 値を取得
                object value = prop.GetValue(obj);
                if (value is string strValue)
                {
                    // 長さの検証
                    if (strValue.Length > attr.MaximumLength)
                    {
                        errors.Add(attr.ErrorMessage ?? $"{prop.Name} is too long.");
                    }
                }
            }
        }

        return errors.Count == 0;
    }
}

4. 実行と結果

実際に不正なデータを入力して検証してみます。

C#
public class Program
{
    public static void Main()
    {
        var user = new UserRegistration
        {
            UserName = "VeryLongUserNameExample",
            Password = "SecurePassword123"
        };

        if (!Validator.Validate(user, out var errors))
        {
            Console.WriteLine("【バリデーションエラーが発生しました】");
            foreach (var error in errors)
            {
                Console.WriteLine($"- {error}");
            }
        }
        else
        {
            Console.WriteLine("検証に成功しました。");
        }
    }
}
実行結果
【バリデーションエラーが発生しました】
- ユーザー名は8文字以内で入力してください。

この手法の素晴らしい点は、「検証ルールを増やしたい場合でも、モデルクラスに属性を1行追加するだけで済む」 という拡張性にあります。

if文による条件分岐をビジネスロジック内に書き散らす必要がなくなります。

パフォーマンスへの配慮と注意点

カスタム属性とリフレクションは非常に便利ですが、多用する際にはパフォーマンス面に注意が必要です。

リフレクションのコスト

リフレクション(GetCustomAttributes 等の呼び出し)は、通常のメソッド呼び出しやプロパティアクセスに比べて 計算コストが高い処理 です。

ループ内で頻繁に属性情報を取得するような実装は、アプリケーションのレスポンス低下を招く恐れがあります。

対策として、以下の方法が推奨されます。

キャッシュの活用

一度取得した属性情報は Dictionary<Type, Attribute> などの静的キャッシュに保持し、二回目以降はリフレクションを行わずに取得します。

これにより 実行時のコストを削減できます。

並行アクセスがある場合は スレッドセーフ なキャッシュ(例:ConcurrentDictionary)や適切な無効化戦略を検討してください。

起動時のスキャン

アプリケーションの起動時(初期化時)に一度だけ属性を読み取り、必要な情報をメモリ上に展開しておきます。

起動時にまとめて処理することで、稼働中のリフレクションを回避し、レスポンス性能を向上させます。

起動時間やメモリ使用量とのトレードオフを考慮し、非同期読み込みや遅延ロードを組み合わせることも有効です。

属性に使用できる型

属性のコンストラクタやプロパティに使用できる型には制限があります。

以下の型に限定されていることに注意してください。

  • プリミティブ型(bool, int, double など)
  • string
  • System.Type
  • 列挙型(enum
  • 上記の型の1次元配列

自分で定義したクラスのインスタンスなどを属性の引数に渡すことはできません。

複雑なデータを渡したい場合は、文字列(JSON形式など)で渡して属性内部でパースするか、複数のプロパティを組み合わせて表現する必要があります。

Source Generators という選択肢

現代のC#(C# 9.0以降)では、パフォーマンスを極限まで追求する場合、リフレクションの代わりに Source Generators(ソースジェネレーター) を検討することが増えています。

Source Generators を使用すると、コンパイル時に属性を解析し、それに基づいたコードを自動生成できます。

これにより、実行時のリフレクションコストをゼロにしながら、属性によるメタデータ駆動開発の恩恵を享受することが可能 になります。

大規模なフレームワーク開発や、ハイパフォーマンスが求められるライブラリ作成においては、この手法が主流になりつつあります。

まとめ

C#におけるカスタム属性の自作は、コードに「意味」を持たせ、共通処理をスマートに共通化するための強力な武器となります。

本記事で解説した内容を振り返ります。

  • 属性は Attribute クラスを継承し、末尾に Attribute と命名する。
  • AttributeUsage を使って、属性の適用範囲や重複可否を適切に制御する。
  • 付与した情報は、実行時にリフレクション(GetCustomAttribute など)を通じて取得する。
  • バリデーションやロギングなどの「横断的な関心事」を分離するのに最適である。
  • 実行時のパフォーマンス低下を避けるため、キャッシュの利用や Source Generators の検討を行う。

属性を使いこなすことで、ソースコードは単なる命令の羅列から、自己記述的な洗練されたシステムへと進化します。

まずはプロジェクト内の小さな共通処理から、カスタム属性の導入を試してみてはいかがでしょうか。