C# 中的本地函数

今天我们来聊一聊 C# 中的本地函数。本地函数是从 C# 7.0 开始引入,并在 C# 8.0 和 C# 9.0 中加以完善的。

引入本地函数的原因

我们来看一下微软 C# 语言首席设计师 Mads Torgersen 的一段话:

Mads Torgersen:
我们认为这个场景是有用的 —— 您需要一个辅助函数。您仅能在单个函数中使用它,并且它可能使用包含在该函数作用域内的变量和类型参数。另一方面,与 lambda 不同,您不需要将其作为第一类对象,因此您不必关心为它提供一个委托类型并分配一个实际的委托对象。另外,您可能希望它是递归的或泛型的,或者将其作为迭代器实现。[1]

正是 Mads Torgersen 所说的这个原因,让 C# 语言团队添加了对本地函数的支持。
本人在近期的项目中多次用到本地函数,发现它比使用委托加 Lambda 表达式的写法更加方便和清晰。

本地函数是什么

用最简单的大白话来说,本地函数就是方法中的方法,是不是一下子就理解了?不过,这样理解本地函数难免有点片面和肤浅。

我们来看一下官方对本地函数的定义:

本地函数是一种嵌套在另一个成员中的私有方法,仅能从包含它的成员中调用它。 [2]

定义中点出了三个重点:

  1. 本地函数是私有方法

  2. 本地函数是嵌套在另一成员中的方法。

  3. 只能从定义该本地函数的成员中调用它,其它位置都不可以。

其中,可以声明和调用本地函数的成员有以下几种:

  • 方法,尤其是迭代器方法和异步方法

  • 构造函数

  • 属性访问器

  • 事件访问器

  • 匿名方法

  • Lambda 表达式

  • 析构函数

  • 其它本地函数

举个简单的示例,在方法 M 中定义一个本地函数 add

public class C
{public void M(){int result = add(100, 200);// 本地函数 addint add(int a, int b) { return a + b; }}
}

本地函数都是私有的,目前可用的修饰符只有 asyncunsafestatic(静态本地函数无法访问局部变量和实例成员) 和 extern 四种。在包含成员中定义的所有本地变量和其方法参数都可在非静态的本地函数中访问。本地函数可以声明在其包含成员中的任意位置,但通常的习惯是声明在其包含成员的最后位置(即结束 } 之前)。

本地函数与Lambda表达式的比较

本地函数和我们熟知的 Lambda 表达式 [3]非常相似,比如上面示例中的本地函数,我们可以使用 Lambda 表达式实现如下:

public void M()
{// Lambda 表达式Func<int, int, int> add = (int a, int b) => a + b;int result = add(100, 200);
}

如此看来,似乎选择使用 Lambda 表达式还是本地函数只是编码风格和个人偏好问题。但是,应该注意到,使用它们的时机和条件其实是存在很大差异的。

我们来看一下获取斐波那契数列第 n 项的例子,其实现包含递归调用。

// 使用本地函数的版本
public static uint LocFunFibonacci(uint n)
{return Fibonacci(n);uint Fibonacci(uint num){if (num == 0) return 0;if (num == 1) return 1;return checked(Fibonacci(num - 2) + Fibonacci(num - 1));}
}
// 使用 Lambda 表达式的版本
public static uint LambdaFibonacci(uint n)
{Func<uint, uint> Fibonacci = null; //这里必须明确赋值Fibonacci = num => {if (num == 0) return 0;if (num == 1) return 1;return checked(Fibonacci(num - 2) + Fibonacci(num - 1));};return Fibonacci(n);
}

命名

本地函数的命名方式和类中的方法类似,声明本地函数的过程就像是编写普通方法。Lambda 表达式是一种匿名方法,需要分配给委托类型的变量,通常是 Action 或 Func 类型的变量。

参数和返回值类型

本地函数因为语法类似于普通方法,所以参数类型和返回值类型已经是函数声明的一部分。Lambda 表达式依赖于为其分配的 Action 或 Func 变量的类型来确定参数和返回值的类型。

