反映参数名称:滥用C#lambda表达式还是语法光彩?


428

我正在看MvcContrib Grid组件,我被Grid语法中使用的句法把戏迷住了,但同时又被它击退了:

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

上面的语法将生成的HTML的style属性设置为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
我认为只要您愿意看一下代码的意图,而不是仅仅解析语法,这看起来就很明显了。如果您正在阅读优质的代码,那么无论如何您应该这样做。语法只是意图的工具,我认为这是意图揭示代码。
杰米·彭尼

190
我只是问过安德斯(以及其他设计团队)他们的想法。只能说结果无法在家庭友好的报纸上打印。
埃里克·利珀特

22
C#当前缺少映射的简洁语法,尤其是传递给函数的映射。各种动态语言(Ruby,Python,JavaScript,ColdFusion 9)对此具有某种简洁明了的语法,无论在某种程度上还是其他方面。
yfeldblum

28
哦,我觉得这很愉快。至于地图的语法,是的,如果我们可以做新的{{“ Do”,“ A deer”},{“ Re”,“ golden sun”} ...}并让编译器推断映射的构造,就像new [] {1,2,3}推断int数组的构造一样。我们正在考虑这些事情。
Eric Lippert

17
埃里克·利珀特(Eric Lippert),我非常尊敬您,但我认为您和整个小组(包括安德斯,我非常尊敬FREAKIN')都对此严厉抨击。如您所知,C#缺乏针对地图的严格语法,而其他一些lang(例如Ruby)也有出色的语法。这个家伙找到了一种获取他想要的语法的方法。我将授予您类似的表达方式,这些表达方式几乎与他的语法一样具有较少的缺点。但是他的语法以及他为获得它而努力工作的事实清楚地表明了对语言增强的需求。过去的表现表明你们将为此创造伟大的东西。
Charlie Flowers

Answers:


147

互操作性差。例如,考虑以下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#不允许这样做,主要是因为如果这样做时,就无法从C#中调用这些API。)在互操作性方面,“边缘”功能始终存在权衡取舍,即您所获得的收益与互操作性之间的权衡。
布赖恩

32
我不喜欢将非互操作性作为不做某事的原因。如果需要互操作性,那么就去做,如果不是,那么为什么要担心呢?这是YAGNI IMHO。
约翰·法瑞尔

15
@jfar:在.NET CLR中,陆地互操作性具有全新的维度,因为在任何编译器中生成的程序集都应该从任何其他语言中使用。
Remus Rusanu

25
我同意您不一定要符合CLS,但是如果您正在编写库或控件(并且是从网格开始的代码片段,是吗?),这似乎是一个好主意。否则,您只是在限制您的观众/客户群
JMarsch

4
也许值得将Func <object,string>更改为Expression << Func <object,string >>,并且如果将表达式的右侧约束为常量,则可以实现以下操作:public static IDictionary <string,string>哈希(参数Expression <Func <object,string >> [] hash){Dictionary <string,string> values = new Dictionary <string,string>(); foreach(哈希中的var func){values [func.Parameters [0] .Name] =(string)((ConstantExpression)func.Body).Value; }返回值;}
davidfowl

154

我发现这种奇怪的原因并不是因为名称,而是因为不需要lambda;它可以使用匿名类型,并且更加灵活:

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

这是许多ASP.NET MVC中使用的模式(例如),还有其他用途注意事项,如果名称是一个魔术值而不是特定于调用者,请注意Ayende的想法


4
这也存在互操作性问题;并非所有语言都支持动态创建匿名类型。
布赖恩2009年

5
我喜欢没人能真正回答这个问题,而是人们提出“这更好”的说法。:p是否滥用?
Sam Saffron

7
我很想看到埃里克·利珀特对此的反应。因为它在FRAMEWORK代码中。这同样可怕。
马特·欣兹

26
我认为编写代码的问题不是可读性。我认为真正的问题是代码的可学习性。当您的智能提示说.Attributes(object obj)时,您会怎么想?您将必须阅读文档(没有人愿意做),因为您不知道将什么传递给该方法。我认为这真的比问题中的示例更好。
2009年

2
@Arnis-为什么更灵活:它不依赖隐含的参数名称,这可能(不引述我)导致某些lambda实现(其他语言)出现问题-但您也可以使用具有定义属性的常规对象。例如,您可能有一个HtmlAttributes具有预期属性的类(用于智能感知),而忽略了那些具有null值的属性……
Marc Gravell

137

只是想提出我的意见(我是MvcContrib网格组件的作者)。

这绝对是语言滥用-毫无疑问。但是,我不会真的认为它与直觉相反-当您看到对Attributes(style => "width:100%", @class => "foo")
我的调用时,我认为这很明显是怎么回事(这肯定不比匿名类型方法差)。从智能感知的角度来看,我同意它是相当不透明的。

对于那些感兴趣的人,有关在MvcContrib中使用它的一些背景信息...

我根据个人喜好将其添加到网格中-我不喜欢将匿名类型用作字典(具有接受“对象”的参数与接受参数Func []的参数一样不透明),而Dictionary集合初始值设定项是相当冗长(我也不喜欢冗长而流畅的接口,例如,必须将对Attribute(“ style”,“ display:none”)。Attribute(“ class”,“ foo”)等的多个调用链接在一起)

如果C#的字典字面量语法不太冗长,那么我就不会在网格组件中包括此语法了:)

我还想指出的是,在MvcContrib中使用此选项是完全可选的-这些是扩展方法,用于包装采用IDictionary的重载。我认为重要的是,如果您提供这样的方法,则还应该支持更“正常”的方法,例如与其他语言互操作。

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


1
我还尝试在我的博客上更深入地解决此处提出的一些问题:jeremyskinner.co.uk/2009/12/02/lambda-abuse-the-mvccontrib-hash
Jeremy Skinner

4
在Intellisense中,它的不透明性不亚于匿名对象。
布拉德·威尔逊,

22
+1表示接口是通过可选扩展方法添加的。非C#用户(以及任何因语言滥用而冒犯的人)可以简单地避免使用它。
约恩·舒德罗德(JørnSchou-Rode)2010年

50

我会选择

Attributes.Add(string name, string value);

它更加明确和标准,使用lambda并不会获得任何好处。


20
是吗 html.Attributes.Add("style", "width:100%");读起来不如style = "width:100%"(实际生成的html)好,而style => "width:100%"与生成的HTML看起来非常接近。
杰米·彭尼

6
它们的语法允许使用诸如.Attributes(id =>'foo',@class =>'bar',style =>'width:100%')之类的技巧。函数签名对可变数量的args使用params语法:属性(参数Func <object,object> [] args)。它非常强大,但是花了我一段时间才了解它的功能。
Remus Rusanu

27
@Jamie:试图使C#代码看起来像HTML代码,这将是设计失败的一个坏原因。它们是出于完全不同的目的而完全不同的语言,并且它们不应看起来相同。
加法

17
在不牺牲“美观”的前提下,是否也可以使用匿名对象?.Attributes(new {id =“ foo”,@class =“ bar”,style =“ width:100%”})?
Funka

10
@Guffa为什么会成为设计决策的不好理由?为什么它们看起来不一样?通过这种推理,他们是否应该故意看起来有所不同?我并不是说您错了,而是您可能想更全面地阐述您的观点。
萨曼莎·布兰纳姆

45

欢迎来到Rails Land :)

