反射参数名:C# lambda表达式的滥用还是语法的辉煌?

433

我正在研究MvcContrib的网格组件,对于在Grid语法中使用的一种句法技巧感到着迷,但同时也感到反感:

.Attributes(style => "width:100%")

上面的语法将生成的HTML的样式属性设置为width:100%。现在如果你注意一下,'style'并没有被明确指定。它是从表达式中参数的名称推断出来的!我不得不挖掘一下,找到了“魔法”发生的地方:

Hash(params Func<object, TValue>[] hash)
{
    foreach (var func in hash)
    {
        Add(func.Method.GetParameters()[0].Name, func(null));
    }
}

因此,实际上,该代码使用参数的编译时名称来创建属性名称-值对的字典。结果的语法结构确实非常具有表现力,但同时也非常危险。

lambda表达式的一般用法允许替换所使用的名称而不会产生副作用。我在一本书中看到了一个例子,它说 collection.ForEach(book => Fire.Burn(book)),我知道我可以在我的代码中写 collection.ForEach(log => Fire.Burn(log))意思是一样的。但在这里,使用MvcContrib Grid语法时,我突然发现,代码会根据我选择的变量名主动查找并做出决策!

那么,这是C# 3.5/4.0社区和lambda表达式爱好者常见的做法吗?还是一个流氓的技巧,我不必担心?


34
只要你愿意关注代码的意图而不仅仅是解析语法,我认为这看起来很明显。如果你在阅读良好的代码,那么这本来就是你应该做的。语法只是表达意图的工具,我认为这是一段能够展现意图的代码。 - Jamie Penney
193
我刚刚问了Anders(和设计团队的其他成员)他们的想法。简单来说,结果不能在适合家庭的报纸上公开。 - Eric Lippert
22
目前,C#缺乏一种简洁、轻便的语法用于创建映射表,特别是传递给函数的映射表。各种动态语言(如Ruby、Python、JavaScript和ColdFusion 9)都有一种简洁、轻便的语法来实现这个功能。 - yfeldblum
28
哦,我觉得它看起来很可爱。至于地图的语法,如果我们可以这样做:new { {"Do", "A deer" } , {"Re", "golden sun"} ... } 并且编译器能够推断出地图的构造方式,就像 new[] {1, 2,3 } 能够推断出整数数组的构造方式一样,那就太棒了。我们正在考虑这些事情。 - Eric Lippert
19
Eric Lippert,我非常尊重您,但是我认为您和团队(包括我非常尊敬的Anders)对此过于严厉。正如您所承认的那样,C#缺乏用于映射的紧凑语法,而其他一些语言(如Ruby)则具有出色的语法。这个人找到了一种获得他想要语法的方法。我承认,有类似的表达方式,可以用更少的缺点几乎与他的语法一样具有表现力。但是他的语法以及他努力工作的事实明确表明了一种语言增强的需求。过去的表现表明,您们将为它创造出色的东西。 - Charlie Flowers
显示剩余21条评论
21个回答

153

我觉得这很奇怪,不是因为名称的原因,而是因为lambda表达式是不必要的;它可以使用匿名类型并更加灵活:

.Attributes(new { style = "width:100%", @class="foo", blip=123 });

这是在许多ASP.NET MVC中使用的一种模式(例如),并且有其他用途(注意一个警告,同时也请注意Ayende的想法,如果名称是魔术值而非调用者特定)


4
这也存在互操作性问题;并非所有语言都支持在运行时创建匿名类型。 - Brian
5
我喜欢没人真正回答问题,而是提供了一个“这更好”的论点。:p 这算滥用吗? - Sam Saffron
7
我很想看到Eric Lippert对此的反应,因为这是“框架代码”,而且它同样糟糕。 - Matt Hinze
26
我认为问题不在于代码编写后的易读性。我认为真正的问题是代码的可学习性。当你的Intellisense显示*.Attributes(object obj)*时,你会怎么想?你将不得不阅读文档(这是没有人愿意做的),因为你不知道要传递什么参数给该方法。我认为这并不比问题中的例子好到哪里去。 - NotDan
2
@Arnis - 为什么更灵活:它不依赖于隐含的参数名称,这可能会导致一些lambda实现(其他语言)出现问题(不要引用我)-但您也可以使用具有定义属性的常规对象。例如,您可以拥有一个HtmlAttributes类,其中包含预期的属性(用于智能感知),并忽略那些具有null值的属性... - Marc Gravell
显示剩余3条评论

149

这种方法的互操作性很差。例如,考虑以下 C# - F# 示例:

C#:

public class Class1
{
    public static void Foo(Func<object, string> f)
    {
        Console.WriteLine(f.Method.GetParameters()[0].Name);
    }
}

F#:

Class1.Foo(fun yadda -> "hello")

结果:

输出了"arg"(而不是"yadda")。

因此,库的设计者应该避免这种‘滥用’,或者至少在他们希望在 .Net 语言之间具有良好的互操作性时提供一个‘标准’重载(例如,带有字符串名称作为额外参数的重载)。


