局部函数(Local Function)是一个很有意思的概念,乍一看仿佛是一种略为简洁的匿名函数创建语法。我们能从下面的例子中发现差别:
public DateTime Max_Anonymous_Function(IList<DateTime> values) { Func<DateTime, DateTime, DateTime> MaxDate = (left, right) => { return (left > right) ? left : right; }; var result = values.First(); foreach (var item in values.Skip(1)) result = MaxDate(result, item); return result; } public DateTime Max_Local_Function(IList<DateTime> values) { DateTime MaxDate(DateTime left, DateTime right) { return (left > right) ? left : right; } var result = values.First(); foreach (var item in values.Skip(1)) result = MaxDate(result, item); return result; }然而,只有深入地接触局部函数,才能发现其中的引入入胜之处。
匿名函数与局部函数的对比正常创建一个匿名函数时,总是会相应地创建一个用于存储该函数的隐含类。该隐含类将会创建一个实例,并存储在类的静态字段中。因此,隐含类一旦创建,就不再需要更多的开销。
反之,本地函数不需要隐含类,而是与其父函数一样,表示为同一个类中的静态方法。
闭包(Closure)如果一个函数中的变量被自身所包含的匿名函数或局部函数引用,则称为形成了一个“闭包”,因为这种行为“包含”(Close-over)或“捕获”(Capture)了局部函数。下面给出一个例子:
public DateTime Max_Local_Function(IList<DateTime> values) { int callCount = 0; DateTime MaxDate(DateTime left, DateTime right) { callCount++; <--变量callCount被闭包。 return (left > right) ? left : right; } var result = values.First(); foreach (var item in values.Skip(1)) result = MaxDate(result, item); return result; }每次调用一个包含匿名函数的函数时,需要新建一个隐含类实例。这种设计确保了每次调用函数时,函数中具有对父函数与匿名函数间共享数据的拷贝。
这种设计的缺点在于,每次调用匿名函数时需要实例化一个新的对象。由于这对垃圾回造成了压力,因此增加了使用的开销。
使用局部函数时会创建一个隐含结构体,而非一个隐含类。这允许局部函数持续存储预调用的数据,同时消除了对单个对象实例化的需求。类似于匿名方程,局部函数也是物理地存储在隐含结构体中。
委托(Delegates)在创建匿名函数或局部函数时,很多情况下会将函数打包为一个委托,这样就可以在事件处理器或是LINQ表达式中使用它。
从定义上看,匿名函数当然是匿名的。因此要使用匿名函数,通常需要将匿名函数以委托的形式存储在变量或参数中。
委托不能指向结构体,除非将委托装箱(Box)。但这种语法很奇怪。因此如果你创建了一个指向局部函数的委托,编译器将会创建一个隐含类,而不是一个隐含结构体。如果该局部函数是一个闭包,那么在每次调用父函数时,需要新建一个隐含类的实例。
迭代器(Iterator)在C#中,如果函数使用了yield return暴露一个IEnumerable<T>,那么就无法立刻对函数的参数进行验证。需要等待在返回的匿名枚举器上调用MoveNext后,参数才会得到验证。
这在VB中并不是一个问题,因为VB支持匿名迭代器。下面是MSDN中给出的一个例子:
Public Function GetSequence(low As Integer, high As Integer) _ As IEnumerable ' 验证参数。 If low < 1 Then Throw New ArgumentException("low is too low") If high > 140 Then Throw New ArgumentException("high is too high") ' 返回一个匿名迭代器方法。 Dim iterateSequence = Iterator Function() As IEnumerable For index = low To high Yield index Next End Function Return iterateSequence() End Function在当前的C#版本中,GetSequence及其迭代器分别是两个完全独立的函数。使用C# 7,可用局部函数将两者组合在一起。例如:
public IEnumerable<int> GetSequence(int low, int high) { if (low < 1) throw new ArgumentException("low is too low"); if (high > 140) throw new ArgumentException("high is too high"); IEnumerable<int> Iterator() { for (int i = low; i <= high; i++) yield return i; } return Iterator(); }迭代器需要构建一个状态机,因此在行为上类似于闭包,需根据隐含类以委托的形式返回。
匿名函数和局部函数的指导原则在不需要委托时,一定要使用本地函数,而非匿名函数,尤其是涉及闭包的情况下。
所需的参数需要验证时,一定要使用局部迭代器。
可以考虑将局部函数定义在一个函数体的开始或结束处,这样可以从观感上将局部函数与它们的父函数区分开来。