5

私はしばしばこれをしたい:

public void Foo(Bar arg)
{
  throw new ArgumentException("Argument is incompatible with " + name(Foo));
}

Foo の名前を変更すると、IDE はエラー メッセージもリファクタリングするため、メソッドの名前 (またはその他の種類のメンバー識別子) を文字列リテラル内に配置するとどうなるかはわかりません。「名前」を実装する唯一の方法はリフレクションを使用することですが、パフォーマンスの低下は保守性の向上を上回り、すべての種類の識別子をカバーすることはできないと思います。

括弧内の式の値は、コンパイル時に (typeof と同様に) 計算され、言語仕様を変更することで 1 つの文字列リテラルになるように最適化できます。これは価値のある機能だと思いますか?

PS:最初の例では、質問が例外のみに関連しているように見えましたが、そうではありません。型メンバー識別子を参照する可能性があるあらゆる状況を考えてみてください。文字列リテラルを介して行う必要がありますよね?

もう一つの例:

[RuntimeAcessibleDocumentation(Description="The class " + name(Baz) +
  " does its job. See method " + name(DoItsJob) + " for more info.")]
public class Baz
{
  [RuntimeAcessibleDocumentation(Description="This method will just pretend " +
    "doing its job if the argument " + name(DoItsJob.Arguments.justPretend) +
    " is true.")]
  public void DoItsJob(bool justPretend) 
  {
    if (justPretend)
      Logger.log(name(justPretend) + "was true. Nothing done.");
  }
}

更新:この質問は C# 6 より前に投稿されましたが、以前のバージョンの言語を使用しているユーザーにはまだ関連している可能性があります。C# 6 を使用している場合は、演算子を確認してください。これは、上記の例nameofの演算子とほとんど同じことを行います。name

4

5 に答える 5

11

まあ、あなたはチートして次のようなものを使うことができます:

public static string CallerName([CallerMemberName]string callerName = null)
{
    return callerName;
}

と:

public void Foo(Bar arg)
{
  throw new ArgumentException("Argument is incompatible with " + CallerName());
}

ここでは、すべての作業はコンパイラによって (コンパイル時に) 行われるため、メソッドの名前を変更すると、すぐに正しいものが返されます。

于 2013-08-08T07:07:06.190 に答える
6

単に現在のメソッド名が必要な場合:MethodBase.GetCurrentMethod().Name

タイプだとtypeof(Foo).Name

変数/パラメーター/フィールド/プロパティの名前が必要な場合は、小さなExpressionツリーを使用します

public static string GetFieldName<T>(Expression<Func<T>> exp)
{
    var body = exp.Body as MemberExpression;

    if (body == null)
    {
        throw new ArgumentException();
    }

    return body.Member.Name;
}

string str = "Hello World";
string variableName = GetFieldName(() => str);

メソッド名については、もう少しトリッキーです。

public static readonly MethodInfo CreateDelegate = typeof(Delegate).GetMethod("CreateDelegate", BindingFlags.Static | BindingFlags.Public, null, new[] { typeof(Type), typeof(object), typeof(MethodInfo) }, null);

public static string GetMethodName<T>(Expression<Func<T>> exp)
{
    var body = exp.Body as UnaryExpression;

    if (body == null || body.NodeType != ExpressionType.Convert)
    {
        throw new ArgumentException();
    }

    var call = body.Operand as MethodCallExpression;

    if (call == null)
    {
        throw new ArgumentException();
    }

    if (call.Method != CreateDelegate)
    {
        throw new ArgumentException();
    }

    var method = call.Arguments[2] as ConstantExpression;

    if (method == null)
    {
        throw new ArgumentException();
    }

    MethodInfo method2 = (MethodInfo)method.Value;

    return method2.Name;
}

それらを呼び出すときは、互換性のあるデリゲートのタイプを指定する必要があります ( Action, Action<...>, Func<...>...)

string str5 = GetMethodName<Action>(() => Main);
string str6 = GetMethodName<Func<int>>(() => Method1);
string str7 = GetMethodName<Func<int, int>>(() => Method2);