11
不行。这个策略无法移植。举一个反例,F#可以重载仅在返回类型上不同的方法(类型推断可以做到这一点)。这可以在CLR中表达。但是,F#禁止这样做,主要是因为如果你这样做,这些API将无法从C#调用。当涉及互操作性时,“边缘”功能总会有权衡取舍,即您获得哪些好处与您交换了哪些互操作性。 - Brian
32
我认为非互操作性不应成为阻止做某事的理由。如果需要实现互操作性,那就去做;如果不需要,为什么要担心呢?在我看来,这是YAGNI(You Ain't Gonna Need It)原则。 - John Farrell
15
在.NET CLR领域中,互操作性具有全新的维度,因为在任何编译器生成的程序集都应该能够被任何其他语言所使用。 - Remus Rusanu
25
我同意你不必遵循CLS规范,但如果你要编写一个库或控件(而且这段代码是来自表格,对吗?),遵循规范似乎是个好主意。否则,你只是在限制你的受众/客户群。 - JMarsch
4
也许将 Func<object, string> 改为 Expression<<Func<object, string>> 可能更值得一试。如果你将表达式的右侧限制为只是常量,你可以使用以下实现方式:public static IDictionary<string, string> Hash(params Expression<Func<object, string>>[] hash) { Dictionary<string, string> values = new Dictionary<string,string>(); foreach (var func in hash) { values[func.Parameters[0].Name] = (string)((ConstantExpression)func.Body).Value; } return values; } - davidfowl
显示剩余6条评论

137

我只是想发表一下我的观点(我是MvcContrib网格组件的作者)。

这绝对是语言滥用 - 毫无疑问。然而,我不认为这是反直觉的 - 当你看到一个调用 Attributes(style => "width:100%", @class => "foo")时,我认为很明显正在发生什么(它肯定不比匿名类型更糟糕)。从智能提示的角度来看,我同意它相当晦涩。

对于那些有兴趣的人,这里提供一些关于其在MvcContrib中使用的背景信息...

我将此添加到网格中作为个人偏好 - 我不喜欢将匿名类型用作字典(具有接受“object”的参数与接受params Func[]的参数一样难以理解),Dictionary集合初始化程序也相当冗长(我也不喜欢冗长的流畅界面,例如必须链接多个调用以获得Attribute("style", "display:none").Attribute("class", "foo")等)。

如果C#有一个更简洁的字典文字语法,那么我就不会费心在网格组件中包含这种语法 :)

我还要指出,在MvcContrib中使用这个是完全可选的 - 这些是扩展方法,它们包装了接受IDictionary的重载。我认为如果你提供这样的方法,你还应该支持更“正常”的方法,例如与其他语言进行交互。

此外,有人提到“反射开销”,我只想指出,使用这种方法实际上没有太多开销 - 没有运行时反射或表达式编译(请参见http://blog.bittercoder.com/PermaLink,guid,206e64d1-29ae-4362-874b-83f5b103727f.aspx)。


2
我也尝试在我的博客上更深入地解决这里提出的一些问题:http://www.jeremyskinner.co.uk/2009/12/02/lambda-abuse-the-mvccontrib-hash/ - Jeremy Skinner
4
在Intellisense中,它与匿名对象一样难以理解。 - Brad Wilson
23
感谢提到接口是通过可选的扩展方法添加的,非C#用户(以及对语言滥用感到不满的人)可以选择不使用它。 - Jørn Schou-Rode

49

我更喜欢

Attributes.Add(string name, string value);

使用lambda表达式并没有什么好处,而且使用明确和标准的方法更为合适。


20
“Is it though?”的意思是“真的吗?”。html.Attributes.Add("style", "width:100%");这行代码不太好读,而style = "width:100%"(实际生成的HTML)则更简洁明了。如果使用style => "width:100%"则非常接近最终生成的HTML结果。 - Jamie Penney
6
他们的语法允许像.Attributes(id=>'foo', @class=>'bar', style=>'width:100%')这样的技巧。该函数的签名使用params语法来处理可变数量的参数:Attributes(params Func<object, object>[] args)。它非常强大,但是我花费了相当长的时间才理解它到底在干什么。 - Remus Rusanu
27
将C#代码看起来像HTML代码只是设计决策的一个不好的原因。这两种语言完全不同,用途也完全不同,它们不应该看起来相同。 - Guffa
18
可以使用匿名对象,不会影响代码的“美观性”?.Attributes(new {id = "foo", @class = "bar", style = "width:100%"}) ?? - Funka
10
为什么设计决策的原因会是不好的?它们为什么不应该看起来一样?按照这种推理,它们应该故意看起来不同吗?我不是说你错了,只是建议你更详细地阐述你的观点。 - Samantha Branham
显示剩余3条评论

45

欢迎来到Rails Land :)

只要你知道发生了什么,这样做就没有任何问题。(当这种事情没有很好记录时才会有问题)。

Rails框架的全部建立在约定优于配置的思想上。用特定的方式命名东西,让你遵循他们使用的约定,并且能免费获得很多功能。遵循命名约定可以让你更快地到达目的地。整个系统运作得非常出色。

我看到另一个类似的技巧出现在Moq中的方法调用断言中。你传递一个lambda表达式,但该lambda表达式实际上从未被执行。他们只是使用这个表达式来确保方法调用发生并在没有发生时抛出异常。


3
我有些犹豫,但我同意。除了使用字符串作为Add()函数参数和使用lambda表达式参数名称之间的反射开销之外,我想不出其他显著区别。无论哪种方式,你都可能会因疏忽而打错单词“style”。 - Samantha Branham
5
我当时想不通为什么这对我来说不奇怪,然后我想起了Rails。 :D - Allyn

42

这个东西在多个方面都很糟糕。不,这和 Ruby 毫不相似。这是对 C# 和 .NET 的滥用。

已经有很多建议以更简单的方式完成此任务:元组、匿名类型、流畅接口等等。

这个东西为什么如此糟糕呢?因为它太花哨了:

  • 当你需要从 Visual Basic 中调用它时会发生什么?

    .Attributes(Function(style) "width:100%")

  • 它完全违反直觉,智能感知提供的帮助非常少。

  • 它效率低下。

  • 没有人知道如何维护它。

  • attributes 参数的类型是什么?是 Func<object,string> 吗?这如何表达意图?智能感知文档会说什么,“请忽略所有 object 的值”?

我认为你完全可以有这种厌恶感。


4
我会说 - 它完全是直觉的。 :) - Arnis Lapsa
4
你说它与 Ruby 完全不同。但是它在指定哈希表的键和值的语法方面非常类似于 Ruby。 - Charlie Flowers
3
在alpha-conversion下崩溃的代码!耶! - Phil
3
@Charlie,从句法上看它们很相似,但从语义上看它们是截然不同的。 - Sam Saffron

