局部函数是C# 7中的一个新功能,允许在一个函数中定义另一个函数。

何时使用局部函数?

局部函数的主要功能与匿名方法非常相似:在某些情况下,创建一个命名函数在读者的认知负担方面代价太大。有时,函数本身就是另一个函数的部分逻辑,因此用一个单独的命名实体来污染“外部”范围是毫无意义的。

您可能认为此功能是多余的,因为匿名委托或Lambda表达式可以实现相同的行为。但事实并非如此,匿名函数有一定的限制,其特征可能不适合您的场景。

用例1:迭代器中的先决条件

这是一个简单的函数,逐行读取一个文件。您知道什么时候ArgumentNullException会被抛出来吗?

    public static IEnumerable<string> ReadLineByLine(string fileName)
{
if (string.IsNullOrEmpty(fileName)) throw new ArgumentNullException(nameof(fileName));
foreach (var line in File.ReadAllLines(fileName))
{
yield return line;
}
} // 什么时候发生错误?
string fileName = null;
// 这里?
var query = ReadLineByLine(fileName).Select(x => $"\t{x}").Where(l => l.Length > 10);
// 还是这里?
ProcessQuery(query);

包含yield return的方法很特殊。它们叫做 迭代器块(Iterator Blocks),它们很懒。这意味着这些方法的执行是“按需”发生的,只有当方法的客户端调用MoveNext生成迭代器时,才会执行它们中的第一个代码块。在我们的例子中,这意味着错误只会在ProcessQuery方法中发生,因为所有的LINQ操作符都是懒惰的。

显然,该行为是不可取的,因为该ProcessQuery方法抛出的异常ArgumentNullException将不具有关于该上下文的足够信息。所以最好尽早抛出异常 - 客户端调用ReadLineByLine时,而不是当客户端处理结果时。

为了解决这个问题,我们需要将验证逻辑提取到一个单独的方法中。匿名函数是最佳候选,但匿名委托和Lambda表达式不支持迭代器块:

VB.NET中的 Lambda表达式支持迭代器块。

    public static IEnumerable<string> ReadLineByLine(string fileName)
{
if (string.IsNullOrEmpty(fileName)) throw new ArgumentNullException(nameof(fileName)); return ReadLineByLineImpl(); IEnumerable<string> ReadLineByLineImpl()
{
foreach (var line in File.ReadAllLines(fileName))
{
yield return line;
}
}
}

用例2:异步方法中的先决条件

异步方法与异常处理有类似的问题,在标记有async关键字的方法中抛出的任何异常,会在一个失败的Task中显现:

    public static async Task<string> GetAllTextAsync(string fileName)
{
if (string.IsNullOrEmpty(fileName)) throw new ArgumentNullException(nameof(fileName));
var result = await File.ReadAllTextAsync(fileName);
Log($"Read {result.Length} lines from '{fileName}'");
return result;
} string fileName = null;
// 无异常
var task = GetAllTextAsync(fileName);
// 以下行将抛出异常
var lines = await task;

从技术上说,async是一个上下文关键字,但这并不改变我的观点。

您可能认为错误发生时没有太大差异,但这远非如此。失败的Task意味着该方法本身未能做到应该做的事情,问题出在方法本身或方法所依赖的某一个构建块中。

在系统中传递结果Task时,急切的先决条件验证尤为重要。在这种情况下,很难理解什么时候出现什么问题。局部函数可以解决这个问题:

    public static Task<string> GetAllTextAsync(string fileName)
{
// 提前参数验证
if (string.IsNullOrEmpty(fileName)) throw new ArgumentNullException(nameof(fileName));
return GetAllTextAsync(); async Task<string> GetAllTextAsync()
{
var result = await File.ReadAllTextAsync(fileName);
Log($"Read {result.Length} lines from '{fileName}'");
return result;
}
}

用例3:迭代器块的局部函数

不能在Lambda表达式中使用迭代器是一个非常麻烦的问题。这是一个简单的例子:如果要获取类型层次结构中的所有字段(包括私有的),则必须手动遍历继承层次结构。但遍历逻辑是特定方法的,应尽可能保持局部可用:

    public static FieldInfo[] GetAllDeclaredFields(Type type)
{
var flags = BindingFlags.Instance | BindingFlags.Public |
BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
return TraverseBaseTypeAndSelf(type)
.SelectMany(t => t.GetFields(flags))
.ToArray(); IEnumerable<Type> TraverseBaseTypeAndSelf(Type t)
{
while (t != null)
{
yield return t;
t = t.BaseType;
}
}
}

用例4:递归匿名方法

