2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】属性についてまとめてみた

Posted at

概要

C#の属性についてまとめてみました。

目次

属性とは

属性(Attribute)は、コードに補助情報(メタデータ)を付与するための仕組みです。これにより実行時やコンパイル時に特定の動作を制御したり、情報を付加したりできます。

メタデータとは

「データに関するデータ」のことです。具体的には、あるデータそのものではなくそのデータを説明・補足するためのデータです。
例えばスマホで撮った写真データの場合は、撮影日時やGPSの位置情報がメタデータになります。テキストデータの場合は、そのテキストデータの作成日時やファイルサイズがメタデータになります。コードでは、コードそのものがデータで、そのコードのメタデータとして、属性を付与できます。
例えば以下のようなイメージです。

  • [Obsolete] → このメソッドは古いですよ
  • [Authorize] → 認証が必要ですよ
  • [JsonIgnore] → このプロパティはJSONに含めないでください

属性が使用できる場所

属性は、クラス・構造体・列挙体・メソッド・プロパティ・フィールド・イベント・パラメータ(引数)・戻り値・アセンブリなど、コードの様々な箇所で使用できます。

属性の定義方法

属性は通常のクラスと同様に定義できますが、C#では System.Attribute クラスを継承する必要があります。また、属性も一つのオブジェクトとして扱われ、リフレクションを通じて取得されます。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class MyCustomAttribute : Attribute
{
    public string Info { get; }
    public MyCustomAttribute(string info) => Info = info;
}

属性クラスの生成タイミング

C#の属性は、コンパイル時にアセンブリのメタデータとして埋め込まれます。
属性のインスタンスが実際に生成されるのは、リフレクションなどで属性情報を読み取ったときです。このとき初めて、属性クラスのコンストラクタが呼び出され、オブジェクトが生成されます。

属性クラスのメソッド呼び出し

属性クラスに定義されたメソッドは、通常のコードから直接呼び出すことはできません。属性はあくまでメタデータとしての役割を果たすものであり、ロジックを直接実行するためのものではありません。

例えば、以下のような属性クラスを定義したとしても、

public class MyAttribute : Attribute
{
    public void DoSomething()
    {
        Console.WriteLine("属性のメソッドが呼ばれました");
    }
}

この DoSomething() メソッドは、属性を付与した対象(クラスやメソッド)から直接呼び出すことはできません。呼び出すには、以下のようにリフレクションを使って属性インスタンスを取得し、その上でメソッドを呼び出す必要があります。もしくは、普通のオブジェクトと同様にnewするとその中の呼び出せます。

var attr = typeof(MyClass).GetCustomAttribute<MyAttribute>();
attr?.DoSomething(); // ← このように明示的に呼び出す必要がある

// もしくは普通のインスタンスとして呼び出せます
var attr = new MyAttribute();
attr.DoSomething();

属性の一例

クラスに使用される属性

[Serializable]

この属性を付けると、クラスのインスタンスをバイナリ形式でファイルに保存したり、読み込みできるようになります。 JSON や XML のような形式で保存するのが主流です。

[ApiController]

ASP.NET Core の Web API 開発で使われる属性です。この属性をクラスに付けると、APIとしての基本的な動作(リクエストの自動解析、400エラーの自動返却など)が有効になります。

メソッドに使用される属性

[HttpGet]

Web API のメソッドが「GETリクエスト」に対応していることを示します。URLにアクセスするだけでデータを取得するような処理に使います。

[HttpPost]

Web API のメソッドが「POSTリクエスト」に対応していることを示します。フォーム送信やデータ登録など、サーバーに情報を送る処理に使います。

[Obsolete]

この属性を付けると、「このメソッドは古くなっているので使わないでください」という警告を出すことができ、古いメソッドに対して注意喚起できます。

[Conditional("DEBUG")]

この属性を付けると、指定された条件(例:DEBUG)が定義されているときだけメソッドが呼び出されるようになります。主にログ出力などに使われます。

[AllowAnonymous]

この属性を付けると、認証が不要になり、誰でもアクセス可能になります。 [Authorize] とセットで使用することが多く、 [Authorize] クラスに付いていても、個別のメソッドに [AllowAnonymous] を付けることで、そのメソッドだけは例外として公開できます。

プロパティ・フィールドに使用される属性

[JsonPropertyName("name")]

JSONのシリアライズ時に、プロパティ名を変更したいときに使います。

[JsonIgnore]

この属性を付けると、JSON出力からそのプロパティを除外できます。内部情報や機密データを保持したプロパティの内容を出力しないようにできます。

クラス・メソッドに使用される属性

[Authorize]

この属性を付けると、ログイン済みのユーザーだけがアクセスできるように制限されます。クラスに付けると、そのクラス内のすべてのアクション(メソッド)に適用されます。メソッドに付けると、そのメソッドだけに適用されます。管理画面やユーザー専用ページなど、未ログイン者に見せたくない機能に使います。

