本文转载自微信公众号「DotNET技术圈」,作者Vladimir Sadov。转载本文请联系DotNET技术圈公众号。
C# 局部函数通常被视为 lambda 表达式的进一步增强。虽然功能是相关的,但也存在重大差异。
Local Functions 是嵌套函数[1]功能的 C# 实现。一种语言在支持 lambdas 之后获得对嵌套函数的支持几个版本是有点不寻常的。通常情况相反。
Lambda 或一般的一流函数需要实现未在堆栈上分配且生命周期与需要它们的功能对象相关联的局部变量。如果不依赖垃圾收集或通过捕获列表等解决方案将变量所有权的负担减轻给用户,则几乎不可能正确有效地实现它们。对于某些早期语言来说,这是一个严重的阻塞问题。嵌套函数的简单实现不会遇到这种复杂情况,因此一种语言更常见的是仅支持嵌套函数而不支持 lambda。
无论如何,由于 C# 长期以来一直使用 lambda,因此从差异和相似之处来看本地函数确实是有意义的。
Lambda 表达式
Lambda 表达式x => x + x是抽象地表示一段代码以及它如何绑定到其词法环境中的参数和变量的表达式。作为代码的抽象表示,lambda 表达式不能单独使用。为了使用由 lambda 表达式生成的值,需要将其转换为更多内容,例如委托或表达式树。
- using System;
- using System.Linq.Expressions;
- class Program
- {
- static void Main(string[] args)
- {
- // can't do much with the lambda expression directly
- // (x => x + x).ToString(); // error
- // can assign to a variable of delegate type and invoke
- Func<int, int> f = (x => x + x);
- System.Console.WriteLine(f(21)); // prints "42"
- // can assign to a variable of expression type and introspect
- Expression<Func<int, int>> e = (x => x + x);
- System.Console.WriteLine(e); // prints "x => (x + x)"
- }
- }
有几点值得注意:
- lambdas 是产生函数值的表达式。
- lambda 值的生命周期是无限的——从 lambda 表达式的执行开始,只要存在对该值的任何引用。这意味着 lambda 从封闭方法中使用或“捕获”的任何局部变量都必须在堆上分配。由于 lambda 值的生命周期不受产生它的堆栈帧的生命周期的限制,因此不能在该堆栈帧上分配变量。
- lambda 表达式要求在执行 lambda 表达式时明确分配主体中使用的所有外部变量。lambda 的第一次和最后一次使用的时刻很少是确定性的,因此该语言假设 lambda 值可以在创建后立即使用,只要它们是可访问的。因此,一个 lambda 值在创建时必须是完全可用的,并且它使用的所有外部变量都必须明确分配。
- int x;
- // ERROR: 'x' is not definitely assigned
- Func<int> f = () => x;
- lambdas 没有名字,也不能被象征性地引用。特别是 lambda 表达式不能递归声明。
注意:可以通过调用分配给 lambda 的变量或传递给自应用其参数的高阶方法来创建递归 lambda(请参阅:C# 中的匿名递归[2]),但这不会表达真正的自我参照。
本地函数
局部函数基本上只是在另一个方法中声明的方法,作为一种降低方法对其声明范围内的可见性的方法。
自然地,局部函数中的代码可以访问其包含范围内可访问的所有内容——局部变量、封闭方法的参数、类型参数、局部函数。一个值得注意的例外是外部方法标签的可见性。封闭方法的标签在局部函数中不可见。这只是普通的词法范围,它的工作原理与 lambdas 相同。
- public class C
- {
- object o;
- public void M1(int p)
- {
- int l = 123;
- // lambda has access to o, p, l,
- Action a = ()=> o = (p + l);
- }
- public void M2(int p)
- {
- int l = 123;
- // Local Function has access to o, p, l,
- void a()
- {
- o = (p + l);
- }
- }
- }
与 lambda 的明显区别在于局部函数具有名称并且可以在没有任何间接方式的情况下使用。局部函数可以是递归的。
- static int Fac(int arg)
- {
- int FacRecursive(int a)
- {
- return a <= 1 ?
- 1 :
- a * FacRecursive(a - 1);
- }
- return FacRecursive(arg);
- }
与 lambda 表达式的主要语义区别在于局部函数不是表达式,它们是声明语句。在代码执行方面,声明是非常被动的实体。事实上,声明并没有真正被“执行”。与标签等其他声明类似,局部函数声明只是将函数引入包含范围,而无需运行任何代码。
更重要的是,无论是声明本身还是嵌套函数的常规调用都不会导致对环境的不确定捕获。在简单和常见的情况下,如普通的调用/返回场景,捕获的局部变量不需要进行堆分配。
例子:
- public class C
- {
- public void M()
- {
- int num = 123;
- // has access to num
- void Nested()
- {
- num++;
- }
- Nested();
- System.Console.WriteLine(num);
- }
- }
上面的代码大致相当于(反编译):
- public class C
- {
- // A struct to hold "num" variable.
- // We are not storing it on the heap,
- // so it does not need to be a class
- private struct <>c__DisplayClass0_0
- {
- public int num;
- }
- public void M()
- {
- // reserve storage for "num" in a display struct on the _stack_
- C.<>c__DisplayClass0_0 env = default(C.<>c__DisplayClass0_0);
- // num = 123
- env.num = 123;
- // Nested()
- // note - passes env as an extra parameter
- C.<M>g__a0_0(ref env);
- // System.Console.WriteLine(num)
- Console.WriteLine(env.num);
- }
- // implementation of the the "Nested()".
- // note - takes env as an extra parameter
- // env is passed by reference so it's instance is shared
- // with the caller "M()"
- internal static void <M>g__a0_0(ref C.<>c__DisplayClass0_0 env)
- {
- env.num += 1;
- }
- }
请注意,上面的代码直接调用了“Nested()”的实现(不是通过委托间接),并且没有在堆上引入显示存储的分配(就像 lambda 会那样)。局部变量存储在结构中而不是类中。的生命周期num并没有因为它在 中的使用而改变Nested(),所以它仍然可以在栈上分配。M()可以只通过num引用传递,但编译器使用结构体进行打包,因此它可以传递所有本地变量,就像num只使用一个 env 参数一样。
另一个有趣的一点是,只要本地函数在给定范围内可见,就可以使用它们。这是一个重要的事实,使递归和相互递归的场景成为可能。这也使得本地函数声明在源代码中的确切位置在很大程度上变得不重要。
例如,封闭方法的所有变量必须在调用读取它们的本地函数时明确分配,而不是在其声明时。实际上,如果调用可以更早发生,那么在声明时提出该要求将没有任何好处。
- public void M()
- {
- // error here -
- // Use of unassigned local variable 'num'
- Nested();
- int num;
- // whether 'num' is assigned here or not is irrelevant
- void Nested()
- {
- num++;
- }
- num = 123;
- // no error here - 'num' is assigned
- Nested();
- System.Console.WriteLine(num);
- }
此外 - 如果从未使用过局部函数,它也不会比一段无法访问的代码和任何变量更好,否则它会使用,不需要分配。
- public void M()
- {
- int num;
- // warning - Nested() is never used.
- void Nested()
- {
- // no errors on unassigned 'num'.
- // this code never runs.
- num++;
- }
- }
那么,局部函数的目的是什么?
与 lambdas 相比,局部函数的主要价值主张是局部函数在概念上和运行时开销方面都更简单。
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