【C#基础概念】本地函数语法

本文全文来自:MSDN C#编程手册

本地函数语法

本地函数被定义为包含成员中的嵌套方法。 其定义具有以下语法:

<modifiers> <return-type> <method-name> <parameter-list>

 

可以将以下修饰符用于本地函数:
async
unsafe
static (在 C# 8.0 和更高版本中)。 静态本地函数无法捕获局部变量或实例状态。
extern (在 C# 9.0 和更高版本中)。 外部本地函数必须为 static 。
在包含成员中定义的所有本地变量(包括其方法参数)都可在非静态本地函数中访

 与方法定义不同,本地函数定义不能包含成员访问修饰符。 因为所有本地函数都是私有的,包括访问修饰符(如
private 关键字)会生成编译器错误 CS0106“修饰符‘private’对于此项无效”。
以下示例定义了一个名为 AppendPathSeparator 的本地函数,该函数对于名为 GetText 的方法是私有的:

private static string GetText(string path, string filename)
{
var reader = File.OpenText($"{AppendPathSeparator(path)}{filename}");
var text = reader.ReadToEnd();
return text;
string AppendPathSeparator(string filepath)
{
return filepath.EndsWith(@"\") ? filepath : filepath + @"\";
}
}

从 C# 9.0 开始,你可以将特性应用于本地函数、其参数和类型参数,如以下示例所示:

#nullable enable
private static void Process(string?[] lines, string mark)
{
foreach (var line in lines)
{
if (IsValid(line))
{
// Processing logic...
}
}
bool IsValid([NotNullWhen(true)] string? line)
{
return !string.IsNullOrEmpty(line) && line.Length >= mark.Length;
}
}

前面的示例使用特殊属性来帮助编译器在可为空的上下文中进行静态分析。
本地函数和异常
从 C# 9.0 开始,你可以将属性应用于本地函数、其参数和类型参数,如以下示例所示:
前面的示例使用特殊属性来帮助编译器在可为空的上下文中进行静态分析。
本地函数的一个实用功能是可以允许立即显示异常。 对于方法迭代器,仅在枚举返回的序列时才显示异常,而非
在检索迭代器时。 对于异步方法,在等待返回的任务时,将观察到异步方法中引发的任何异常。
以下示例定义 OddSequence 方法,用于枚举指定范围中的奇数。 因为它会将一个大于 100 的数字传递到
OddSequence 迭代器方法,该方法将引发 ArgumentOutOfRangeException。 如示例中的输出所示,仅当循环访问
数字时才显示异常,而非检索迭代器时。

using System;
using System.Collections.Generic;
public class IteratorWithoutLocalExample
{
public static void Main()
{
IEnumerable<int> xs = OddSequence(50, 110);
Console.WriteLine("Retrieved enumerator...");
foreach (var x in xs) // line 11
{
Console.Write($"{x} ");
}
}
public static IEnumerable<int> OddSequence(int start, int end)
{
if (start < 0 || start > 99)
throw new ArgumentOutOfRangeException(nameof(start), "start must be between 0 and 99.");
if (end > 100)
throw new ArgumentOutOfRangeException(nameof(end), "end must be less than or equal to 100.");
if (start >= end)
throw new ArgumentException("start must be less than end.");
for (int i = start; i <= end; i++)
{
if (i % 2 == 1)
yield return i;
}
}
}
// The example displays the output like this:
//
// Retrieved enumerator...
// Unhandled exception. System.ArgumentOutOfRangeException: end must be less than or equal to 100.
(Parameter 'end')
// at IteratorWithoutLocalExample.OddSequence(Int32 start, Int32 end)+MoveNext() in
IteratorWithoutLocal.cs:line 22
// at IteratorWithoutLocalExample.Main() in IteratorWithoutLocal.cs:line 11

如果将迭代器逻辑放入本地函数,则在检索枚举器时会引发参数验证异常,如下面的示例所示:

using System;
using System.Collections.Generic;
public class IteratorWithLocalExample
{
public static void Main()
{
IEnumerable<int> xs = OddSequence(50, 110); // line 8
Console.WriteLine("Retrieved enumerator...");
foreach (var x in xs)
{
Console.Write($"{x} ");
}
}
public static IEnumerable<int> OddSequence(int start, int end)
{
if (start < 0 || start > 99)
throw new ArgumentOutOfRangeException(nameof(start), "start must be between 0 and 99.");
if (end > 100)
throw new ArgumentOutOfRangeException(nameof(end), "end must be less than or equal to 100.");
if (start >= end)
throw new ArgumentException("start must be less than end.");
return GetOddSequenceEnumerator();
IEnumerable<int> GetOddSequenceEnumerator()
{
for (int i = start; i <= end; i++)
{
if (i % 2 == 1)
yield return i;
}
}
}
}
// The example displays the output like this:
//
// Unhandled exception. System.ArgumentOutOfRangeException: end must be less than or equal to 100.
(Parameter 'end')
// at IteratorWithLocalExample.OddSequence(Int32 start, Int32 end) in IteratorWithLocal.cs:line 22
// at IteratorWithLocalExample.Main() in IteratorWithLocal.cs:line 8

本地函数与 Lambda 表达式

public static int LocalFunctionFactorial(int n)
{
return nthFactorial(n);
int nthFactorial(int number) => number < 2
? 1
: number * nthFactorial(number - 1);
}

乍看之下,本地函数和 lambda 表达式非常相似。 在许多情况下,选择使用 Lambda 表达式还是本地函数是风格
和个人偏好的问题。 但是,应该注意,从两者中选用一种的时机和条件其实是存在差别的。
让我们检查一下阶乘算法的本地函数实现和 lambda 表达式实现之间的差异。 下面是使用本地函数的版本:

public static int LambdaFactorial(int n)
{
Func<int, int> nthFactorial = default(Func<int, int>);
nthFactorial = number => number < 2
? 1
: number * nthFactorial(number - 1);
return nthFactorial(n);
}

命名
本地函数的命名方式与方法相同。 Lambda 表达式是一种匿名方法,需要分配给 delegate 类型的变量,通常是
Action 或 Func 类型。 声明本地函数时,此过程类似于编写普通方法;声明一个返回类型和一个函数签名。

函数签名和 Lambda 表达式类型
Lambda 表达式依赖于为其分配的 Action / Func 变量的类型来确定参数和返回类型。 在本地函数中,因为语法
非常类似于编写常规方法,所以参数类型和返回类型已经是函数声明的一部分。
明确赋值

Lambda 表达式是在运行时声明和分配的对象。 若要使用 Lambda 表达式,需要对其进行明确赋值:必须声明要
分配给它的 Action / Func 变量,并为其分配 Lambda 表达式。 请注意, LambdaFactorial 必须先声明和初始化
Lambda 表达式 nthFactorial ,然后再对其进行定义。 否则,会导致分配前引用 nthFactorial 时出现编译时错
误。

本地函数在编译时定义。 由于未将它们分配给变量,因此可以从范围内的任意代码位置引用它们;在第一个示例
LocalFunctionFactorial 中,我们可以在 return 语句的上方或下方声明本地函数,而不会触发任何编译器错
误。

这些区别意味着使用本地函数创建递归算法会更轻松。 你可以声明和定义一个调用自身的本地函数。 必须声明
Lambda 表达式,赋给默认值,然后才能将其重新赋给引用相同 Lambda 表达式的主体。

实现为委托

Lambda 表达式在声明时转换为委托。 本地函数更加灵活,可以像传统方法一样编写,也可以作为委托编写。 只
有在用作委托时,本地函数才转换为委托。

如果声明了本地函数,但只是通过像调用方法一样调用该函数来引用该函数,它将不会转换成委托

变量捕获

会捕捉类和函数的变量包括参数, 明确分配的规则也会影响本地函数或 Lambda 表达式捕获的任何变量。 编译器可以执行静态分析,因此本地函
数能够在封闭范围内明确分配捕获的变量。 请看以下示例:

int M()
{
int y;
LocalFunction();
return y;
void LocalFunction() => y = 0;
}

编译器可以确定 LocalFunction 在调用时明确分配 y 。 因为在 return 语句之前调用了 LocalFunction ,所以
在 return 语句中明确分配了 y 。
请注意,当本地函数捕获封闭范围中的变量时,本地函数将作为委托类型实现。

堆分配
根据它们的用途,本地函数可以避免 Lambda 表达式始终需要的堆分配。 如果本地函数永远不会转换为委托,并
且本地函数捕获的变量都不会被其他转换为委托的 lambda 或本地函数捕获,则编译器可以避免堆分配。

请看以下异步示例:

public async Task<string> PerformLongRunningWorkLambda(string address, int index, string name)
{
if (string.IsNullOrWhiteSpace(address))
throw new ArgumentException(message: "An address is required", paramName: nameof(address));
if (index < 0)
throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non negative");
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));
Func<Task<string>> longRunningWorkImplementation = async () =>
{
var interimResult = await FirstWork(address);
var secondResult = await SecondStep(index, name);
return $"The results are {interimResult} and {secondResult}. Enjoy.";
};
return await longRunningWorkImplementation();
}

该 lambda 表达式的闭包包含 address 、 index 和 name 变量。 就本地函数而言,实现闭包的对象可能为
struct 类型。 该结构类型将通过引用传递给本地函数。 实现中的这个差异将保存在分配上。
Lambda 表达式所需的实例化意味着额外的内存分配,后者可能是时间关键代码路径中的性能因素。 本地函数不
会产生这种开销。 在以上示例中,本地函数版本具有的分配比 Lambda 表达式版本少 2 个。
如果你知道本地函数不会转换为委托,并且本地函数捕获的变量都不会被其他转换为委托的 lambda 或本地函数
捕获,则可以通过将本地函数声明为 static 本地函数来确保避免在堆上对其进行分配。 请注意,此功能在 C#
8.0 及更高版本中提供。

注意:

等效于此方法的本地函数还将类用于闭包。 实现详细信息包括本地函数的闭包是作为 class 还是 struct 实现。 本地函
数可能使用 struct ,而 lambda 将始终使用 class 。

public async Task<string> PerformLongRunningWork(string address, int index, string name)
{
if (string.IsNullOrWhiteSpace(address))
throw new ArgumentException(message: "An address is required", paramName: nameof(address));
if (index < 0)
throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non negative");
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));
return await longRunningWorkImplementation();
async Task<string> longRunningWorkImplementation()
{
var interimResult = await FirstWork(address);
var secondResult = await SecondStep(index, name);
return $"The results are {interimResult} and {secondResult}. Enjoy.";
}
}