默认情况下,匿名函数无法引用自身。要解决此限制,您应该声明一个委托类型的局部变量,然后在Lambda表达式或匿名委托中使用该局部变量:

    public static List<Type> BaseTypesAndSelf(Type type)
{
Action<List<Type>, Type> addBaseType = null;
addBaseType = (lst, t) =>
{
lst.Add(t);
if (t.BaseType != null)
{
addBaseType(lst, t.BaseType);
}
}; var result = new List<Type>();
addBaseType(result, type);
return result;
}

这种方法可读性不强,类似的解决方案,局部函数感觉会更自然:

    public static List<Type> BaseTypesAndSelf(Type type)
{
return AddBaseType(new List<Type>(), type); List<Type> AddBaseType(List<Type> lst, Type t)
{
lst.Add(t);
if (t.BaseType != null)
{
AddBaseType(lst, t.BaseType);
}
return lst;
}
}

用例5:内存分配

如果您曾经开发过一个性能要求非常高的的应用程序,应该知道匿名方法的开销也不小:

  • 委托调用的开销(非常小,但确实存在);
  • 如果Lambda捕获本地变量或封闭方法的参数,则需要分配2个堆内存(一个用于闭包实例,另一个用于委托本身);
  • 如果Lambda捕获一个封闭的实例状态,则需要分配1个堆内存(只是分配委托);
  • 只有当Lambda没有捕获任何东西或捕获静态时,分配0个堆内存。

但是局部函数的分配模式不同。

    public void Foo(int arg)
{
PrintTheArg();
return;
void PrintTheArg()
{
Console.WriteLine(arg);
}
}

如果一个局部函数捕获一个局部变量或一个参数,那么C#编译器会生成一个特殊的闭包结构,实例化它并通过引用传递给一个生成的静态方法:

    internal struct c__DisplayClass0_0
{
public int arg;
} public void Foo(int arg)
{
// Closure instantiation
var c__DisplayClass0_ = new c__DisplayClass0_0() { arg = arg };
// Method invocation with a closure passed by ref
Foo_g__PrintTheArg0_0(ref c__DisplayClass0_);
} internal static void Foo_g__PrintTheArg0_0(ref c__DisplayClass0_0 ptr)
{
Console.WriteLine(ptr.arg);
}

(编译器生成无效字符的名称,例如<>。为了提高可读性,我更改了名称并简化了代码。)

局部函数可以捕获实例状态、局部变量或参数,不会发生堆内存分配。

局部函数中使用的局部变量应该在局部函数声明站点中明确指定。

堆内存分配将发生的情况很少:

  • 局部函数被明确地或隐式地转换为委托。

如果局部函数捕获静态/实例字段但不捕获局部变量/参数,则只会发生委托分配。

    public void Bar()
{
// Just a delegate allocation
//只是一个委托分配
Action a = EmptyFunction;
return;
void EmptyFunction() { }
}

如果局部函数捕获局部变量/参数,将发生闭包分配和委托分配:

    public void Baz(int arg)
{
// Local function captures an enclosing variable.
// The compiler will instantiate a closure and a delegate
//本地函数捕获一个封闭的变量。
//编译器将实例化一个闭包和一个委托
Action a = EmptyFunction;
return;
void EmptyFunction() { Console.WriteLine(arg); }
}
  • 本地函数捕获局部变量/参数,匿名函数从同一范围捕获变量/参数。

这种情况更为微妙。

C#编译器为每个词法范围生成一个不同的闭包类型(方法参数和顶级局部变量驻留在同一个顶级范围内)。在以下情况中,编译器将生成两个闭包类型:

    public void DifferentScopes(int arg)
{
{
int local = 42;
Func<int> a = () => local;
Func<int> b = () => local;
} Func<int> c = () => arg;
}

两个不同的Lambda表达式如果它们从同一范围捕获局部变量,将使用相同的闭包类型,Lambda ab驻留在同一闭包:

    private sealed class c__DisplayClass0_0
{
public int local; internal int DifferentScopes_b__0()
{
// Body of the lambda 'a'
return this.local;
} internal int DifferentScopes_b__1()
{
// Body of the lambda 'a'
return this.local;
}
} private sealed class c__DisplayClass0_1
{
public int arg; internal int DifferentScopes_b__2()
{
// Body of the lambda 'c'
return this.arg;
}
} public void DifferentScopes(int arg)
{
var closure1 = new c__DisplayClass0_0 { local = 42 };
var closure2 = new c__DisplayClass0_1() { arg = arg };
var a = new Func<int>(closure1.DifferentScopes_b__0);
var b = new Func<int>(closure1.DifferentScopes_b__1);
var c = new Func<int>(closure2.DifferentScopes_b__2);
}