只要您知道发生了什么,它的确没有错。(就是这种事情没有得到很好的记录时才出现问题)。

整个Rails框架都基于约定优于配置的思想。以某种方式命名事物可以将您引导到他们正在使用的约定中,并且您可以免费获得大量功能。遵循命名约定可以使您更快地前进。整个过程非常出色。

我见过这种技巧的另一个地方是Moq中的方法调用断言。您传递了一个lambda,但从未执行过lambda。他们只是使用表达式来确保方法调用发生了,否则就抛出异常。


3
我有点犹豫,但我同意。除了反射开销外,在Add()中使用字符串与使用lambda参数名称之间没有显着差异。至少我能想到。您可以将其打包并键入“ sytle”,而无需同时注意两种方式。
萨曼莎·布兰纳姆

5
我不知道为什么这对我不奇怪,然后我想起了Rails。:D
Allyn

43

这在多个层面上都是可怕的。不,这不像Ruby。这是对C#和.Net的滥用。

关于如何以更直接的方式执行此操作的建议很多,例如元组,匿名类型,流利的接口等等。

令它如此糟糕的是,它只是为了自己的利益而幻想:

  • 当您需要从VB调用此功能时会发生什么?

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

  • 它的智能感知能力完全与直觉相反,在弄清楚如何传递东西方面几乎没有帮助。

  • 它不必要地效率低下。

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

  • 进入属性的参数类型是什么Func<object,string>?这个意图如何揭示。您的智能感知文档将说什么:“请忽略对象的所有值”

