在运行时通过Lambda表达式获取本地变量(和参数)的名称

20

我希望以一种能够保持重构安全的方式,在运行时获取本地变量(和参数)的名称。我有以下扩展方法:

public static string GetVariableName<T>(Expression<Func<T>> variableAccessExpression)
{
    var memberExpression = variableAccessExpression.Body as MemberExpression;
    return memberExpression.Member.Name;
}

该 lambda 表达式捕获的变量名称将通过此语法返回:

static void Main(string[] args)
{
    Console.WriteLine(GetVariableName(() => args));
    // Output: "args"

    int num = 0;
    Console.WriteLine(GetVariableName(() => num));
    // Output: "num"
}

然而,这只有在C#编译器将任何被捕获到匿名函数中的局部变量(和参数)自动提升为同名实例变量并生成一个编译器生成的类时才能起作用(参见Jon Skeet)。如果不是这种情况,将Body转换为MemberExpression会失败,因为MemberExpression表示字段或属性访问。

该变量提升是否为文档化行为,还是受其他版本框架更改的实现细节?

注意:此问题是对我以前关于参数验证的问题的概括。


你可以在这里找到替代方案:https://dev59.com/NHVD5IYBdhLWcg3wJIEK,但是这些方法都没有文档说明。使用匿名类型的方法速度更快。 - nawfal
3个回答

12

更新:自C# 6开始,引入了nameof运算符来解决这种情况(请参见MSDN),因此这不再是一个问题。

看起来我的问题的答案是;该特性未进行标准化。情况似乎比我最初预计的还要糟糕。捕获变量的提升不仅未进行标准化,将匿名函数转换为表达式树表示的整个规范也是如此。

这意味着即使是简单的匿名函数,例如下面的函数,在框架的不同实现中也不能保证产生一致的表达式树(直到转换被标准化):

Expression<Func<int, int, int>> add = (int x, int y) => x + y;

以下摘自C#语言规范4.0(在所有情况下均加粗)。
来自“4.6表达式树类型”:
引用: < p >泛型类型Expression<D>的确切定义以及当匿名函数转换为表达式树类型时构造表达式树的精确规则都超出了本规范的范围,并在其他地方进行描述。 来自“6.5.2将匿名函数转换为表达式树类型的评估”:
引用: < p >将匿名函数转换为表达式树类型会产生一个表达式树(§4.6)。 更精确地说,匿名函数转换的评估导致构造表示匿名函数本身结构的对象结构。 表达式树的精确结构以及创建它的确切过程是实现定义的。 “6.5.3实现示例”的第三个示例演示了捕获局部变量的匿名函数的转换,并确认了我问题中提到的变量晋升:
局部变量的生命周期现在必须至少延长到匿名函数委托的生命周期。这可以通过将局部变量“提升”到编译器生成的类的字段中来实现。局部变量的实例化(§7.15.5.2)对应于创建编译器生成的类的实例,而访问局部变量对应于访问编译器生成的类实例中的字段
本节末尾进一步证实了这一点: 引用编译器生成的对象可以存储在表达式树中,对局部变量的访问可以表示为对这些对象上的字段访问。这种方法的优点是它允许“提升”的局部变量在委托和表达式树之间共享。
然而,在本节开头有一个免责声明: 这里描述的实现基于微软C#编译器使用的相同原则,但这并不是强制性的实现,也不是唯一可能的实现。它仅简要提及转换为表达式树,因为其确切的语义超出了本规范的范围。

补充说明:Eric Lippert在此评论中确认,表达式树规范从未发布。在CodePlex上的DLR文档中存在一个Expression Trees v2 Spec,但其范围似乎不涵盖将C#中的匿名函数转换为表达式树。


@rally25rs:说得好。我的假设是微软确实有一个内部规范,他们遵循所有实现,但选择不承诺公开标准化。这意味着几乎可以肯定地假定在框架的其他部分(如Linq2Sql)中依赖的转换将保持一致;然而,这种保证不适用于表达式(例如我在问题中提到的读取参数名称的表达式)。 - Douglas

4

这是一种您不应该依赖的行为。

看一下滥用C# Lambda表达式还是语法辉煌?

现在阅读来自C#设计团队成员Eric Lippert的评论,其中包括:

我刚刚问Anders(和设计团队的其他成员)他们的想法。让我们说结果不能在家庭友好报纸上印出来

至于为什么这很可怕,我们可以从不明显、聪明(记住,聪明是坏的,聪明的代码难以维护)、完全不在Lambda设计者设想的使用案例范围内、缓慢、脆弱、不可移植和不必要开始

从这些声明中,我会说它不会成为一种记录或受支持的行为。


+1:这指引我找到了正确的答案方向。然而,在链接的问题中,.Attributes(style =>“width:100%”) lambda似乎只是用于外观/样式上比传统的.Attributes(“style”,“width:100%”)语法更好看,而不是因为它引入了任何其他不可用的功能,证实了Lippert的评论,即它是“不必要的”。(继续...) - Douglas
3
在我的问题中,我觉得使用lambda表达式获取成员/变量名的用例更为强大。我发现.NET将成员/变量名嵌入字符串的做法 - 例如PropertyChangedEventArgsArgumentException - 比我提出的替代方案更加可怕。 - Douglas
Prism4在其RaisePropertyChanged的Expression tree重载中使用了相同的东西。 - JKor
@JKor:感谢指引!我看过很多在线资源使用lambda表达式来实现INotifyPropertyChanged,但几乎没有用于ArgumentException的。这是可以理解的,因为这种方法似乎比针对参数/变量更不容易出错。 - Douglas
1
Eric和Anders的强硬回复,委婉地说,令人困惑。例如,C ++标准化了此捕获行为,似乎没有明显的理由不这样做。实质上,称其依赖为“不明显,脆弱,低效,不必要的hack”不仅是侮辱性的,而且事实上是错误的。它不是这些东西,它只是一个解决实际问题的方案。 - Konrad Rudolph

1
据我所知,这是一个实现细节。
然而,我认为你可以打赌它实际上不会改变。
我刚在VS2012 RC中进行了测试,它按预期工作-所以至少在未来几年内你是安全的。

1
Mono和框架的其他实现如何呢?众所周知,它们在实现细节方面会有些自由(例如,它们提供的内存模型语义比.NET Framework要宽松)。即使它们今天使用相同的实现,它们在未来的版本中可能不会这样做。 - Douglas
1
还是谢谢你关注VS2012 :-) 很高兴知道微软至少不会破坏它。 - Douglas
我不知道Mono - 你需要检查一下。这真的是一个抉择:你认为你的解决方案出错的机会比使用CheckNotNull(“args”,args)出现人为错误的机会更大吗? - Nick Butler
1
这是一个棘手的问题。我认为某些硬编码名称变得无效的机会更大(主要是随着参数自动重命名),而不是 lambda 表达式背后的实现发生变化。然而,前者的后果相对温和(ArgumentException 将显示错误的名称),而后者的后果则更为严重(所有参数名称都将无法读取)。 - Douglas
1
我同意,尽管如果lambda解决方案因编译器实现更改而出现故障,它将会出现大问题,因此很明显需要修复。在我看来,这比一些较小的错误不被注意要好。 - Nick Butler
1
你说得有道理。我的主要顾虑是Lambda表达式是一种“非传统”的读取参数名称的方法 - 所有MSDN示例都将它们的名称硬编码为字符串 - 因此,如果我要偏离常规,我需要能够证明新方法的稳健性。尽管它比传统方法更具优势,但在其他版本的框架中出现故障的可能性降低了切换到它的动力。 - Douglas

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接