38

我属于“语法辉煌”阵营,只要他们将其清晰地记录下来,并且看起来非常酷,这里面几乎没有任何问题,就我个人而言!


11
阿门,兄弟。阿门。(第二个“阿门”是为了满足评论长度要求 :)) - Charlie Flowers
你的评论已经足够了,但是你不能只点赞一次然后再加上你的评论 :D - Sleeper Smith

37

两者都是问题,这是对lambda表达式的错误使用以及语法的不当运用。


17
所以这是对lambda表达式的一个巧妙的语法误用?我想我同意 :) - Seth Petry-Johnson

21

我很少遇到这种用法,我认为它是“不适当的” :)

这不是一种常见的用法,与通常的约定不一致。当然,这种语法有利有弊:

劣势

  • 代码不直观(通常的约定不同)。
  • 它倾向于脆弱(更改参数名称会破坏功能)。
  • 测试略微困难(模拟API需要在测试中使用反射)。
  • 如果表达式被频繁使用,那么由于需要分析参数而不仅仅是值(反射成本),它将变慢。

优势

  • 在开发人员适应这种语法后,它更易读。

最终结论 - 在公共API设计中,我会选择更明确的方式。


2
@Elisha - 你的优缺点被颠倒了。我希望你不是在说编码“不直观”是优点。;-) - Metro Smurf
对于这种情况来说 - lambda 参数名和字符串参数都很脆弱。这就像在 XML 解析中使用动态类型一样 - 因为你无法确定 XML 的结构。 - Arnis Lapsa

19

不,这绝对不是常规做法。它是违反直觉的,没有办法仅通过查看代码来弄清楚它的作用。您必须知道它的使用方式才能理解它的用途。

相比提供一个委托数组来设置属性,链式调用方法会更加清晰且性能更好:

.Attribute("style", "width:100%;").Attribute("class", "test")

虽然这样打字略微繁琐,但很清晰易懂。


6
真的吗?当我看到那段代码片段时,我完全知道它的意图。除非您非常严格,否则它并不难懂。对于字符串连接重载+符号,可以给出同样的论点,即我们应该总是使用Concat()方法,而不是+符号。 - Samantha Branham
4
@Stuart:不,你并不确切知道,你只是基于使用的数值在猜测。任何人都可以做出这样的猜测,但猜测并不是理解代码的好方法。 - Guffa
11
我猜使用.Attribute("style", "width:100%")会给我 style="width:100%",但我不确定它是否会给我foooooo。我没有看出两者的区别。 - Samantha Branham
5
查看代码时,“基于使用的值进行猜测”始终是您所做的。如果遇到stream.close()的调用,您会假设它关闭了流,但它实际上可能完全不同。 - Wouter Lievens
1
@Wouter:如果你在读代码时总是猜测,那么你一定很难读懂代码... 如果我看到一个名为“close”的方法调用,我可以推断出类的作者不知道命名约定,因此我会非常谨慎地对该方法的作用做任何假设。 - Guffa

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