我认为您完全有理由感到愤慨。


4
我会说-这是完全直观的。:)
Arnis Lapsa

4
您说它不像Ruby。但这很像Ruby的用于指定哈希表的键和值的语法。
Charlie Flowers

3
在alpha转换下破裂的代码!是的
Phil

3
@Charlie,在语法上看起来相似,在语义上是不同的。
山姆·萨弗隆

39

我正处在“语法辉煌”阵营中,如果他们清楚地记录下来,而且看起来很酷,那么imo几乎没有问题!


10
阿们,兄弟 阿们(第二个阿们必须满足最小长度才能发表评论:)
查理·弗劳斯

仅您的评论远远超出了需要。但是,那么,您不能只一次阿门,然后发表您的评论:D
睡眠者史密斯,2010年


21

我几乎从未遇到过这种用法。我认为这是“不适当的” :)

这不是常见的使用方式,它与一般约定不一致。当然,这种语法有利有弊:

缺点

  • 代码不直观(通常约定不同)
  • 它往往很脆弱(重命名参数会破坏功能)。
  • 测试起来有点困难(伪造该API将需要在测试中使用反射)。
  • 如果大量使用表达式,由于需要分析参数,而不仅仅是分析值(反射成本),它会变慢

优点

  • 在开发人员调整此语法后,它更具可读性。

底线 -在公共API设计中,我会选择更明确的方式。


2
@Elisha-您的优缺点相反。至少我希望您不要说Pro的代码“不直观”。;-)
Metro Smurf

对于这种特殊情况-lambda参数名称和字符串参数都很脆弱。就像使用动态进行xml解析一样-这样做很合适,因为无论如何您都无法确定xml。
09年

19

不,这当然不是惯例。这是违反直觉的,无法仅查看代码来确定其功能。您必须知道如何使用它,才能了解它的使用方法。

与其使用一组委托来提供属性,不如链接方法更清晰,性能更好:

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

尽管要输入更多内容,但它清晰直观。


6
真?当我查看代码片段时,我确切地知道了该代码片段的意图。除非您非常严格,否则并不是那么令人讨厌。关于字符串连接的重载,可以给出相同的论点,而我们应该始终使用Concat()方法代替。
萨曼莎·布兰纳姆

4
@Stuart:不,您不确定,您只是在根据使用的值进行猜测。任何人都可以做出这种猜测,但是猜测并不是理解代码的好方法。
加法

11
我想用可以.Attribute("style", "width:100%")给我style="width:100%",但就我所知,它可以给我foooooo。我没看到区别。
萨曼莎·布兰纳姆

5
当您查看代码时,“总是根据所使用的值进行猜测”。如果遇到对stream.close()的调用,则假定它关闭了一个流,但是它也可能做一些完全不同的事情。
Wouter Lievens

1
@Wouter:如果您在阅读代码时总是在猜测,那么阅读代码肯定会遇到很大的困难...如果我看到对名为“ close”的方法的调用,我会发现类的作者不知道命名约定,因此我非常想知道该方法的作用是什么。
Guffa

17

我可以用它来造一个短语吗?

magic lambda(n):仅用于替换魔术字符串的lambda函数。


是的...真有趣。在没有编译时安全的意义上,也许这是一种神奇的尝试,在这种地方是否会引起运行时而不是编译时错误?
马斯洛


16

关于“可怕”的所有这些抱怨是一群长期的C#家伙反应过度(我是C#的长期程序员,仍然是该语言的忠实拥护者)。这种语法没有什么可怕的。这只是使语法看起来更像您要表达的内容的一种尝试。某种事物的语法中的“噪音”越少,程序员就越容易理解它。减少一行代码中的噪音只会有所帮助,但是让它在越来越多的代码中累积起来,这是一个很大的好处。

这是作者试图实现与DSL相同的收益的尝试-当代码只是“看起来像”您要说的内容时,您就到达了一个神奇的地方。您可以争论这是否对互操作性好,或者它是否比匿名方法好得多以证明某些“复杂性”成本是合理的。足够公平……因此在您的项目中,您应该正确选择是否使用这种语法。但是,仍然……这是程序员的明智尝试,最终,我们都在尝试做(无论我们是否意识到)。我们所有人都想做的是:“用尽可能接近我们对想要做什么的想法的语言告诉计算机我们想要它做什么。”

