htsign's blog

ノンジャンルで書くつもりだけど技術系が多いんじゃないかと思います

.NET Framework 3.5環境で nameof 演算子っぽいことする

最近仕事でこんな感じのメソッドを定義して使っています。

public static class Utility
{
    private static Dictionary<string, string> exprCache = new Dictionary<string, string>();
    
    public static string NameOf<T>(Expression<Func<T>> expr)
    {
        string exprString = expr.ToString();
        string name;
        
        if (!exprCache.TryGetValue(exprString, out name))
        {
            var body = expr.Body;
            
            if (body is ConstantExpression)
            {
                object value = ((ConstantExpression)body).Value;
                
                if (body.Type == typeof(Type))
                {
                    string typeName = value.ToString();
                    
                    int backqIndex = typeName.IndexOf('`');
                    if (backqIndex >= 0)
                        typeName = typeName.Substring(0, backqIndex);
                    
                    typeName = typeName.Substring(typeName.LastIndexOf('+') + 1);
                    typeName = typeName.Substring(typeName.LastIndexOf('.') + 1);
                    name = typeName;
                }
                else
                    name = value != null ? value.ToString() : (string)null;
            }
            
            else if (body is MemberExpression)
                name = ((MemberExpression)body).Member.Name;
            
            else if (body is MethodCallExpression)
                name = ((MethodCallExpression)body).Method.Name;
            
            else
                throw new ArgumentException(exprString);
            
            exprCache[exprString] = name;
        }
        return name;
    }
}

使い方は簡単。

public class Program
{
    private static void Main(string[] args)
    {
        // リテラルはそのまま出力されます。
        // nameof 演算子ではそもそもリテラルを囲うことができません。
        Console.WriteLine(Utility.NameOf(() => "magic string"));   // => "magic string"
        
        // 定数の場合はILへのコンパイル時に完全に置き換えられる為、定数名を出力できません。
        const string constString = "const string";
        Console.WriteLine(Utility.NameOf(() => constString));      // => "const string"
        
        // 型名が欲しい場合は、typeof演算子を使います。
        // 構文上の制限による苦肉の策です。
        Console.WriteLine(Utility.NameOf(() => typeof(MetaVar));   // => "MetaVar"
        Console.WriteLine(Utility.NameOf(() => typeof(Klass));     // => "Klass"
        
        // ==== ここから下は nameof 演算子と近い挙動を示します ====
        
        // 列挙型を変数に入れるとNameOfメソッドは変数名を返します。
        // これは列挙型に限らず、ローカル変数は全て変数名を返します。
        var hoge = MetaVar.Hoge;
        Console.WriteLine(Utility.NameOf(() => hoge));             // => "hoge"
        
        // 列挙型を直接指定すると列挙型の名前を返します。
        Console.WriteLine(Utility.NameOf(() => MetaVar.Fuga));     // => "Fuga"
        
        // klass はローカル変数なので、変数名を返します。
        var klass = new Klass();
        Console.WriteLine(Utility.NameOf(() => klass));            // => "klass"
        
        // プロパティの場合はプロパティ名を返します。
        Console.WriteLine(Utility.NameOf(() => klass.Number));     // => "Number"
        
        // メソッドの場合はメソッド名を返します。
        Console.WriteLine(Utility.NameOf(() => klass.Echo()));     // => "Echo"
    }
    
    public enum MetaVar { Hoge, Fuga, Piyo }
    
    public class Klass
    {
        public int Number { get; set; }
        
        public string Echo()
        {
            return "I'm a instance of Klass";
        }
    }
}

今のところArgumentExceptionが投げられる場面には遭遇してません。

本来のnameof演算子とは違ってコンパイラがよしなにするものではなく、式木の操作は動的な実行です。
定数はコンパイル時に置換されてしまっていますので、Utility.NameOfメソッドはその違いを認識できません。
そのため、Utility.NameOf(constString)nameof(constString)では実行結果が違います。

とはいえ、多少違いはあるものの、そこそこ実用できるのでいいと思います(こなみ)

そもそも C# 6.0/VB 14.0 の機能であって、.NET Framework 3.5はあんまり関係ないですね。
どちらかと言うとVisual Studioのバージョンが古いときに使えるtipsみたいな。

あと、注意点としては、式木の計算は結構重いです。
その対策として、上ではキャッシュ機構を組み込んでいます。
どれくらいキャッシュの効果があるかはベンチ取ってないので分かりませんが、対策なしは止めましょう。
INotifyPropertyChangedの実装など、比較的呼び出される頻度が高いと効いてきそうです。