xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

拡張DisplayName属性

C#が「とっても×6 だーいすーきよー?」な皆さん、こんにちは!今回の記事はC# Advent Calender 2012の22日目の記事です。投稿日付が23日?...野暮なこと言っちゃいけません。ちっちゃなことは気にしちゃいけません。誰が何と言おうと、22日目の記事です(棒

ちょっと前フリ

みなさん、属性使ってますか?属性、凄いですよね、カッコいいですよね!属性を初めて見たときからその設計思想に感激して、今でも考えた人を称賛したいくらいです。えぇ、.NET Frameworkの実装設計ですごいところのひとつは属性だと本気で思ってます。「属性?何ソレ?」な方はちょこっと属性についてググってみてください。Microsoftを愛してやまない方はBingでも問題ありません。ASP.NET MVCをやっている人はバリバリだと思いますし、従来のデスクトップアプリをやっている人はあんまり使わないかもしれません。かく言う僕は、最近ASP.NET MVCと仲良しこよしなので(ry。

今回はDisplayName属性がもっと便利になるように拡張してみます。DisplayName属性はプロパティやメソッドなどの表示名を指定するために利用します。例えば、PropertyGridなどと一緒に利用します。ところで、静的な記述ではあるものの、せっかく表示名を設定するならenumなどに設定できればかなり嬉しいでしょう。さらに言うと、構造体だろうと引数だろうとGenericパラメーターだろうと設定できると嬉しいかもしれません。つまり、属性を設定できる箇所、設定できる対象すべてに名前を付けられて良いはずです。そして、そこから比較的簡単に値を取り出せるべきです。ということで作りました。

実装

拡張したDisplayName属性の実装は以下のような感じです。System.ComponentModel名前空間にあるDisplayName属性は属性を設定できる対象が限られていましたが、AttributeTargets.Allとすることで制限を撤廃しています。また、継承しているので既存のものの置き換えも可能です

using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Base = System.ComponentModel.DisplayNameAttribute;
using This = Xin9le.ComponentModel.DisplayNameAttribute;

namespace Xin9le.ComponentModel
{
    //--- ターゲットをすべてに拡張
    [AttributeUsageAttribute(AttributeTargets.All, AllowMultiple = false)]
    public class DisplayNameAttribute : Base
    {
        public DisplayNameAttribute(string name)
            : base(name)
        {}

        //--- ほとんどコレでカバー
        public static string Get(ICustomAttributeProvider provider)
        {
            if (provider == null)
                throw new ArgumentNullException("provider");
            var attributes = provider.GetCustomAttributes(typeof(This), false) as This[];
            return  attributes != null && attributes.Any()
                ?   attributes[0].DisplayName
                :   null;
        }

        //--- enumは特別
        public static string Get(Enum value)
        {
            var type = value.GetType();
            var name = Enum.GetName(type, value);
            var info = type.GetField(name);
            return This.Get(info);
        }

        //--- フィールド/プロパティ/戻り値のあるメソッド用
        public static string Get<T>(Expression<Func<T>> expression)
        {
            return This.Get((LambdaExpression)expression)
                ?? This.Get(typeof(T));  //--- エラーのときは型情報でリトライ
        }

        //--- 戻り値のないメソッド/イベント用
        public static string Get(Expression<Action> expression)
        {
            return This.Get((LambdaExpression)expression);
        }

        //--- まとめて処理しちゃえ作戦
        private static string Get(LambdaExpression expression)
        {
            if (expression == null)
                throw new ArgumentNullException("expression");

            var body        = expression.Body;
            MemberInfo info = body is MemberExpression      ? (body as MemberExpression).Member
                            : body is MethodCallExpression  ? (body as MethodCallExpression).Method
                            : null;
            return info == null ? null : This.Get(info);
        }
    }
}

追加されている4つの静的Getメソッドによって、DisplayName属性から表示名を取得できるようにしています。Type型やMemberInfo型、ParameterInfo型などは全てICustomAttributeProviderインターフェースを実装しているので、大抵コレひとつでまかなえます。その他の3つは、enumのフィールドやメソッド、プロパティ、フィールドなどにDisplayName属性を付けた場合に備えた、より簡単に表示名を取得するためのショートカットです。

さらに単純化

ICustomAttributeProviderインターフェースとenumに関しては、もっと単純にしたいと思うでしょう。なので、次のような拡張メソッドも用意しました。

using System;
using System.Reflection;

namespace Xin9le.ComponentModel
{
    public static class DisplayNameAttributeExtensions
    {
        public static string DisplayName(this ICustomAttributeProvider provider)
        {
            return DisplayNameAttribute.Get(provider);
        }

        public static string DisplayName(this Enum value)
        {
            return DisplayNameAttribute.Get(value);
        }
    }
}

利用例

最初にenumのフィールドに属性を付けた場合の例を示します。拡張メソッドの力で超簡単に取得できます。

//--- こんな列挙体があるとする
[DisplayName("フルーツ")]
enum Fruits
{
    [DisplayName("りんご")] Apple = 0,
    [DisplayName("みかん")] Orange,
    [DisplayName("いちご")] Strawberry,
}

//--- こんな感じで簡単に名称を拾えます
class Program
{
    static void Main()
    {
        var type = typeof(Fruits);
        Console.WriteLine(type.DisplayName());
        foreach (Fruits x in Enum.GetValues(type))
            Console.WriteLine("\\t{0}", x.DisplayName());
    }
}

//--- コンソール出力結果
/*
フルーツ
       りんご
       みかん
       いちご
*/

次にクラスのメンバに属性を付けた場合です。こんなバカみたいにメンバに表示名を付けるシーンなどまずないとは思いますが、「できる」ということで...。

//--- こんなクラスがあるとする
[DisplayName("クラス")]
class Sample
{
    [DisplayName("コンストラクタ")] public Sample(){}
    [DisplayName("メソッド")]      public void Method(){}
    [DisplayName("フィールド")]    public int Field = 10;
    [DisplayName("デリゲート")]    public delegate void Delegate();
    [DisplayName("プロパティ")]
    public string Property
    {
        [DisplayName("Getプロパティ")] get;
        [DisplayName("Setプロパティ")] set;
    }
    [DisplayName("イベント")]
    public event Action Event
    {
        [DisplayName("Addイベント")]    add{}
        [DisplayName("Removeイベント")] remove{}
    }
}

//--- こんな感じで簡単に名称を拾えます
class Program
{
    static void Main()
    {
        var type  = typeof(Sample);
        var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly;
        Console.WriteLine(type.DisplayName());
        foreach (var x in type.GetMembers(flags))
            Console.WriteLine("\\t{0}\\t{1}", x.DisplayName(), x.Name);
    }
}

//--- コンソール出力結果
/*
クラス
       メソッド        Method
       Getプロパティ   get_Property
       Setプロパティ   set_Property
       Addイベント     add_Event
       Removeイベント  remove_Event
       コンストラクタ  .ctor
       プロパティ      Property
       イベント        Event
       フィールド      Field
       デリゲート      Delegate
*/

最後に式木を使った例です。「MemberInfoをイチイチ取得するなんてめんどくさい!」という方でも、タイプセーフに取得できます。もちろん、静的なメンバーに対しても適用可能です。

static void Main()
{
    var sample = new Sample();
    Console.WriteLine(DisplayNameAttribute.Get(() => sample.Method()));
    Console.WriteLine(DisplayNameAttribute.Get(() => sample.Property));
    Console.WriteLine(DisplayNameAttribute.Get(() => sample.Field));
}

//--- コンソール出力結果
/*
メソッド
プロパティ
フィールド
*/

まとめ

現状、すでにいくつかの不満点やうまくできないケースもありますが、ほとんどのケースで耐えられると思います。コレのベースになるものを作ったのは3年半程前になるのですが、個人的にはenumのフィールドに属性を付けるコーディングをよくするので大変重宝しています。ちなみにenumに関してのみ言えば、すでに「いげ太のブログ - enumに文字列の属性を」で取り上げられているのですが、enumのフィールド専用のものを作るというのはイヤだったので、作成当初から思想は既存機能の拡張な感じで作っていました。今回はこの記事に合わせて式木バージョンをを追加したのですが、なんかシックリ来ない感じが拭い切れない...。式木レベルが低いということで、もっと勉強します!

さらにちなみにですが、2013年のHokuriku.NETでは「メタプログラミング入門」と題して、リフレクション、dynamic、式木なんかについてハンズオン勉強会を開く予定です。もっとC#を内側からコネくり回してみたい方は是非ご参加ください!

次の担当者

明日 (というかすでに今日) の担当は、C# MVPの長老@AILightさんです。10年連続受賞って神様ですね!モチベーション維持の方法も含めて色々教わりたいです。では、23日目よろしくお願いします!...とか書いていたら、すでに先に投稿されていた...orz

石野光仁 Blog - dynamic型を利用した時のパフォーマンス調査