カスタム属性の定義と読み取り

属性は、情報をコードに宣言的に関連付けるための手段を提供します。 また、さまざまなターゲットに適用できる再利用可能な要素も提供します。 ObsoleteAttribute について考えてみましょう。 この属性はクラス、構造体、メソッド、コンストラクトなどに適用できます。 これは、その要素が古いことを "宣言" します。 この属性を検索して、対応する何らかのアクションを実行するのは、C# コンパイラの役目です。

このチュートリアルでは、コードに属性を追加する方法、独自の属性を作成して使用する方法、.NET に組み込まれているいくつかの属性を使用する方法について学びます。

前提条件

お使いのマシンで、.NET を実行するように設定する必要があります。 インストールの手順については、「.NET のダウンロード」のページを参照してください。 このアプリケーションは、Windows、Ubuntu Linux、macOS または Docker コンテナーで実行できます。 お好みのコード エディターをインストールしてください。 次の説明では、オープン ソースのクロスプラットフォーム エディターである Visual Studio Code を使用しています。 しかし、他の使い慣れたツールを使用しても構いません。

アプリを作成する

すべてのツールをインストールしたら、新しい .NET コンソール アプリを作成します。 コマンド ライン ジェネレーターを使用するには、お使いのシェルで次のコマンドを実行します。

dotnet new console

このコマンドは、必要最低限の .NET プロジェクト ファイルを作成します。 dotnet restore を実行して、このプロジェクトのコンパイルに必要な依存関係を復元します。

復元を必要とするすべてのコマンド (dotnet newdotnet builddotnet rundotnet testdotnet publishdotnet pack など) によって暗黙的に実行されるため、dotnet restore を実行する必要がなくなりました。 暗黙的な復元を無効にするには、--no-restore オプションを使用します。

dotnet restoreなどの、明示的な復元が意味のある一部のシナリオや、復元が行われるタイミングを明示的に制御する必要があるビルド システムでは、dotnet restore は引き続き有用なコマンドです。

NuGet フィードの管理方法については、dotnet restore のドキュメントをご覧ください。

プログラムを実行するには dotnet run を使用します。 コンソールに "Hello, World" という出力が表示されます。

コードに属性を追加する

C# では、属性は Attribute 基底クラスを継承するクラスです。 Attribute クラスから継承したクラスは、コードの他の部分で一種の "タグ" として使用できます。 たとえば ObsoleteAttribute という名前の属性があります。 この属性は、コードが古く、使用されなくなったことを示します。 この属性を、たとえば角かっこを使用して、クラスに配置します。

[Obsolete]
public class MyClass
{
}

この属性の名前は ObsoleteAttribute ですが、コードで使う必要があるのは [Obsolete] だけです。 ほとんどの C# コードは、この規則に従います。 完全な名前 [ObsoleteAttribute] も使用できます。

クラスを現在使用されていないとマークするときは、その "理由" と、代わりに "何を" 使用べきかについての情報を提供することをお勧めします。 この説明を提供するには、Obsolete 属性に文字列パラメーターを含めます。

[Obsolete("ThisClass is obsolete. Use ThisClass2 instead.")]
public class ThisClass
{
}

この文字列は、var attr = new ObsoleteAttribute("some string") と記述した場合と同様に、ObsoleteAttribute コンストラクターに引数として渡されます。

属性コンストラクターに渡すパラメーターは、単純な型/リテラル (bool, int, double, string, Type, enums, etc) とそれらの配列のみに限られます。 式または変数は使用できません。 位置指定または名前付きパラメーターは自由に使用できます。

独自の属性の作成

属性を作成するには、Attribute 基底クラスから継承する新しいクラスを定義します。

public class MySpecialAttribute : Attribute
{
}

上記のコードでは、[MySpecial] (または [MySpecialAttribute]) をコード ベースの他の場所の属性として使用できます。

[MySpecial]
public class SomeOtherClass
{
}

.NET の基本クラス ライブラリに含まれる ObsoleteAttribute のような属性は、コンパイラ内で特定の動作をトリガーします。 しかし、作成した属性はメタデータとしてのみ機能するため、属性クラス内のコードは実行されません。 コード内の他の場所で、そのメタデータを利用するかどうかはユーザー次第です。

ここで注意すべき "罠" があります。 前述のように、属性を使用するときは、特定の型のみを引数として渡すことができます。 しかし、属性の型を作成するときに、C# コンパイラによってそれらのパラメーターの作成が阻止されることはありません。 次の例では、正しくコンパイルするコンストラクターを持つ属性を作成しました。

public class GotchaAttribute : Attribute
{
    public GotchaAttribute(Foo myClass, string str)
    {
    }
}

しかし、このコンストラクターを属性構文で使用することはできません。

[Gotcha(new Foo(), "test")] // does not compile
public class AttributeFail
{
}

上記のコードでは、Attribute constructor parameter 'myClass' has type 'Foo', which is not a valid attribute parameter type のようなコンパイラ エラーが発生します

属性の用途を制限する方法

