Delphi匿名方法-优缺点。在Delphi中使用闭包(匿名方法)的良好实践。

8
我在团队中有一个同事在我们使用Delphi开发的项目中广泛使用闭包。个人而言,我不喜欢这样做是因为它使代码变得更难读,并且我认为只有在需要时才应该使用闭包。
另一方面,我已经阅读了Can someone explain Anonymous methods to me?和其他相关链接,并且考虑到我可能是错的,所以我想请您给我一些例子,说明何时最好使用闭包而不是“老式”的方法(不使用闭包)。

4
主要问题是变量捕获及其导致的生命周期更改。当在同一个作用域内定义多个共享变量捕获的匿名方法时,可能会产生一些意想不到的效果。除非匿名方法可以使代码更清晰简单,否则我通常会避免使用它们。 - David Heffernan
匿名方法属于函数式编程范式。它们在命令式的 Delphi 环境中相对陌生,并且与函数式语言相比具有非常有限的价值。 - kludg
3
例如,查看OmniThreadLibrary-我喜欢Gabriel在那里使用闭包的方式... - George
1
Lambda表达式之所以被添加到Delphi中,只是因为“其他人都有它们,而我们没有”,并且没有Heilsberg来劝告他们。这违背了Pascal的范例,自然导致了严重的语言丑化(同时也违反了基本的Pascal括号语法)。 - Premature Optimization
Gabriel的代码是现代聪明人的良好示例,他使用所有新功能,但非常谨慎,并使用单元测试和制作可信赖的低级别代码。 - Warren P
4
我强烈不同意Serg的观点。我熟悉Python和Delphi,并认识到Delphi中更长的语法是强类型的必要副作用。动态类型和lambda是很好的匹配,但像C ++和Pascal这样的强类型语言,lambda /闭包也非常有价值,就像在Python中一样。除了使用接口实现的traits之外,实际上还有一些强类型系统从Lambda中受益的强大方式,甚至比Python中更多,因为在Python中你可以使用动态语言技巧来完成许多Lambda特技。 - Warren P
1个回答

12
我认为这个问题需要非常主观的判断。我是一名老派的Delphi开发者,也倾向于同意你的看法。闭包不仅会带来某些风险(正如David H在评论中指出的那样),而且还会降低所有接受过经典Delphi培训的开发者的可读性。那么,为什么它们被添加到语言中呢?例如,在Delphi XE中,语法格式化功能和闭包无法很好地配合使用,这增加了我对闭包的不信任;在Delphi编译器中添加了多少东西,IDE还没有完全升级以支持它们?当您公开承认如果Delphi语言冻结在Delphi 7级别并且永远不再进行改进,您会感到满意时,您就知道自己是一个暴躁的老计时器。但是,Delphi是一种生动、强大、不断发展的语法。这是一件好事。在您发现老式曲折的代码占据上风时,请反复告诉自己这一点。试试看。
我可以想到至少十个匿名方法真正有意义的场景以及为什么应该使用它们的原因,尽管我之前表示我不信任它们。我只会指出我决定个人使用的两个场景以及我在使用它们时限制自己的范围:
1.在Generics.Collections中,容器类的排序方法接受一个匿名方法,以便您可以轻松地提供一个排序逻辑位而不必编写与该排序方法期望的相同签名匹配的常规(非匿名)函数。新的泛型语法与此风格紧密结合,虽然一开始看起来很陌生,但会让你变得更加方便。
2.TThread方法(如Synchronize)被重载,并且除了支持单个TThreadMethod作为参数Thread.Synchronize(aClassMethodWithoutParameters)外,我一直感到痛苦的是将参数传递给该synchronize方法。现在,您可以使用闭包(匿名方法)并传递参数。
我建议在编写匿名方法时遵循以下限制:
A.我有一个个人的经验法则,即每个函数只有一个闭包,如果超过一个,请将该代码片段重构为自己的方法。这使您的“方法”的圆形复杂性不会变得疯狂。
B.此外,在每个闭包内,我更喜欢仅有一个方法调用及其参数,如果我最终编写了大块的代码,则将其重写为方法。闭包是用于变量捕获,而不是任意编写无限扭曲的代码。
样例排序:
 var     
   aContainer:TList<TPair<String, Integer>>;  
 begin   
  aContainer.Sort(    
    TMyComparer.Construct(
      function (const L, R: TPair<String, Integer>): integer
      begin
        result := SysUtils.CompareStr(L.Key,R.Key);
      end ) {Construct end}   );  {aContainer.Sort end}  
 end;

