如何在不将其写入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; }
当你调用它们时,你必须指定兼容的委托的类型( Action
, Action<...>
, 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是范围中的变量。 相反,你必须通过类型点。