C#7局部函数剖析
局部函数是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表达式不⽀持迭代器块:
中的 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:迭代器块的局部函数writeline函数
不能在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 a和b驻留在同⼀闭包: 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()
=> GetHashCode();
internal int ImplicitCapture_b__1()
=> this.arg;
}
private Func<int> func;
public void ImplicitCapture(int arg)
{
var c__DisplayClass1_ = new c__DisplayClass1_0()
{
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论