またはもっと簡単に、式を使用せずに:-)

public static string GetMethodName(Delegate del)
{
    return del.Method.Name;
}

string str8 = GetMethodName((Action)Main);
string str9 = GetMethodName((Func<int>)Method1);
string str10 = GetMethodName((Func<int, int>)Method2);
于 2013-08-08T07:06:30.743 に答える
1

元の質問は、「C# で文字列リテラルに書き込まずに識別子を参照する方法は?」という名前です。この回答はその質問には答えません。代わりに、「プリプロセッサを使用して名前を文字列リテラルに書き込んで識別子を参照する方法は?」という質問に答えます。

以下は、非常に単純な「概念実証」C# プリプロセッサ プログラムです。

using System;
using System.IO;

namespace StackOverflowPreprocessor
{
   /// <summary>
   /// This is a C# preprocessor program to demonstrate how you can use a preprocessor to modify the 
   /// C# source code in a program so it gets self-referential strings placed in it.
   /// </summary>
   public class PreprocessorProgram
   {
      /// <summary>
      /// The Main() method is where it all starts, of course. 
      /// </summary>
      /// <param name="args">must be one argument, the full name of the .csproj file</param>
      /// <returns>0 = OK, 1 = error (error message has been written to console)</returns>
      static int Main(string[] args)
      {
         try
         {
            // Check the argument
            if (args.Length != 1)
            {
               DisplayError("There must be exactly one argument.");
               return 1;
            }

            // Check the .csproj file exists
            if (!File.Exists(args[0]))
            {
               DisplayError("File '" + args[0] + "' does not exist.");
               return 1;
            }

            // Loop to process each C# source file in same folder as .csproj file. Alternative 
            //  technique (used in my real preprocessor program) is to read the .csproj file as an 
            //  XML document and process the <Compile> elements.
            DirectoryInfo directoryInfo = new DirectoryInfo(Path.GetDirectoryName(args[0]));
            foreach (FileInfo fileInfo in directoryInfo.GetFiles("*.cs"))
            {
               if (!ProcessOneFile(fileInfo.FullName))
                  return 1;
            }
         }
         catch (Exception e)
         {
            DisplayError("Exception while processing .csproj file '" + args[0] + "'.", e);
            return 1;
         }

         Console.WriteLine("Preprocessor normal completion.");
         return 0; // All OK
      }


      /// <summary>
      /// Method to do very simple preprocessing of a single C# source file. This is just "proof of 
      /// concept" - in my real preprocessor program I use regex and test for many different things 
      /// that I recognize and process in one way or another.
      /// </summary>
      private static bool ProcessOneFile(string fileName)
      {
         bool fileModified = false;
         string lastMethodName = "*unknown*";
         int i = -1, j = -1;

         try
         {
            string[] sourceLines = File.ReadAllLines(fileName);
            for (int lineNumber = 0; lineNumber < sourceLines.Length - 1; lineNumber++)
            {
               string sourceLine = sourceLines[lineNumber];

               if (sourceLine.Trim() == "//?GrabMethodName")
               {
                  string nextLine = sourceLines[++lineNumber];
                  j = nextLine.IndexOf('(');
                  if (j != -1)
                     i = nextLine.LastIndexOf(' ', j);
                  if (j != -1 && i != -1 && i < j)
                     lastMethodName = nextLine.Substring(i + 1, j - i - 1);
                  else
                  {
                     DisplayError("Unable to find method name in line " + (lineNumber + 1) + 
                                  " of file '" + fileName + "'.");
                     return false;
                  }
               }

               else if (sourceLine.Trim() == "//?DumpNameInStringAssignment")
               {
                  string nextLine = sourceLines[++lineNumber];
                  i = nextLine.IndexOf('\"');
                  if (i != -1 && i != nextLine.Length - 1)
                  {
                     j = nextLine.LastIndexOf('\"');
                     if (i != j)
                     {
                        sourceLines[lineNumber] = 
                                    nextLine.Remove(i + 1) + lastMethodName + nextLine.Substring(j);
                        fileModified = true;
                     }
                  }
               }
            }

            if (fileModified)
               File.WriteAllLines(fileName, sourceLines);
         }
         catch (Exception e)
         {
            DisplayError("Exception while processing C# file '" + fileName + "'.", e);
            return false;
         }

         return true;
      }


