Как ссылаться на идентификатор, не записывая его в строковый литерал в С#?

Я часто хочу это сделать:

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

Потому что, если я изменю имя Foo, среда IDE также будет реорганизовывать мое сообщение об ошибке, что не произойдет, если я поместил имя метода (или любого другого типа <элемент > участника) внутри строковый литерал. Единственный способ, которым я знаю реализацию "имени", - использовать рефлексию, но я думаю, что потеря производительности перевешивает коэффициент полезного действия и не будет охватывать все типы идентификаторов.

Значение выражения между скобками может быть вычислено во время компиляции (например, typeof) и оптимизировано для того, чтобы стать одним строковым литералом, изменив спецификацию языка. Считаете ли вы, что это достойная функция?

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.");
  }
}

ОБНОВЛЕНИЕ: этот вопрос был отправлен до С# 6, но может по-прежнему иметь значение для тех, кто использует предыдущие версии языка. Если вы используете С# 6, обратитесь к оператору nameof, который в приведенных выше примерах выполняет почти то же самое, что и оператор name.

Ответ 1

В версии 6 С# введен оператор nameof, который работает как оператор name, описанный в примерах вопроса, но с некоторыми ограничениями. Вот несколько примеров и выдержки из С# часто задаваемых вопросов:

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

Вы можете помещать более сложные точечные имена в выражение expression, но это просто сказать компилятору, где искать: будет использоваться только конечный идентификатор:

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

Примечание: с момента создания предварительного просмотра есть небольшие изменения в дизайне. В предпросмотре пунктирные выражения, подобные в последнем примере, где человек является переменной в области видимости, недопустимы. Вместо этого вам нужно прорисовать тип.

Ответ 2

ну, можно обмануть и использовать что-то вроде:

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

и

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

Здесь вся работа выполняется компилятором (во время компиляции), поэтому, если вы переименуете метод, он немедленно вернет правильную вещь.

Ответ 3

Если вы просто хотите имя текущего метода: MethodBase.GetCurrentMethod().Name

Если это тип typeof(Foo).Name

Если вам нужно имя свойства variable/parameter/field/с небольшим деревом 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);

Ответ 4

Как было описано, использование этого подхода для исключений кажется ненужным из-за того, что имя метода находится в стеке вызовов в исключении.

В отношении другого примера в вопросе регистрации значения параметра кажется, что PostSharp был бы хорошим кандидатом здесь и, вероятно, позволил бы многим новым функциям такого типа, которые вас интересуют.

Посмотрите эту страницу на PostSharp, которая появилась, когда я искал, как использовать PostSharp для регистрации значений параметров (которые он охватывает), Выдержка из этой страницы:

Вы можете получить много полезной информации с аспектом, но есть три популярные категории:

  • Информация о кодах: имя функции, имя класса, значения параметров и т.д. Это может помочь вам уменьшить угадывание в выявлении логических ошибок или сценариев крайних случаев.
  • Информация о производительности: отслеживайте, сколько времени принимает метод.
  • Исключения: выбор catch/все исключения и информация о журнале о них

Ответ 5

Оригинальный вопрос называется "Как ссылаться на идентификатор, не записывая его в строковый литерал в С#?" Этот ответ не отвечает на этот вопрос, вместо этого он отвечает на вопрос "Как обратиться к идентификатору, записав его имя в строковый литерал с помощью препроцессора?"

Вот очень простое "доказательство концепции" препроцессора С#:

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 в двух местах. Вставьте эту строку в первый раздел:

<UseHostCompilerIfAvailable>false</UseHostCompilerIfAvailable>

(Это необязательно - см. здесь fooobar.com/info/16789/... для получения дополнительной информации.)

И в конце файла .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

OK