ygormutti
ygormutti

Reputation: 357

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

I often want to do this:

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

Because if I change the name of Foo the IDE will refactor my error message too, what won't happen if I put the name of the method (or any other kind of member identifier) inside a string literal. The only way I know of implementing "name" is by using reflection, but I think the performance loss outweighs the mantainability gain and it won't cover all kinds of identifiers.

The value of the expression between parenthesis could be computed at compile time (like typeof) and optimized to become one string literal by changing the language specification. Do you think this is a worthy feature?

PS: The first example made it look like the question is related only to exceptions, but it is not. Think of every situation you may want to reference a type member identifier. You'll have to do it through a string literal, right?

Another example:

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

UPDATE: this question was posted before C# 6, but may still be relevant for those who are using previous versions of the language. If you are using C# 6 check out the nameof operator, which does pretty much the same thing as the name operator in the examples above.

Upvotes: 5

Views: 1406

Answers (5)

ygormutti
ygormutti

Reputation: 357

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.

Upvotes: 0

RenniePet
RenniePet

Reputation: 11658

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?

Upvotes: 1

xanatos
xanatos

Reputation: 111890

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

Upvotes: 6

Niall Connaughton
Niall Connaughton

Reputation: 16107

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

Upvotes: 1

Marc Gravell
Marc Gravell

Reputation: 1063338

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.

Upvotes: 11

Related Questions