属性は、次の "targets" で使用できます。 上の例ではクラスに使用しましたが、次に対しても使用できます。

  • アセンブリ
  • クラス
  • コンストラクター
  • 代理人
  • 列挙型
  • Event
  • フィールド
  • GenericParameter
  • インターフェイス
  • メソッド
  • モジュール
  • パラメーター
  • プロパティ
  • ReturnValue
  • 構造体

C# の既定では、属性クラスを作成した場合、その属性は可能なすべての属性ターゲットに使用できます。 属性を特定のターゲットにのみ使用できるように制限するには、属性クラスに対してAttributeUsageAttribute を使用します。 つまり、属性に属性を設定します。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public class MyAttributeForClassAndStructOnly : Attribute
{
}

クラスまたは構造体以外のターゲットに上の属性を設定しようとすると、Attribute 'MyAttributeForClassAndStructOnly' is not valid on this declaration type. It is only valid on 'class, struct' declarations のようなコンパイラ エラーが発生します。

public class Foo
{
    // if the below attribute was uncommented, it would cause a compiler error
    // [MyAttributeForClassAndStructOnly]
    public Foo()
    { }
}

コード要素にアタッチされた属性を使用する方法

属性はメタデータとして機能します。 外からの力が働かないかぎり、実際には何の処理も行いません。

属性を見つけて操作するには、通常、リフレクションが必要です。 リフレクションを使用すると、他のコードを調べるコードを C# で記述できます。 たとえば、Reflection を使用して次のクラスに関する情報を取得できます (コードの先頭に using System.Reflection; を追加する)。

TypeInfo typeInfo = typeof(MyClass).GetTypeInfo();
Console.WriteLine("The assembly qualified name of MyClass is " + typeInfo.AssemblyQualifiedName);

これにより、次のようなものが出力されます。The assembly qualified name of MyClass is ConsoleApplication.MyClass, attributes, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

TypeInfo オブジェクト (または MemberInfoFieldInfo、あるいは他のオブジェクト) を取得したら、GetCustomAttributes メソッドを使用できます。 このメソッドにより、Attribute オブジェクトのコレクションが返されます。 また、GetCustomAttribute を使用して Attribute 型を指定することもできます。

MyClass クラス (前の例で [Obsolete] 属性を適用したクラス)の MemberInfo インスタンスに対して GetCustomAttributes を使用する例を以下に示します。

var attrs = typeInfo.GetCustomAttributes();
foreach(var attr in attrs)
    Console.WriteLine("Attribute on MyClass: " + attr.GetType().Name);

これにより Attribute on MyClass: ObsoleteAttribute がコンソールに出力されます。 MyClass に他の属性を追加してみてください。

Attribute オブジェクトは限定的にインスタンス化されることに注意してください。 つまり、GetCustomAttribute または GetCustomAttributes を使用するまでインスタンスは作成されません。 また、毎回インスタンスが作成されます。 1 行内で GetCustomAttributes を 2 回呼び出すと、ObsoleteAttribute の異なる 2 つのインスタンスが返されます。

ランタイムの一般的な属性

属性は、さまざまなツールやフレームワークで使用されます。 NUnit は、[Test][TestFixture] などの属性を NUnit テスト ランナーで使用します。 ASP.NET MVC は、[Authorize] などの属性を使用して、MVC アクションに対する横断的な処理を実行するためのアクション フィルター フレームワークを提供します。 PostSharp は、属性構文を使用して C# でアスペクト指向プログラミングを行えるようにします。

.NET Core の基本クラス ライブラリに組み込まれている、よく使用される属性のいくつかを以下に示します。

  • [Obsolete]。 これは上の例で使用した属性で、System 名前空間に格納されています。 この属性は、コード ベースの変更に関する宣言的なドキュメントを提供するのに便利です。 メッセージは文字列の形式で指定でき、別のブール型パラメーターを使用すると、コンパイラの警告をコンパイラのエラーにエスカレートすることができます。
  • [Conditional]. この属性は System.Diagnostics 名前空間に格納されています。 この属性はメソッド (または属性クラス) に適用できます。 コンス トラクターに文字列を渡す必要があります。 その文字列が #define ディレクティブと一致しない場合、そのメソッドの呼び出し (メソッド自体ではありません) が C# コンパイラによって削除されます。 通常、この手法はデバッグ (診断) の目的で使用します。
  • [CallerMemberName]. この属性はパラメーターに使用でき、System.Runtime.CompilerServices 名前空間に格納されています。 CallerMemberName は、別のメソッドを呼び出しているメソッドの名前を挿入するために使用する属性です。 これは、さまざまな UI フレームワークで INotifyPropertyChanged を実装する際に "マジック文字列" を排除する方法です。 例
public class MyUIClass : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    public void RaisePropertyChanged([CallerMemberName] string propertyName = default!)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    private string? _name;
    public string? Name
    {
        get { return _name;}
        set
        {
            if (value != _name)
            {
                _name = value;
                RaisePropertyChanged();   // notice that "Name" is not needed here explicitly
            }
        }
    }
}

上のコードでは、リテラルの "Name" 文字列を使用する必要はありません。 CallerMemberName を使用すると、入力ミス関連のバグを防ぎ、よりスムーズなリファクタリングや名前変更に役立ちます。 属性によって C# に宣言機能が追加されますが、それらはメタデータ形式のコードであり、単独では機能しません。