      /// <summary>
      /// Method to display an error message on the console. 
      /// </summary>
      private static void DisplayError(string errorText)
      {
         Console.WriteLine("Preprocessor: " + errorText);
      }


      /// <summary>
      /// Method to display an error message on the console. 
      /// </summary>
      internal static void DisplayError(string errorText, Exception exceptionObject)
      {
         Console.WriteLine("Preprocessor: " + errorText + " - " + exceptionObject.Message);
      }
   }
}

元の質問の前半に基づいたテスト ファイルを次に示します。

using System;

namespace StackOverflowDemo
{
   public class DemoProgram
   {
      public class Bar
      {}


      static void Main(string[] args)
      {}


      //?GrabMethodName
      public void Foo(Bar arg)
      {
         //?DumpNameInStringAssignment
         string methodName = "??";  // Will be changed as necessary by preprocessor

         throw new ArgumentException("Argument is incompatible with " + methodName);
      }
   }
}

プリプロセッサ プログラムの実行をビルド プロセスの一部にするには、.csproj ファイルの 2 つの場所を変更します。最初のセクションに次の行を挿入します。

<UseHostCompilerIfAvailable>false</UseHostCompilerIfAvailable>

(これはオプションです。詳細については、 https://stackoverflow.com/a/12163384/253938を参照してください。)

そして、.csproj ファイルの最後で、コメントアウトされているいくつかの行を次の行に置き換えます。

  <Target Name="BeforeBuild">
    <Exec WorkingDirectory="D:\Merlinia\Trunk-Debug\Common\Build Tools\Merlinia Preprocessor\VS2012 projects\StackOverflowPreprocessor\bin" Command="StackOverflowPreprocessor.exe &quot;$(MSBuildProjectFullPath)&quot;" />
  </Target>

テストプログラムを再コンパイルすると、

     string methodName = "??";  // Will be changed as necessary by preprocessor

言うように魔法のように変換されます

     string methodName = "Foo";  // Will be changed as necessary by preprocessor

わかった?

于 2013-08-10T02:51:52.617 に答える
1

既に説明したように、メソッド名が例外のコール スタックにあるため、例外に対してこのアプローチを使用する必要はないようです。

パラメータ値を記録するという問題の他の例に関連して、ここでは PostSharp が適切な候補であり、おそらく、あなたが興味を持っているこの種の多くの新機能を可能にするでしょう.

PostSharpを使用してパラメーター値をログに記録する方法を検索したときに表示された PostSharp のこのページをご覧ください(これについて説明しています)。そのページからの抜粋:

アスペクトで多くの有用な情報を得ることができますが、人気のあるカテゴリが 3 つあります。

  • コード情報: 関数名、クラス名、パラメーター値など。これにより、ロジックの欠陥や特殊なシナリオを特定する際の推測を減らすことができます。
  • パフォーマンス情報: メソッドの所要時間を追跡する
  • 例外: 選択/すべての例外をキャッチし、それらに関する情報をログに記録します
于 2013-08-08T07:20:43.060 に答える
0

C# のバージョン 6 では、質問の例で説明されている演算子nameofのように機能する演算子が導入さnameれましたが、いくつかの制限があります。C# FAQ ブログからの例と抜粋を次に示します。

(if x == null) throw new ArgumentNullException(nameof(x));

より精巧なドット付きの名前を nameof 式に入れることができますが、それは単にコンパイラにどこを見ればよいかを伝えるためのものです: 最終的な識別子だけが使用されます:

WriteLine(nameof(person.Address.ZipCode)); // prints "ZipCode"

注: プレビューがビルドされてから、nameof に小さな設計変更があります。プレビューでは、最後の例のように、 person がスコープ内の変数であるドット付き式は許可されていません。代わりに、型にドットを挿入する必要があります。

于 2015-01-09T19:43:09.707 に答える