更新:有一条评论指出了“语言丑化”,我认为这种丑化是指写法上的差异:

  x.Sort(     
    TMyComparer.Construct(
      function (const L, R: TPair<String, Integer>): integer
      begin
        result := SysUtils.CompareStr(L.Key,R.Key);
      end )   ); 

与其使用我在此进行比较的下面这个假设的鸭子类型(或者我应该说是推断类型)语法:

  x.Sort( lambda( [L,R], [ SysUtils.CompareStr(L.Key,R.Key) ] ) )

像Smalltalk和Python这样的其他语言可以更紧凑地编写lambda,因为它们是动态类型的。例如,需要将IComparer作为传递给容器中Sort()方法的类型,就是强类型语言中具有泛型的接口特性所导致的复杂性的一个示例,以实现类似排序的特征。我不认为有一种好的方法来做到这一点。个人而言,我讨厌在函数调用括号内看到procedure、begin和end关键字,但我不知道还有什么别的合理的做法。


如果我理解你的意思正确,(1)只是一个“速记”参数,可能与它允许类型聚合作为参数有关,而不需要先单独定义它(顺便说一下,标准Pascal也支持这个)。但是,类型聚合在参数声明中的允许也可以应用于普通的过程/方法吗? - Marco van de Voort
1
不完全是速记,虽然是更短的。更多的是你编写的条件或函数比较在调用上下文中本地化,而不是在其他地方声明(在过程范围之外,作为自己的过程)。这不仅允许捕获参数,如果您原谅lambda的丑陋过程开始结束部分,它真的很可读。如果您想允许调用具有任意参数的线程过程,则将创建100多个版本的TThread.Synchronize(method:TIntStringCharDoubleIntStrCharMethod); - Warren P
1
可以更紧凑地编写Lambda,因为它们是动态类型。你不需要动态类型来实现这一点。在静态类型语言中,编写编译器会更困难一些。但看看C#如何支持这些情况下的类型推断。 - CodesInChaos
正如我所说,类型推断(鸭子类型)需要编译器拥有某些智能,而 Delphi 编译器基础设施缺乏这种能力。C#几乎不是一种纯静态类型的语言,因为它是由 CLR 驱动的代码文档(Code-DOM)支持的动态语言环境,同时也具有静态类型,但是在这一点上,它基本上只是语法糖。我的意思是,你可以在 .NET 上运行 Python(IronPython),因此,.NET 现在已经完全成为动态类型,而 C# 只是在其之上可用的几个语法风格之一。 - Warren P
@WarrenP 我不同意。类型推断并非鸭子类型。在鸭子类型中,唯一关心的是能力,而不是名称。就像接口一样,它只关心对象是否具有方法 X、Y 和 Z,而不是类型。类型推断使用 Hindley-Milner 算法从变量的使用方式推断出静态类型。最具静态类型的语言可以使用 H-M,完全不需要支持鸭子类型。C# 是静态类型的。这不是“糖”。SWIG 为 Python 包装了 C++,但 C++ 并不是动态的。Python 有 Qt 绑定,尽管 Qt 是用 C++ 编写的。 - alcalde
我可以在任何东西中实现任何东西。你的观点是什么?Hindley-Milner等等。如果我在卡车上螺栓一个橙树,它就可以变成一个橙树。(顺便说一下,这就是我对C++的感觉,你可以在其上添加一个好的语言,只可惜没有人制作出真正的语言来替代它。)无论如何,Delphi需要明确指定类型,并且不提供紧凑的类型推断系统,你和我都无法添加它。编译器作者有可能在某一天添加它,但这并不重要。 - Warren P

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