在某些情况下,这种行为可能会导致一些非常严重的内存相关问题。这是一个例子:

    private Func<int> func;
public void ImplicitCapture(int arg)
{
var o = new VeryExpensiveObject();
Func<int> a = () => o.GetHashCode();
Console.WriteLine(a()); Func<int> b = () => arg;
func = b;
}

在委托调用a()之后,变量o似乎应该符合垃圾回收的条件,但事实并非如此,两个Lambda表达式共享相同的闭包类型:

    private sealed class c__DisplayClass1_0
{
public VeryExpensiveObject o;
public int arg; internal int ImplicitCapture_b__0()
=> this.o.GetHashCode(); internal int ImplicitCapture_b__1()
=> this.arg;
} private Func<int> func; public void ImplicitCapture(int arg)
{
var c__DisplayClass1_ = new c__DisplayClass1_0()
{
arg = arg,
o = new VeryExpensiveObject()
};
var a = new Func<int>(c__DisplayClass1_.ImplicitCapture_b__0);
Console.WriteLine(func());
var b = new Func<int>(c__DisplayClass1_.ImplicitCapture_b__1);
this.func = b;
}

这意味着闭包实例的生命周期将被绑定到func字段的生命周期:闭包保持活动,直到应用程序到达委托func。这可以延长VeryExpensiveObject生命周期,这基本上会导致内存泄漏。

当局部函数和Lambda表达式捕获来自同一范围的变量时,会发生类似的问题。即使捕获不同的变量,封闭类型将被共享,导致堆分配:

    public int ImplicitAllocation(int arg)
{
if (arg == int.MaxValue)
{
// This code is effectively unreachable
Func<int> a = () => arg;
} int local = 42;
return Local(); int Local() => local;
}

编译为:

    private sealed class c__DisplayClass0_0
{
public int arg;
public int local; internal int ImplicitAllocation_b__0()
=> this.arg; internal int ImplicitAllocation_g__Local1()
=> this.local;
} public int ImplicitAllocation(int arg)
{
var c__DisplayClass0_ = new c__DisplayClass0_0 { arg = arg };
if (c__DisplayClass0_.arg == int.MaxValue)
{
var func = new Func<int>(c__DisplayClass0_.ImplicitAllocation_b__0);
}
c__DisplayClass0_.local = 42;
return c__DisplayClass0_.ImplicitAllocation_g__Local1();
}

正如您可以看到,顶层作用域中的所有局部变量现在都成为封闭类的一部分,即使当局部函数和Lambda表达式捕获不同的变量时也会导致关闭分配。

总结

以下是C#中关于局部函数的最重要特性:

  1. 局部函数可以定义迭代器;
  2. 局部函数对异步方法和迭代器块的预先验证非常有用;
  3. 局部函数可以递归;
  4. 如果没有向委托进行转换,局部函数是免分配的;
  5. 由于缺少委托调用开销,局部函数的效率比匿名函数稍高;
  6. 局部函数可以在返回语句之后声明,它将主逻辑与辅助函数分开;
  7. 局部函数可以“隐藏”在外部范围中声明的具有相同名称的函数;
  8. 局部函数可以使用async和/或unsafe修饰符,不允许使用其它修饰符;
  9. 局部函数无法使用属性;
  10. 局部函数在IDE中还不是非常友好:没有“提取局部函数重构”(目前为止),如果一个局部函数的代码被破坏了,您将在IDE中收到很多“波浪线”。

这是Benchmark测试结果:

    private static int n = 42;

    [Benchmark]
public bool DelegateInvocation()
{
Func<bool> fn = () => n == 42;
return fn();
} [Benchmark]
public bool LocalFunctionInvocation()
{
return fn();
bool fn() => n == 42;
}
Method Mean Error StdDev Allocated
  DelegateInvocation | 2.3035 ns | 0.0847 ns | 0.0869 ns |       0 B

LocalFunctionInvocation | 0.0142 ns | 0.0176 ns | 0.0137 ns | 0 B

不要因为差异而感到困惑,它看起来很大,但我几乎从来没有看到委托调用开销造成的真正问题。

原文:《Dissecting the local functions in C# 7》https://blogs.msdn.microsoft.com/seteplia/2017/10/03/dissecting-the-local-functions-in-c-7/

翻译:Sweet Tang

本文地址:http://www.cnblogs.com/tdfblog/p/dissecting-the-local-functions-in-c-7.html

欢迎转载,请在明显位置给出出处及链接。

