如何在不将其写入 C# 中的字符串文本的情况下引用标识符
本文关键字:字符串 文本 情况下 标识符 引用 | 更新日期: 2023-09-27 18:26:53
我经常想这样做:
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.");
}
}
更新:此问题是在 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<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);
如前所述,由于方法名称位于异常的调用堆栈中,因此对异常使用此方法似乎是不必要的。
关于记录参数值问题中的另一个示例,PostSharp 似乎是一个很好的候选者,并且可能会允许许多您感兴趣的此类新功能。
看看PostSharp上的这个页面,当我搜索如何使用PostSharp记录参数值(它涵盖了(时,它出现了。摘自该页面:
您可以通过一个方面获得很多有用的信息,但有三个流行的类别:
- 代码信息:函数名、类名、参数值等。这可以帮助您减少在确定逻辑缺陷或边缘情况时的猜测
- 性能信息:跟踪方法花费的时间
异常- :捕获选择/所有异常并记录有关它们的信息
原始问题名为"如何在不将其写入 C# 中的字符串文本的情况下引用标识符?这个答案没有回答这个问题,相反,它回答了这个问题"如何使用预处理器将其名称写入字符串文字来引用标识符?
下面是一个非常简单的"概念验证"C#预处理器程序:
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>
(这是可选的 - 有关详细信息,请参阅此处 https://stackoverflow.com/a/12163384/253938。
在 .csproj 文件的末尾,将一些注释掉的行替换为以下行:
<Target Name="BeforeBuild">
<Exec WorkingDirectory="D:'Merlinia'Trunk-Debug'Common'Build Tools'Merlinia Preprocessor'VS2012 projects'StackOverflowPreprocessor'bin" Command="StackOverflowPreprocessor.exe "$(MSBuildProjectFullPath)"" />
</Target>
现在,当您重新编译测试程序时,该行显示
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));
您可以在 nameof 表达式中放置更精细的虚线名称,但这只是告诉编译器在哪里查找:只会使用最终标识符:
WriteLine(nameof(person.Address.ZipCode)); // prints "ZipCode"
注意:自预览版构建以来,名称的设计发生了一些小的变化。在预览中,不允许使用虚线表达式,如上一个示例中的 person 是作用域中的变量。相反,您必须在类型中点入。