如何在不将其写入C#中的字符串文字的情况下引用标识符?

我经常想这样做:

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

因为如果我更改了Foo的名称,IDE也会重构我的错误消息,如果我将方法的名称(或任何其他类型的成员标识符)放在字符串文字中,将不会发生什么。 我知道实现“名称”的唯一方法是使用reflection,但我认为性能损失超过了可持续性增益,并且它不会涵盖所有类型的标识符。

括号之间的表达式的值可以在编译时计算(如typeof),并通过更改语言规范进行优化以成为一个字符串文字。 你认为这是一个有价值的function吗?

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运算符完全相同。

好吧,你可以欺骗和使用类似的东西:

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

和:

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

在这里,所有工作都由编译器完成(在编译时),因此如果重命名方法,它将立即返回正确的东西。

如果您只想要当前的方法名称: MethodBase.GetCurrentMethod().Name

如果它是类型typeof(Foo).Name

如果你想要一个变量/参数/字段/属性的名称,使用一个小的Expression

 public static string GetFieldName(Expression> 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(Expression> 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; } 

当你调用它们时,你必须指定兼容的委托的类型( ActionAction<...>Func<...> …)

 string str5 = GetMethodName(() => Main); string str6 = GetMethodName>(() => Method1); string str7 = GetMethodName>(() => Method2); 

或者更简单,不使用表达式:-)

 public static string GetMethodName(Delegate del) { return del.Method.Name; } string str8 = GetMethodName((Action)Main); string str9 = GetMethodName((Func)Method1); string str10 = GetMethodName((Func)Method2); 

如前所述,由于方法名称位于exception的调用堆栈中,因此使用此方法进行exception似乎是不必要的。

关于记录参数值的问题中的另一个例子,似乎PostSharp在这里是一个很好的候选者,并且可能会允许你感兴趣的许多这类新function。

看看PostSharp上的这个页面,当我搜索如何使用PostSharp来记录参数值(它涵盖的内容)时出现了这个页面 。 摘自该页面的摘录:

您可以通过一个方面获得许多有用的信息,但有三种流行的类别:

  • 代码信息:函数名称,类名,参数值等。这可以帮助您减少猜测固定逻辑缺陷或边缘情况
  • 性能信息:跟踪方法花费的时间
  • 例外:捕获select / allexception并记录有关它们的信息

最初的问题被命名为“如何引用标识符而不将其写入C#中的字符串文字?” 这个答案没有回答这个问题,相反,它回答了“如何通过使用预处理器将其名称写入字符串文字来引用标识符?”的问题。

这是一个非常简单的“概念certificate”C#预处理器程序:

 using System; using System.IO; namespace StackOverflowPreprocessor { ///  /// 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. ///  public class PreprocessorProgram { ///  /// The Main() method is where it all starts, of course. ///  /// must be one argument, the full name of the .csproj file /// 0 = OK, 1 = error (error message has been written to console) 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  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 } ///  /// 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. ///  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; } ///  /// Method to display an error message on the console. ///  private static void DisplayError(string errorText) { Console.WriteLine("Preprocessor: " + errorText); } ///  /// Method to display an error message on the console. ///  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文件。 在第一部分插入此行:

 false 

(这是可选的 – 有关详细信息,请参阅https://stackoverflow.com/a/12163384/253938 。)

并在.csproj文件的末尾替换一些用这些行注释掉的行:

     

现在,当您重新编译测试程序时,该行说明了

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

将神奇地转换成说

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

好?

C#的第6版引入了nameof运算符,它的工作方式类似于问题示例中描述的name运算符,但有一些限制。 以下是C#FAQ博客的一些示例和摘录:

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

您可以在表达式名称中添加更精细的点名称,但这只是告诉编译器在哪里查看:只使用最终标识符:

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

注意:自构建预览版以来,nameof有一些小的设计更改。 在预览中,不允许使用上一个示例中的虚线表达式,其中person是范围中的变量。 相反,你必须通过类型点。