How to refer to an identifier without writing it into a string literal in C#?

前端 未结 5 793
甜味超标
甜味超标 2020-12-18 06:45

I often want to do this:

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


        
相关标签:
5条回答
  • 2020-12-18 07:18

    As has been covered, using this approach for exceptions seems unnecessary due to the method name being in the call stack on the exception.

    In relation to the other example in the question of logging the parameter value, it seems PostSharp would be a good candidate here, and probably would allow lots of new features of this kind that you're interested in.

    Have a look at this page on PostSharp which came up when I searched for how to use PostSharp to log parameter values (which it covers). An excerpt taken from that page:

    You can get a lot of useful information with an aspect, but there are three popular categories:

    • Code information: function name, class name, parameter values, etc. This can help you to reduce guessing in pinning down logic flaws or edge-case scenarios
    • Performance information: keep track of how much time a method is taking
    • Exceptions: catch select/all exceptions and log information about them
    0 讨论(0)
  • 2020-12-18 07:19

    Version 6 of C# has introduced the nameof operator which works like the name operator described in the examples of the question, but with some restrictions. Here are some examples and excerpts from the C# FAQ blog:

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

    You can put more elaborate dotted names in a nameof expression, but that’s just to tell the compiler where to look: only the final identifier will be used:

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

    Note: there are small design changes to nameof since the Preview was built. In the preview, dotted expressions like in the last example, where person is a variable in scope, are not allowed. Instead you have to dot in through the type.

    0 讨论(0)
  • 2020-12-18 07:22

    The original question is named "How to refer to an identifier without writing it into a string literal in C#?" This answer does not answer that question, instead, it answers the question "How to refer to an identifier by writing its name into a string literal using a preprocessor?"

    Here is a very simple "proof of concept" C# preprocessor program:

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

    And here's a test file, based on the first half of the original question:

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

    To make the running of the preprocessor program a part of the build process you modify the .csproj file in two places. Insert this line in the first section:

    <UseHostCompilerIfAvailable>false</UseHostCompilerIfAvailable>
    

    (This is optional - see here https://stackoverflow.com/a/12163384/253938 for more information.)

    And at the end of the .csproj file replace some lines that are commented-out with these lines:

      <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>
    

    Now when you recompile the test program the line that says

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

    will be magically converted to say

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

    OK?

    0 讨论(0)
  • 2020-12-18 07:26

    well, you could cheat and use something like:

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

    and:

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

    Here, all the work is done by the compiler (at compile-time), so if you rename the method it will immediately return the correct thing.

    0 讨论(0)
  • 2020-12-18 07:41

    If you simply want the current method name: MethodBase.GetCurrentMethod().Name

    If it's a type typeof(Foo).Name

    If you want the name of a variable/parameter/field/property, with a little Expression tree

    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);
    

    For method names it's a little more tricky:

    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;
    }
    

    and when you call them you have to specify the type of a compatible delegate (Action, Action<...>, Func<...> ...)

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

    or more simply, without using expressions :-)

    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);
    
    0 讨论(0)
提交回复
热议问题