明确赋值

本地函数是在编译时定义的方法。由于未将本地函数分配给变量,因此可以从包含它的成员的任意代码位置调用它们。在本例中,我们将本地函数 Fibonacci 定义在其包含方法 LocFunFibonacci 的 return 语句之后,方法体的结束 } 之前,而不会有任何编译错误。

而 Lambda 表达式是在运行时声明和分配的对象。使用 Lambda 表达式时,必须先对其进行明确赋值:声明要分配给它的 Action 或 Func 变量,并为其分配 Lambda 表达式,然后才能在后面的代码中调用它们。在本例中,我们首先声明并初始化了一个委托变量 Fibonacci, 然后将 Lambda 表达式赋值给了该委托变量。

这些区别意味着使用本地函数创建递归算法会更轻松。因为在创建递归算法时,使用本地函数和使用普通方法是一样的; 而使用 Lambda 表达式,则必须先声明并初始化一个委托变量,然后才能将其重新分配给引用相同 Lambda 表达式的主体。

变量捕获

我们使用 VS 编写或者编译代码时,编译器可以对代码执行静态分析,提前告知我们代码中存在的问题。

看下面一个例子:

static int M1()
{int num; //这里不用赋值默认值LocalFunction();return num; //OKvoid LocalFunction() => num = 8; // 本地函数
}static int M2()
{int num;    //这里必须赋值默认值(比如改为:int num = 0;),下面使用 num 的行才不会报错Action lambdaExp = () => num = 8; // Lambda 表达式lambdaExp();return num; //错误 CS0165 使用了未赋值的局部变量“num”
}

在使用本地函数时,因为本地函数是在编译时定义的,编译器可以确定在调用本地函数 LocalFunction 时明确分配 num。因为在 return 语句之前调用了 LocalFunction,也就在 return 语句前明确分配了 num,所以不会引发编译异常。
而在使用 Lambda 表达式时,因为 Lambda 表达式是在运行时声明和分配的,所以在 return 语句前,编译器不能确定是否分配了 num,所以会引发编译异常。

内存分配

为了更好地理解本地函数和 Lambda 表达式在分配上的区别,我们先来看下面两个例子,并看一下它们编译后的代码。

Lambda 表达式:

public class C
{public void M(){int c = 300;int d = 400;int num = c + d;//Lambda 表达式Func<int, int, int> add = (int a, int b) => a + b + c + d;var num2 = add(100, 200);}
}

使用 Lambda 表达式,编译后的代码如下:

public class C
{[CompilerGenerated]private sealed class <>c__DisplayClass0_0{public int c;public int d;internal int <M>b__0(int a, int b){return a + b + c + d;}}public void M(){<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();<>c__DisplayClass0_.c = 300;<>c__DisplayClass0_.d = 400;int num = <>c__DisplayClass0_.c + <>c__DisplayClass0_.d;Func<int, int, int> func = new Func<int, int, int>(<>c__DisplayClass0_.<M>b__0);int num2 = func(100, 200);}
}

可以看出,使用 Lambda 表达式时,编译后实际上是生成了包含实现方法的一个类,然后创建该类的一个对象并将其分配给了委托。因为要创建类的对象,所以需要额外的堆(heap)分配。

我们再来看一下具有同样功能的本地函数实现:

public class C
{public void M(){int c = 300;int d = 400;int num = c + d;var num2 = add(100, 200);//本地函数int add(int a, int b) { return a + b + c + d; }}
}

使用本地函数,编译后的代码如下:

public class C
{[StructLayout(LayoutKind.Auto)][CompilerGenerated]private struct <>c__DisplayClass0_0{public int c;public int d;}public void M(){<>c__DisplayClass0_0 <>c__DisplayClass0_ = default(<>c__DisplayClass0_0);<>c__DisplayClass0_.c = 300;<>c__DisplayClass0_.d = 400;int num = <>c__DisplayClass0_.c + <>c__DisplayClass0_.d;int num2 = <M>g__add|0_0(100, 200, ref <>c__DisplayClass0_);}[CompilerGenerated]private static int <M>g__add|0_0(int a, int b, ref <>c__DisplayClass0_0 P_2){return a + b + P_2.c + P_2.d;}
}

可以看出,使用本地函数时,编译后只是在包含类中生成了一个私有方法,因此调用时不需要实例化对象,不需要额外的堆(heap)分配。
当本地函数中使用到其包含成员中的变量时,编译器生成了一个结构体,并将此结构体的实例以引用(ref)方式传递到了本地函数,这也有助于节省内存分配。

综上所述,使用本地函数相比使用 Lambda 表达式更能节省时间和空间上的开销。

本地函数与异常

本地函数还有一个比较实用的功能是,可以在迭代器方法和异步方法中立即显示异常。

我们知道,迭代器方法的主体是延迟执行的,所以仅在枚举其返回的序列时才显示异常,而并非在调用迭代器方法时。
我们来看一个经典的迭代器方法的例子:

static void Main(string[] args)
{int[] list = new[] { 1, 2, 3, 4, 5, 6 };var result = Filter(list, null);Console.WriteLine(string.Join(',', result));
}public static IEnumerable<T> Filter<T>(IEnumerable<T> source, Func<T, bool> predicate)
{if (source == null) throw new ArgumentNullException(nameof(source));if (predicate == null) throw new ArgumentNullException(nameof(predicate));foreach (var element in source)if (predicate(element))yield return element;
}

运行上面的代码,由于迭代器方法的主体是延迟执行的,所以抛出异常的位置将发生在 string.Join(',', result) 所在的行,也就是在枚举返回的序列结果 result 时显示,如图:

如果我们把上面的迭代器方法 Filter 中的迭代器部分放入本地函数:

static void Main(string[] args)
{int[] list = new[] { 1, 2, 3, 4, 5, 6 };var result = Filter(list, null);Console.WriteLine(string.Join(',', result));
}public static IEnumerable<T> Filter<T>(IEnumerable<T> source, Func<T, bool> predicate)
{if (source == null) throw new ArgumentNullException(nameof(source));if (predicate == null) throw new ArgumentNullException(nameof(predicate));//本地函数IEnumerable<T> Iterator(){foreach (var element in source)if (predicate(element))yield return element;}return Iterator();
}

那么这时抛出异常的位置将发生在 Filter(list, null) 所在的行,也就是在调用 Filter 方法时显示,如图:

可以看出,使用了本地函数包装迭代器逻辑的写法,相当于把显示异常的位置提前了,这有助于我们更快的观察到异常并进行处理。

同理,在使用了 async 的异步方法中,如果把异步执行部分放入 async 的本地函数中,也有助于立即显示异常。由于篇幅问题这里不再举例,可以查看官方文档。

总结

综上所述,本地函数是方法中的方法,但它又不仅仅是方法中的方法,它还可以出现在构造函数、属性访问器、事件访问器等等成员中;本地函数在功能上类似于 Lambda 表达式,但它比 Lambda 表达式更加方便和清晰,在分配和性能上也比 Lambda 表达式略占优势;本地函数支持范型和作为迭代器实现;本地函数还有助于在迭代器方法和异步方法中立即显示异常。


相关链接:

  1. https://github.com/dotnet/roslyn/issues/3911 C# Design Meeting Notes ↩︎

  2. https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/local-functions 本地函数 ↩︎