yield 关键字的用法

在本示例中尚未演示的最后一个优点是,可将本地函数作为迭代器实现,使用 yield return 语法生成一系列值。

public IEnumerable<string> SequenceToLowercase(IEnumerable<string> input)
{
if (!input.Any())
{
throw new ArgumentException("There are no items to convert to lowercase.");
}
return LowercaseIterator();
IEnumerable<string> LowercaseIterator()
{
foreach (var output in input.Select(item => item.ToLower()))
{
yield return output;
}
}
}

Lambda 表达式中不允许使用 yield return 语句,请参阅编译器错误 CS1621。
虽然本地函数对 lambda 表达式可能有点冗余,但实际上它们的目的和用法都不一样。 如果想要编写仅从上下文
或其他方法中调用的函数,则使用本地函数更高效。

 

本地方法与 Lambda 的比较

1. 性能

当创建 Lambda 的时候,将会创建一个委托,这需要内存分配,因为委托是一个对象。而本地方法则不需要,它是一个真正的方法。

另外,本地方法可以更为有效地使用本地变量,Lambda 将变量放到类中,而本地方法可以使用结构,而不使用内存分配。

这意味着调用本地方法更为节约且可能内联。

2. 本地方法可以递归

Lambda 也可以实现递归,但是代码丑陋,您需要先赋予 lambda 为 null。本地方法可以更为自然地递归。

3. 本地方法可以使用泛型

Lambda 不能使用泛型。这是因为需要赋予一个实例类型的变量。

4. 本地方法可以实现迭代器

Lambda 不能使用 yield return (以及 yield break)关键字,以实现 IEnumerable<T> 返回函数。本地方法可以。

5. 本地方法更为易读

posted @ 2021-11-08 21:56  小林野夫  阅读(446)  评论(0编辑  收藏  举报
原文链接:https://www.cnblogs.com/cdaniu/