C# 7 局部函数剖析的更多相关文章

  1. Python开发【第四章】:Python函数剖析

    一.Python函数剖析 1.函数的调用顺序 #!/usr/bin/env python # -*- coding:utf-8 -*- #-Author-Lian #函数错误的调用方式 def fun ...

  2. Python开发【第四章】:函数剖析

    一.Python函数剖析 1.函数的调用顺序 #!/usr/bin/env python # -*- coding:utf-8 -*- #-Author-Lian #函数错误的调用方式 def fun ...

  3. javascript函数中的实例对象、类对象、局部变量(局部函数)

    定义 function Person(national,age) { this.age = age; //实例对象,每个示例不同 Person.national = national; //类对象,所 ...

  4. Javascript-全局函数和局部函数作用域的理解

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  5. C# 局部函数与事件

    本文告诉大家使用局部函数可能遇到的坑. 在以前,如果有一个事件public event EventHandler Foo和一个函数private void Program_Foo(object sen ...

  6. 2018-8-10-C#-局部函数与事件

    title author date CreateTime categories C# 局部函数与事件 lindexi 2018-08-10 19:16:52 +0800 2018-2-13 17:23 ...

  7. js中的局部函数和全局函数的调用

    //局部函数和全局函数的特点 function fc1(){ var name ="chenhao"; function fc2(){ var age = 30; alert(na ...

  8. opencv-6-图像绘制与opencv Line 函数剖析

    opencv-6-图像绘制与opencv Line 函数剖析 opencvc++qt 开始之前 越到后面, 写的越慢, 之前还抽空去看了下 学堂在线那篇文章提供的方法, 博客第一个人评论的我, 想想还 ...

  9. Dotnet的局部函数和委托的对比

    上一篇说了一下委托,这篇来说说局部函数和委托的对比.   把委托和局部函数放成前后篇,是因为这两个内容很像,用起来容易混. 需要了解委托相关内容,可以看这一篇 [传送门]   使用委托表达式(Lamb ...

随机推荐

  1. ScrollView嵌套ListView只显示一行

    错误描述 ScrollView嵌套ListView中导致ListView高度计算不正确,只显示一行. 解决方法 重写ListView的onMeasure方法,代码如下. @Override publi ...

  2. java 运行时常量、编译时常量、静态块执行顺序

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt223 常量是程序运行时恒定不变的量,许多程序设计语言都有某种方法,向编译器告 ...

  3. EF增删改查+使用Expression进行动态排序分页

    注:以下部分来自<ASP.NET MVC 企业级实战>一书的摘抄和改写以及部分个人学习心得. EF简单增删改查 增加 public static int Add() { using (No ...

  4. 201521123022 《Java程序设计》 第8周学习总结

    1.本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结集合与泛型相关内容. 2. 书面作业 Q1.List中指定元素的删除(题目4-1) Q1.1 实验总结 本题要求的是编写covnert ...

  5. 201521123064 《Java程序设计》第6周学习总结

    1. 本章学习总结 1.1 面向对象学习暂告一段落,请使用思维导图,以封装.继承.多态为核心概念画一张思维导图,对面向对象思想进行一个总结. 注1:关键词与内容不求多,但概念之间的联系要清晰,内容覆盖 ...

  6. 201521123030《Java程序设计》第5周学习总结

    1. 本周学习总结 1.代码阅读:Child压缩包内源代码 1.1 com.parent包中Child.java文件能否编译通过?哪句会出现错误?试改正该错误.并分析输出结果. 因为i在类中是priv ...

  7. 201521123064 《Java程序设计》第4周学习总结

    1. 本章学习总结 1.1 尝试使用思维导图总结有关继承的知识点. 1.2 使用常规方法总结其他上课内容. ① 以上周PTA实验"形状"为例,Circle类和Rectangle类中 ...

  8. 201521123076《java程序设计》第三周学习总结

    1. 本周学习总结 2.书面作业 Q1.代码阅读 public class Test1 { private int i = 1;//这行不能修改 private static int j = 2; p ...

  9. 201521123103 《Java学习笔记》第二周学习笔记

    一.本周学习总结 1.学习了数据类型的使用:整数类型.浮点类型. boolean类型.数组等以及类型的转换,最重要的是学会了import引用包: 2.学习了string类对象的拼接.字符串池.枚举类型 ...

  10. 201521123088 《Java程序设计》第1周学习总结

    第1周学习总结 1.本周学习总结本周我们正式开始了对一门新的编程语言java的学习.Java是一门面向对象编程语言,不仅吸收了C++语言的各种优点,还摒弃了C++里难以理解的多继承.指针等概念,因此J ...