サンプルコード

例1

属性を貼ったメソッドだけスキップする例です。
Main() ではリフレクションで Service クラスの公開インスタンスメソッドを列挙し、各メソッドにカスタム属性である [Skip] が付いているかを調べています。
付いていれば 実行せずメッセージを出す、付いていなければ実行するという分岐を行っています。

using System;
using System.Linq;
using System.Reflection;

[AttributeUsage(AttributeTargets.Method)]
public sealed class SkipAttribute : Attribute
{
}

public class Service
{
    [Skip] // ← この属性を外すと、メソッドが実行されるようになります
    public void Dangerous()
    {
        Console.WriteLine("Dangerous() 実行");
    }

    public void Safe()
    {
        Console.WriteLine("Safe() 実行");
    }
}

public static class Program
{
    public static void Main()
    {
        var service = new Service();
        var methods = typeof(Service).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);

        foreach (var m in methods)
        {
            bool hasSkip = m.GetCustomAttribute<SkipAttribute>() != null;
            if (hasSkip)
            {
                Console.WriteLine($"{m.Name} は [Skip] のため実行しません");
            }
            else
            {
                m.Invoke(service, null);
            }
        }
    }
}

例2

シンボルが 定義されているときだけ メソッド呼び出しを有効にする例です。
#define USE_LOG をコメントアウトするとLog() メソッドの処理が実行されなくなります。

#define USE_LOG // ← この行をコメントアウトすると Log 呼び出しは消えます (先頭に記述する必要あり)

using System;
using System.Diagnostics;

public static class Program
{
    public static void Main()
    {
        Console.WriteLine("Start");
        Log("ここは条件付きログ");
        Console.WriteLine("End");
    }

    [Conditional("USE_LOG")]
    private static void Log(string message)
    {
        Console.WriteLine($"[LOG] {message}");
    }
}

例3

属性を使用して、クラスをJSON形式にシリアライズする際にコントロールできます。

using System;
using System.Text.Json;
using System.Text.Json.Serialization;

public class Product
{
    [JsonPropertyName("id")] // JSON のキー名を変更
    public int ProductId { get; set; }

    [JsonIgnore] // JSON 出力から除外
    public bool IsInternal { get; set; }
}

public static class Program
{
    public static void Main()
    {
        var p = new Product
        {
            ProductId = 1,
            IsInternal = true
        };

        string json = JsonSerializer.Serialize(p, new JsonSerializerOptions
        {
            WriteIndented = true
        });

        Console.WriteLine(json);
    }
}

例4

カスタム属性を使ってパンくずリスト(Breadcrumb)を構築しています。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
public class BreadcrumbAttribute : Attribute
{
    public string Title { get; }
    public string Parent { get; }

    public BreadcrumbAttribute(string title, string parent = null)
    {
        Title = title;
        Parent = parent;
    }
}
[Breadcrumb("商品管理")]
public class ProductController : Controller
{
    [Breadcrumb("商品一覧", "商品管理")]
    public IActionResult Index() => View();

    [Breadcrumb("商品詳細", "商品一覧")]
    public IActionResult Details(int id) => View();
}

リフレクションで取得してパンくずを構築しています。

public static List<string> GetBreadcrumbs(Type controllerType, string actionName)
{
    var breadcrumbs = new List<string>();

    // controllerType の公開インスタンスメソッドのうち、名前が actionName の MethodInfo を取得
    var method = controllerType.GetMethod(actionName);

    // メソッドに付与された BreadcrumbAttribute を取得
    var methodAttr = method?.GetCustomAttribute<BreadcrumbAttribute>();

    // クラスに付与された BreadcrumbAttribute を取得
    var classAttr = controllerType.GetCustomAttribute<BreadcrumbAttribute>();

    if (methodAttr != null)
    {
        breadcrumbs.Add(methodAttr.Title);

        // Parent が指定されていれば追加
        if (methodAttr.Parent != null) breadcrumbs.Add(methodAttr.Parent);
    }

    if (classAttr != null && !breadcrumbs.Contains(classAttr.Title))
    {
        // クラス属性が存在し、まだリストにクラスタイトルが含まれていなければ追加
        breadcrumbs.Add(classAttr.Title);
    }

    breadcrumbs.Reverse(); // 階層順に並べる
    return breadcrumbs;
}

カスタム属性を使用する際は、リフレクションを使うのが一般的です。というより、カスタム属性の本質は「リフレクションで読み取るためのメタデータをコードに埋め込むこと」にあります。

終わりに

属性はコードに意味を付与するメタデータを付けることができる仕組みです。上手に活用することで、で実用的な機能も簡潔に実現できると思いました。

2
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?