ネコのために鐘は鳴る

寺院に住み着くパソコ〇好き

C#5でもnameofしたい

結論

前置きが長いと結論を早く知りたいマンにボコられるので、 まずはタイトルに対する結論から。

using System;
using System.Linq.Expressions;

public static class GetName
{
    public static string Of<MemberType>(Expression<Func<MemberType>> expression)
    {
        return (expression.Body as MemberExpression).Member.Name;
    }
}

これでC#6未満でもnameofとほぼ同等のことができます。

public class Sample
{
    public string Hoge { get; set; }

    public void Sample()
    {
        // 今回のやり方
        var name1 = GetName.Of(() => Hoge);
        // C#6以降
        var name2 = nameof(Hoge);
    }
}

もっとくわしく

背景

C#6から導入された演算子nameofC#5以前のバージョンでも使いたい。 そもそも何故そんなことがしたいのかと言うと、 オトナの事情により人権を与えられず2019年現在にVisual Studio 2013 (C#5) を使って開発させられているC#erが世の中には存在するからです。

私です。

PropertyChangedイベントなどを実装すると、必ずと言ってもいいほどプロパティの名前を取得したいものです。 そんな時にリテラルでプロパティ名をベタ書きするとVisual Studioの参照が効かないため、あらゆるリファクタリング機能の恩恵を受けられません。

Visual Studioの機能がない状態でC#を書くなど、ナイフ1本でバイオハザード4をクリア [注1] するぐらいマゾヒストもいいところです。

そこで、なんとかしてリテラル文字列を使わず、参照した状態で要素名を取得したいのです。

実装と注意点

やっている内容は、Expressionをラムダ式の形で対象の要素を受け取ると、そのBodyからその要素自身の定義名を取得できることを利用しています。 書き方の簡便さは対象の要素を直接書くだけの本家nameofには負けますが、それなりに短く見た目も使い方もnameofに近くまぁまぁ妥協点ではあると思います。

ちなみにクラス名をNameではなくGetNameにしているのは、Nameはよく使用される使用頻度の高いプロパティ名なので衝突を避ける意味でそうしています。

このGetName.Of()と本家nameofの違いですが、この実装ではプロパティ、フィールド、イベントについては名前が取得できますが、 クラス名、メソッドについては取得できません。本家nameofは全て取れます。何か良い方法があれば教えてください。

それから、nameofは静的解決で定数リテラル扱いであるのに対して、今回のGetName.Ofは動的解決です。 これが何を意味するのかと言うと、例えばnameofによって得られた文字は属性の引数として使えますが、GetName.Ofコンパイルエラーになります。 (属性の引数は静的に決定している必要があるため)

// これはOK
[DisplayName(nameof(Hoge))]
public int SomeProperty { get; set; }

// これはコンパイルエラー
[DisplayName(GetName.Of(() => Hoge))]
public int SomeProperty { get; set; }

あと気になるのは、実行速度でしょうか。テストしました。

public class Sample
{
    public string Hoge { get; set; }

    public void CalcTime()
    {
        var length = 5000000;

        var sw = new Stopwatch();
        sw.Start();
        for(int i = 0; i < length; i++) {
            var name = nameof(Hoge);
        }
        sw.Stop();
        var time1 = sw.ElapsedMilliseconds;
        sw.Restart();
        for(int i = 0; i < length; i++) {
            var name = GetName.Of(() => Hoge);
        }
        sw.Stop();
        var time2 = sw.ElapsedMilliseconds;
        Console.WriteLine($"time1: {time1}ms, time2: {time2}ms");
    }
}

私のPC(Core i5-4300U @ 1.9GH)での計測結果はこちらです

time1: 17ms, time2: 7494ms

nameofに比べて2, 3桁遅いことが分かります。が500万ループで7500msなので1回約1.5us。

この時間を気にかける前にもっとすべき最適化は他にあるでしょう。これを使うことでVisual Studioによって得られる恩恵を考えると十分実用範囲内です。

まとめ

オトナを説得して人権を得て、nameofを使用した方がより幸せになるでしょう。

参考文献

注1 超余裕!瀕死のレオンがナイフ1本でバイオ4攻略1-1【ゆっくり】