  3. https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/lambda-expressions Lambda 表达式 ↩︎

作者 :技术译民 
出品 :技术译站(https://ITTranslator.cn/)

END

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/306765.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据结构——图-有向图和无向图的邻接表基础

#include <stdio.h> #include <stdlib.h> #define VertexType char //顶点的数据类型&#xff08;char&#xff09; #define VertexMax 20 //最大顶点个数 typedef struct ArcNode//边表 {int adjvex;//存储的是该顶点在顶点数组即AdjList[]中的位置 struct ArcN…

2019山科计算机专业分数线,2019山东科技大学研究生分数线汇总(含2016-2019历年复试)...

2019山东科技大学研究生分数线(含2016-2019历年复试)考研就是人生的第二次高考&#xff0c;是再一次改变自己命运的机会&#xff0c;所谓7分靠努力&#xff0c;3分靠填报&#xff0c;山东科技大学历年研究生复试分数线是2019-2020届考研学子十分关心的问题&#xff0c;以下是如…

b+树时间复杂度_第15期:索引设计(索引组织方式 B+ 树)

谈到索引&#xff0c;大家并不陌生。索引本身是一种数据结构&#xff0c;存在的目的主要是为了缩短数据检索的时间&#xff0c;最大程度减少磁盘 IO。任何有数据的场景几乎都有索引&#xff0c;比如手机通讯录、文件系统(ext4xfsfs)、数据库系统(MySQLOracle)。数据库系统和文件…

结合控制台程序和K8S的CronJob完成定时任务

前言 老黄前段时间遇到了一个数据清洗的需求&#xff0c;其实就是每天凌晨把昨天的数据清洗一遍&#xff0c;归归类。这是一个比较典型的定时任务的处理场景。定时任务可以说就一把利器&#xff0c;几乎每个公司都离不开&#xff0c;它的应用场景也不在少数&#xff0c;比如&am…

数据结构——图-有向带权图的邻接表基础

#include <stdio.h> #include <stdlib.h> #define VertexType char //顶点的数据类型&#xff08;char&#xff09; #define VertexMax 20 //最大顶点个数 typedef struct ArcNode//边表 {int adjvex;//存储的是该顶点在顶点数组即AdjList[]中的位置int weight; …

vs里面mfc是什么_最近!一大批人正在前往文安,究竟发生了什么?

阅读本文前&#xff0c;请您先点击上面蓝色字体“文安家乡群”再点击“关注”&#xff0c;这样您就可以加入文安家乡群了。最近&#xff0c;文安的火车站汽车站&#xff0c;来了一大批外地人&#xff0c;挤爆了文安车站…究竟发生了什么事儿&#xff1f;让这么多人蜂拥而至&…

html 去除max-height,HTML Style maxHeight用法及代码示例

maxHeight属性设置/返回元素的最大高度。 maxHeight属性仅影响block-level元素&#xff0c;绝对或固定位置元素。用法:它用于设置maxHeight属性&#xff1a;object.style.maxHeight "none|length|%|initial|inherit"它用于返回maxHeight属性&#xff1a;object.styl…

数据结构——图-有向带权图的邻接表

#include <stdio.h> #include <stdlib.h> #define VertexType char //顶点的数据类型&#xff08;char&#xff09; #define VertexMax 20 //最大顶点个数 typedef struct ArcNode//边表 {int adjvex;//存储的是该顶点在顶点数组即AdjList[]中的位置int weight; …

ASP.NET Core使用HostingStartup增强启动操作

概念在ASP.NET Core中我们可以使用一种机制来增强启动时的操作&#xff0c;它就是HostingStartup。如何叫"增强"操作&#xff0c;相信了解过AOP概念的同学应该都非常的熟悉。我们常说AOP使用了关注点分离的方式&#xff0c;增强了对现有逻辑的操作。而我们今天要说的…

计算机网络实验arp协议分析,计算机网络ARP地址协议解析实验报告

计算机网络ARP地址协议解析实验报告 (5页)本资源提供全文预览&#xff0c;点击全文预览即可全文预览,如果喜欢文档就下载吧&#xff0c;查找使用更方便哦&#xff01;9.9 积分计算机网络实验报告、实验目的:1. 掌握ARP协议的报文格式2. 掌握ARP协议的工作原理3. 理解ARP高速缓存…

数据结构——图-最短路径长度中最大的一个

#include<stdio.h> #include<string.h> #define INF 32767 #define MAXVEX 30 int dist[MAXVEX]; //建立dist数组int path[MAXVEX]; //建立path数组int S[MAXVEX]; //建立S数组typedef char VertexType;typedef struct graph {int n,e;VertexType vexs[MAXVE…

一个情怀引发的生产事故

在一个项目中&#xff0c;需要轻量级用到脚本语言&#xff0c;来提高应用服务的灵活性。因为知道Roslyn可以动态编辑C#&#xff0c;本着情怀&#xff0c;就自然用Roslyn来处理这块业务了。开在windows上执行&#xff0c;一次调用风平浪静&#xff0c;因为这个功能使用频次不高&…

python standardscaler_教你用python一步步解决“维度灾难”

全文共7016字&#xff0c;预计学习时长40分钟或更长现代科技时代产生和收集的数据越来越多。然而在机器学习中&#xff0c;太多的数据可不是件好事。某种意义上来说&#xff0c;特征或维度越多&#xff0c;越会降低模型的准确性&#xff0c;因为需要对更多的数据进行泛化——这…

c++的输入和输出

1. 输入/输出流的成员函数 put()函数 put函数常用的调用形式&#xff1a; cout.put(char ch); 功能&#xff1a;用于输出一个字符&#xff0c;还可以是ASCII代码&#xff08;或者是ASCII表达式&#xff09; cout.put(65)&#xff1b; get()函数 常用形式为&#xff1a;cin.g…

被 C# 的 ThreadStatic 标记的静态变量,都存放在哪里了?

一&#xff1a;背景 1. 讲故事前几天公号里有一位朋友留言说&#xff0c;你windbg玩的溜&#xff0c;能帮我分析下被 ThreadStatic 修饰的变量到底存放在哪里吗&#xff1f;能不能帮我挖出来????????????&#xff0c;其实这个问题问的挺深的&#xff0c;玩高级语言…

唐山师范学院计算机论文,唐山师范学院校园网络解决方案 毕业论文

唐山师范学院校园网络解决方案 毕业论文 (43页)本资源提供全文预览&#xff0c;点击全文预览即可全文预览,如果喜欢文档就下载吧&#xff0c;查找使用更方便哦&#xff01;9.90 积分1 唐山师范学院 专 科毕业论文 题 目 唐山师范学院校园网络解决方案 学 生 指导教师 年 级 200…

基本系统设备感叹号更新不了_电脑识别不了U盘?别紧张,免费教你如何解决...

你的电脑会出现识别不了U盘的情况吗&#xff1f;为什么会识别不了U盘你了解过吗&#xff1f;首先我们说一下&#xff0c;U盘作为一个办公必备品&#xff0c;用来存储文件以便随时使用&#xff0c;简单方便。但是如果某一天你的电脑突然之间无法识别U盘的话&#xff0c;就代表新…

群同态基本定理证明_群论(7): 群代数, 群表示基础

内容提要:1 群代数; 2 域上的有限维群代数和Maschke定理; 3 函数环; 4 代数闭域上的群表示论; 本文主要参考文献.本文的前置内容为:格罗卜&#xff1a;群论(1): 群, 同构定理, 循环群格罗卜&#xff1a;群论(2): 群作用, Sylow定理更多内容&#xff0c;请移步专栏目录:格罗卜&a…

c++的文件输入/输出

1文件的概述 根据文件中数据的组织形式分为&#xff1a; 1 文本文件 文本文件又称ASCII文件&#xff0c;它的每一个字节存放一个ASCII代码&#xff0c;代表一个字符 输出文本&#xff1a;接收从内存输出的数据 输出文件&#xff0c;向它写入数据 ofstream fout; 输入文件&…

南阳理工计算机全国排名,全国工科实力最强的10所高校排名,

评价工科实力一个非常明显的指标就是学科实力&#xff0c;2017年公布的第四次学科评估结果就是最好的参考。下面就以学科评估排名全国5%的学科数作为评比基准&#xff0c;对国内主要高校的工科实力进行一个排名&#xff0c;结果如下&#xff1a;前3甲&#xff1a;清华大学、浙江…