C# 委托和闭包
前言
本文只是为了复习,巩固,和方便查阅,一些知识点的详细知识会通过相关链接和文献标记出来。
委托是什么
大部分的解释是 委托是一个对方法的引用,可以不用自己执行,而是转交给其他对象。就好比每天都有一个黄毛旅行者,给npc做委托任务一样,npc并不是自己去做任务。
于是我们可以有以下代码,delegate
就是声明一个委托,它的作用是调用sum
方法
// See https://aka.ms/new-console-template for more information
using System.Diagnostics.CodeAnalysis;
using System.Threading.Channels;
Foo foo = sum;
Console.Write(foo(1, 2));
static int sum(int a, int b) => a + b;
static int minus(int a, int b) => a - b;
internal delegate int Foo(int a, int b);
可能我们还见过其他写法,比如 Func
和 Action
,他们的区别在下面的代码里面有所展示,Func
存在返回值,Action
是void
Func<int, int, int> func = Sum;
Action ac = OutPut;
func.Invoke(1, 2);
static void OutPut()
{
Console.WriteLine("action");
}
如果反编译其中的实现,一眼顶针~
namespace System
{
/// <summary>Encapsulates a method that has two parameters and returns a value of the type specified by the <typeparamref name="TResult" /> parameter.</summary>
/// <param name="arg1">The first parameter of the method that this delegate encapsulates.</param>
/// <param name="arg2">The second parameter of the method that this delegate encapsulates.</param>
/// <typeparam name="T1">The type of the first parameter of the method that this delegate encapsulates.</typeparam>
/// <typeparam name="T2">The type of the second parameter of the method that this delegate encapsulates.</typeparam>
/// <typeparam name="TResult">The type of the return value of the method that this delegate encapsulates.</typeparam>
/// <returns>The return value of the method that this delegate encapsulates.</returns>
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
}
public delegate void Action();
委托内部是什么
如果细心的话,会从 Func
发现一点线索 Invoke
,我们把最开始的代码变为IL看看,嗯,也是存在 Foo::Invoke
// [7 1 - 7 26]
IL_000d: ldloc.0 // foo
IL_000e: ldc.i4.1
IL_000f: ldc.i4.2
IL_0010: callvirt instance int32 Foo::Invoke(int32, int32)
IL_0015: call void [System.Console]System.Console::Write(int32)
IL_001a: nop
IL_001b: nop
IL_001c: nop
IL_001d: ret
把Foo
在完整的展示出来,可以知道,它是由MulticastDelegate
继承得到,以及存在三个方法 Invoke BeginInvoke EndInvoke
,把原来的方法放在了object
中,再Foo::Invoke
有关MulticastDelegate
(多播委托)可以阅读 https://learn.microsoft.com/zh-cn/dotnet/api/system.multicastdelegate?view=net-7.0
.class private sealed auto ansi
Foo
extends [System.Runtime]System.MulticastDelegate
{
.method public hidebysig specialname rtspecialname instance void
.ctor(
object 'object',
native int 'method'
) runtime managed
{
// Can't find a body
} // end of method Foo::.ctor
.method public hidebysig virtual newslot instance int32
Invoke(
int32 a,
int32 b
) runtime managed
{
// Can't find a body
} // end of method Foo::Invoke
.method public hidebysig virtual newslot instance class [System.Runtime]System.IAsyncResult
BeginInvoke(
int32 a,
int32 b,
class [System.Runtime]System.AsyncCallback callback,
object 'object'
) runtime managed
{
// Can't find a body
} // end of method Foo::BeginInvoke
.method public hidebysig virtual newslot instance int32
EndInvoke(
class [System.Runtime]System.IAsyncResult result
) runtime managed
{
// Can't find a body
} // end of method Foo::EndInvoke
} // end of class Foo
委托的多个调用和不变性
就好比旅行者可以接到4个委托一下,这里Foo
也可以一次性接到多个方法,要不然为啥叫多播委托呢
Foo foo = Sum;
foo += Minus;
Console.Write(foo(1, 2)); //输出-1
应该好理解,执行最后一个方法。如果想要一个个执行,可以这么做 foo.GetInvocationList()
现在我们来看看这个不变性,顾名思义就是委托创建后是始终不变的。来一段代码,现在我们知道了委托也是个引用,那么假如我在re
移除一个方法,会不会影响到foo
?答案是不会。证明完毕~
Foo foo = Sum;
foo += Minus;
Foo re = foo;
re -= Minus;
foreach (var del in foo.GetInvocationList())
{
Console.WriteLine(del.Method);
}
闭包
可以理解为 一个代码块(在C#中,指的是匿名方法或者Lambda表达式,也就是匿名函数),并且这个代码块使用到了代码块以外的变量,于是这个代码块和用到的代码块以外的变量(上下文)被“封闭地包在一起”
这下看懂了(
以下代码就存在一个变量i,那为啥会输入全是5呢?
public static class Test
{
public static void Foo()
{
List<Action> ac = new List<Action>();
for (int i = 0; i < 5; i++)
{
ac.Add(() =>
{
Console.WriteLine(i);
});
}
foreach (var action in ac)
{
action.Invoke();
}
}
}
5
5
5
5
5
把代码翻译成IL,可以得到有效线索,首先 ac.Add(() => { Console.WriteLine(i);});
被生成了一个类 c__DisplayClass0_0
,存在字段 .field public int32 i
,那么本来应该是处于循环的i被c__DisplayClass0_0
赋予了更长的生命周期,加上for循环看做是同一个外部变量,导致了这个问题的出现。
.class nested private sealed auto ansi beforefieldinit
'<>c__DisplayClass0_0'
extends [System.Runtime]System.Object
{
.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor()
= (01 00 00 00 )
.field public int32 i
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 8
IL_0000: ldarg.0 // this
IL_0001: call instance void [System.Runtime]System.Object::.ctor()
IL_0006: nop
IL_0007: ret
} // end of method '<>c__DisplayClass0_0'::.ctor
.method assembly hidebysig instance void
'<Foo>b__0'() cil managed
{
.maxstack 8
// [18 17 - 18 18]
IL_0000: nop
// [19 21 - 19 42]
IL_0001: ldarg.0 // this
IL_0002: ldfld int32 Delegate.Test/'<>c__DisplayClass0_0'::i
IL_0007: call void [System.Console]System.Console::WriteLine(int32)
IL_000c: nop
// [20 17 - 20 18]
IL_000d: ret
} // end of method '<>c__DisplayClass0_0'::'<Foo>b__0'
} // end of class '<>c__DisplayClass0_0'
怎么解决闭包问题
实际上vs已经很贴心的提示了,如果非得使用闭包,可以采取以下两种办法
for (int i = 0; i < 5; i++)
{
var i1 = i;
ac.Add(() => { Console.WriteLine(i1);});
}
List<int> li = new List<int> { 1, 2, 3, 4, 5 };
foreach (var value in li)
{
ac.Add(() => { Console.WriteLine(value); });
}
参考链接和文件代码
https://www.cnblogs.com/cdaniu/p/15352317.html
《C#8.0本质论》委托
《C#图解教程》委托
https://github.com/yinghualuowu/blogsCodeSimple/tree/main/Delegate