当前位置: 首页 > 科技观察

C#局部函数与Lambda表达式

时间:2023-03-14 10:26:31 科技观察

本文转载自微信公众号《DotNET技术圈》,作者VladimirSadov。转载本文请联系DotNET技术圈公众号。C#本地函数通常被认为是lambda表达式的进一步增强。虽然这些功能是相关的,但也存在重大差异。本地函数是嵌套函数[1]功能的C#实现。一种语言在支持lambda后的几个版本中获得对嵌套函数的支持有点不寻常。通常情况恰恰相反。Lambda或一般的一流函数需要实现未在堆栈上分配且其生命周期与需要它们的函数对象相关联的局部变量。如果不依赖垃圾收集或通过捕获列表等解决方案将变量所有权卸载给用户的解决方案,则几乎不可能正确有效地实施它们。对于某些早期语言来说,这是一个严重的阻塞问题。嵌套函数的简单实现不会受到这种复杂性的影响,因此一种语言更常见的是只支持嵌套函数而不支持lambda。不管怎样,由于C#已经使用lambda很长时间了,所以从异同的角度来看待原生函数确实有意义。Lambda表达式Lambda表达式x=>x+x是一个表达式,它抽象地表示一段代码以及它如何绑定到其词法环境中的参数和变量。作为代码的抽象表示,lambda表达式不能单独使用。为了使用lambda表达式产生的值,需要将其转换为更多的东西,例如委托或表达式树。usingSystem;usingSystem.Linq.Expressions;classProgram{staticvoidMain(string[]args){//不能直接使用lambda表达式//(x=>x+x).ToString();//error//可以赋值给变量的delegatetypeandinvokeFuncf=(x=>x+x);System.Console.WriteLine(f(21));//打印“42”//可以分配给变量的表达式类型和introspectExpression>e=(x=>x+x);System.Console.WriteLine(e);//prints"x=>(x+x)"}}一些值得注意的事情:lambda是产生函数值的表达式。lambda值的生命周期是无限的——从lambda表达式的执行开始,只要对该值的任何引用都存在。这意味着lambda使用或从封闭方法“捕获”的任何局部变量都必须在堆上分配。由于lambda值的生命周期不限于生成它的堆栈帧的生命周期,因此不能在该堆栈帧上分配变量。Lambda表达式要求在执行lambda表达式时显式分配主体中使用的所有外部变量。第一次和最后一次使用lambda的时刻很少是确定性的,因此该语言假定lambda值可以在创建后立即使用,只要它们是可访问的。因此,lambda值在创建时必须完全可用,并且它使用的所有外部变量都必须显式分配。intx;//错误:'x'未明确分配Funcf=()=>x;lambda没有名字,不能被符号引用。特别是lambda表达式不能递归声明。注意:递归lambda可以通过调用分配给lambda的变量或传递给自应用其参数的高阶方法来创建(请参阅:C#[2]中的匿名递归),但这并不表示真正的自引用.局部函数局部函数基本上只是在另一个方法中声明的方法,作为降低方法在其声明范围内可见性的一种方式。自然地,局部函数中的代码可以访问其包含范围内可访问的所有内容——局部变量、封闭方法的参数、类型参数、局部函数。一个值得注意的例外是外部方法标签的可见性。封闭方法的标签在局部函数中不可见。这只是正常的词法范围,它的工作原理与lambda相同。publicclassC{objecto;publicvoidM1(intp){intl=123;//lambdahasaccesstoo,p,l,Actiona=()=>o=(p+l);}publicvoidM2(intp){intl=123;//LocalFunctionhasaccesstoo,p,l,voida(){o=(p+l);}}}与lambda的不同之处在于局部函数具有名称并且可以在没有任何间接访问的情况下使用。局部函数可以递归。staticintFac(intarg){intFacRecursive(inta){returna<=1?1:a*FacRecursive(a-1);}returnFacRecursive(arg);}和lambda表达式之间的主要语义区别是局部函数不是表达式,它们是语句语句。在代码执行方面,语句是非常被动的实体。事实上,语句并没有真正“执行”。与标签等其他声明一样,局部函数声明只是将函数带入包含作用域,而无需运行任何代码。更重要的是,声明本身和嵌套函数的常规调用都不会导致不确定的环境捕获。在简单和常见的情况下,例如普通的调用/返回场景,捕获的局部变量不需要堆分配。示例:publicclassC{publicvoidM(){intnum=123;//hasaccesstonumvoidNested(){num++;}Nested();System.Console.WriteLine(num);}}上面的代码大致等同于(反编译):publicclassC{//Astructtohold"num"variable.//Wearenotstoringitontheheap,//soitdoesnotneedtobeaclassprivastruct<>c__DisplayClass0_0{publicintnum;}publicvoidM(){//reservestoragefor"num"inadisplaystructonthe_stack_C.<>c__DisplayClass0_0env=default(Cplay/Class<0_0Dis)123env.num=123;//Nested()//注意-passesenvasanextraparameterC.g__a0_0(refenv);//System.Console.WriteLine(num)Console.WriteLine(env.num);}//实现"Nested()".//note-takesenvasanextraparameter//envispassedbyreferencesoit'sinstanceisshared//withthecaller"M()"internalstaticvoidg__a0_0(refC.<>c__DisplayClass0_0env){env.num+=1;}}注意上面的代码直接实现了“嵌套”()"被调用(不是通过委托间接调用),并且没有在堆上引入显式存储分配(就像lambda那样)。局部变量存储在结构中而不是类中。Nested()中的使用并没有改变num的生命周期,因此它仍然可以在堆栈上分配。M()可以只通过num引用传递,但编译器使用结构进行包装,因此它可以传递所有局部变量,就像num只接受一个env参数一样。另一个有趣的地方是,只要在给定范围内可见,就可以使用局部函数。这是实现递归和相互递归场景的重要事实。这也使得它在很大程度上与本地函数声明在源代码中的确切位置无关。例如,封闭方法的所有变量必须在调用读取它们的本地函数时显式赋值,而不是在声明它们时赋值。事实上,如果调用可以更早发生,则在声明时要求它没有任何好处。publicvoidM(){//errorhere-//Useofunassignedlocalvariable'num'Nested();intnum;//这里'num'是否被赋值ornotisirrelevantvoidNested(){num++;}num=123;//noerrorhere-'num'isassignedNested();System.Console.WriteLine(num);}此外-如果从未使用过本地函数,它并不比一段无法访问任何变量的代码更好,否则它已被使用,无需分配。publicvoidM(){intnum;//warning-Nested()isneverused.voidNested(){//noerrorsonunassigned'num'.//thiscodeneverruns.num++;}}那么,局部函数的作用是什么?与lambda相比,局部函数的主要价值主张是局部函数在概念上和运行时开销方面都更简单。Lambda可以很好地充当一类函数[3]的角色,但有时您只需要一个简单的帮助程序。分配给局部变量的Lambda可以完成这项工作,但以间接、委托分配和可能的闭包开销为代价。私有方法也可以工作并且调用起来更便宜,但是存在封装问题,或者没有封装问题。这样的助手对包含类型中的每个人都是可见的。这些助手太多会导致严重的混乱。本地函数非常适合这种情况。调用本地函数的开销与调用私有方法的开销相当,但是用其他不应调用的方法污染包含类型是没有问题的。http://mustoverride.com/local_functions/References[1]嵌套函数:https://en.wikipedia.org/wiki/Nested_function[2]C#中的匿名递归:https://blogs.msdn.microsoft。com/wesdyer/2007/02/02/anonymous-recursion-in-c/[3]一级函数:https://en.wikipedia.org/wiki/First-class_function