越来越接近以内部认为的相同方式向计算机表达指令,这是使软件更易于维护和更准确的关键。

编辑:我曾经说过“使软件更易于维护和更准确的关键”,这是疯狂天真地夸大了独角兽的含义。我将其更改为“键”。


12

这是表达式树的好处之一-可以检查代码本身以获得更多信息。这样便.Where(e => e.Name == "Jamie")可以将其转换为等效的SQL Where子句。这是对表达式树的一种巧妙用法,尽管我希望它不会超出此范围。任何更复杂的事情都可能比它希望替换的代码更困难,因此我怀疑这将是自我限制。


是有道理的,但是在广告中却是事实:LINQ附带了诸如TableAttribute和ColumnAttribute的一整套属性,这使这更加合法。linq映射还会查看类名和属性名,可以说它们比参数名更稳定。
Remus Rusanu

我在那里同意你的看法。在阅读了埃里克·利珀特(Eric Lippert)/安德斯·赫尔斯贝格(Anders Helsberg)/等人对此事的发言后,我对此稍作改动。以为我会留下这个答案,因为它仍然有所帮助。对于它的价值,我现在认为使用HTML的这种工作方式很好,但是不适合该语言。
杰米·彭尼

7

这是一种有趣的方法。如果仅将表达式的右侧约束为常量,则可以使用

Expression<Func<object, string>>

我认为这是您真正想要的而不是委托(您正在使用lambda来获取双方的名称),请参见下面的幼稚实现:

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] = ((ConstantExpression)func.Body).Value.ToString();
    }
    return values;
}

这甚至可以解决线程中前面提到的跨语言互操作问题。


6

该代码非常聪明,但是它可能导致解决更多问题。

如您所指出的,参数名称(样式)和HTML属性之间现在存在模糊的依赖关系。不进行编译时检查。如果参数名称输入错误,则该页面可能不会显示运行时错误消息,但是很难发现逻辑错误(没有错误,但行为不正确)。

更好的解决方案是拥有一个可以在编译时检查的数据成员。所以代替这个:

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

带有Style属性的代码可以由编译器检查:

.Attributes.Style = "width:100%";

甚至:

.Attributes.Style.Width.Percent = 100;

对于代码编写者来说,这还需要更多工作,但是这种方法利用了C#强大的类型检查功能,这有助于防止错误首先进入代码。


3
我感谢编译时检查,但是我认为这归结为一个问题。也许像new Attributes(){Style:“ width:100%”}这样的东西会吸引更多的人,因为它更简洁。尽管如此,实现HTML所允许的一切都是一项艰巨的工作,我不能怪别人仅仅使用字符串/ lambdas /匿名类。
萨曼莎·布兰纳姆

5

确实看起来像Ruby =),至少对我而言,将静态资源用于以后的动态“查找”不适合api设计考虑,希望此聪明技巧在该api中是可选的。

我们可以从IDictionary继承(也可以不继承),并且当您不需要添加用于设置值的键时,可以提供行为类似于php数组的索引器。这将是对.net语义的有效使用,而不仅仅是c#,并且仍然需要文档。

希望这可以帮助


4

我认为这是对lambda的滥用。

至于语法的辉煌,我感到很style=>"width:100%"困惑。Particularily因为=>代替=


4

恕我直言,这是一种很酷的方法。我们都喜欢这样一个事实:命名一个类Controller将使其成为MVC中的控制器,对吗?因此,在某些情况下,命名确实很重要。

这里的意图也很明确。这是很容易理解,.Attribute( book => "something")将导致book="something".Attribute( log => "something")将导致log="something"

我想,如果您像对待惯例那样对待它应该不是问题。我认为,使您减少编写代码并使意图显而易见的任何事情都是一件好事。


4
如果您也不要继承控制器,则命名控制器类不会深蹲……
Jordan Wallwork

3

如果正确选择了方法(func)名称,则这是避免维护麻烦的绝妙方法(即:添加新的func,但忘记将其添加到功能参数映射列表中)。当然,您需要对其进行大量记录,并且最好从该类中的函数的文档中自动生成参数的文档...


1

我认为这并不比“魔术弦”更好。我也不是那么喜欢匿名类型。它需要一种更好的强类型化方法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.