标签:delegate ted ++ 循环 闭包 tap toc 比较 tac
本文例子基于 .NET Core 3.1 的编译结果反编译得出结论,不同版本的编译器的编译结果可能不一致,因此本文仅供参考。为节省篇幅和便于阅读,大部分例子只写出编译成的IL等效的C#代码,不直接展示IL。
本文不讨论的内容:
Lambda 表达式
如何构建表达式树。Lambda 表达式
的好基友们 匿名方法(delegate(int x){return x+1;} 这种)
以及 Local Function
。首先我们来看下一个委托是怎么被实例化的。
C# 代码
public class Test
{
public Test()
{
Action action = Foo;
}
private void Foo()
{
}
}
为节约篇幅,只列出构造函数中的 IL代码
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 2
.locals init (
[0] class [System.Runtime]System.Action action
)
// [7 9 - 7 22]
IL_0000: ldarg.0 // this
IL_0001: call instance void [System.Runtime]System.Object::.ctor()
IL_0006: nop
// [8 9 - 8 10]
IL_0007: nop
// [9 13 - 9 33]
IL_0008: ldarg.0 // this
IL_0009: ldftn instance void TestApp.Test::Foo()
IL_000f: newobj instance void [System.Runtime]System.Action::.ctor(object, native int)
IL_0014: stloc.0 // action
// [10 9 - 10 10]
IL_0015: ret
} // end of method Test::.ctor
其中关键的部分是下面三行
// 加载 this 对象引用 到 evaluation stack
ldarg.0 // this
// 加载 Foo 方法指针 到 evaluation stack
ldftn instance void TestApp.Test::Foo()
// 将上述两项传入构造函数
newobj instance void [System.Runtime]System.Action::.ctor(object, native int)
简单来说,就是调用委托的构造函数的时候传入了两个参数,第一个是实例方法当前实例的对象引用,第二个是实例方法指针。这个实例对象引用被维护在委托实例的 Target 属性上。
简单地通过在上述构造函数中加一行来说明。
public Test()
{
Action action = Foo;
// 走到这里时会输出 True
Console.WriteLine(action.Target == this);
}
那将上述的 Foo 方法改成静态方法会发生什么呢?
public class Test
{
public Test()
{
Action action = Foo;
}
private static void Foo()
{
}
}
对应的 构造函数 IL 代码
.method public hidebysig specialname rtspecialname instance void
.ctor() cil managed
{
.maxstack 2
.locals init (
[0] class [System.Runtime]System.Action action
)
// [7 9 - 7 22]
IL_0000: ldarg.0 // this
IL_0001: call instance void [System.Runtime]System.Object::.ctor()
IL_0006: nop
// [8 9 - 8 10]
IL_0007: nop
// [9 13 - 9 33]
IL_0008: ldnull // 注意这里,从 ldarg.0 变成了 ldnull。
IL_0009: ldftn void TestApp.Test::Foo()
IL_000f: newobj instance void [System.Runtime]System.Action::.ctor(object, native int)
IL_0014: stloc.0 // action
// [10 9 - 10 10]
IL_0015: ret
} // end of method Test::.ctor
和实例方法相比,构建委托的第一个参数从方法所关联的实例变成了null。
为什么委托引用实例方法要维护一个this?因为实例方法中保不准会用到this。在 IL 层面,实例方法中,this 总是第一个参数。这也就是为什么 ldarg.0 是 this 的原因了。
为了证明后面委托执行的时候要用用到这个 Target,在做一个小实验。
public class Test
{
private readonly int _id;
public Test(int id)
{
_id = id;
}
public void Foo()
{
Console.WriteLine(_id);
}
}
class Program
{
static void Main(string[] args)
{
var a = new Test(1);
var b = new Test(2);
Action action = a.Foo;
action(); // 输出 1
Console.WriteLine(action.Target == a); // 输出 True
var targetField =
typeof(Delegate)
.GetField("_target",
BindingFlags.Instance | BindingFlags.NonPublic);
// 将 action 的 Target 改成对象 b
targetField.SetValue(action, b);
action(); // 输出 2
Console.WriteLine(action.Target == b); // 输出 True
}
}
没错 Target 一变,方法所绑定的 实例 也变了。
不同场景下创建的Lambda 表达式会有不同的实现方式,这里指语法糖被编译成 IL 之后的真实形态。
为节省篇幅做出6个提前说明:
去重后总结出下面4种基本CASE
public class Test
{
public void Foo()
{
Func<int, int> func = x => x + 1;
}
}
编译后等效 C# 代码
public class Test
{
// 匿名内部类
private class AnonymousNestedClass
{
// 缓存匿名类单例
public static readonly AnonymousNestedClass _anonymousInstance;
// 缓存委托实例
public static Func<int, int> _func;
static AnonymousNestedClass()
{
_anonymousInstance = new AnonymousNestedClass();
}
internal int AnonymousMethod(int x)
{
return x + 1;
}
}
public void Foo()
{
// 这里是编译器的一个优化,委托实例是单例
if (AnonymousNestedClass._func == null)
{
AnonymousNestedClass._func =
new Func<int, int>(AnonymousNestedClass._anonymousInstance.AnonymousMethod);
}
Func<int, int> func = AnonymousNestedClass._func;
}
}
我们的Lambda表达式实质上变成了匿名类型的实例方法。开篇讲构建委托实例的例子的目的就在这了。
public class Test
{
public void Foo()
{
int y = 1;
Func<int, int> func = x => x + y;
}
}
编译后等效 C# 代码
public class Test
{
// 匿名内部类
private class AnonymousNestedClass
{
// 局部变量变成了匿名类实例字段
public int _y;
internal int AnonymousMethod(int x)
{
return x + _y;
}
}
public void Foo()
{
AnonymousNestedClass anonymousInstance = new AnonymousNestedClass();
// 对局部变量的赋值变成了对匿名类型实例字段的赋值
anonymousInstance._y = 1;
// 委托没有缓存了,每次都要重新实例化
Func<int, int> func = new Func<int, int>(anonymousInstance.AnonymousMethod);
}
}
public class Test
{
private int _y = 1;
public void Foo()
{
Func<int, int> func = x => x + _y;
}
}
编译后等效 C# 代码
public class Test
{
private int _y = 1;
public void Foo()
{
Func<int, int> func = new Func<int, int>(this.AnonymousMethod);
}
// Lambda 表达式 变成了当前类型的匿名实例方法
internal int AnonymousMethod(int x)
{
return x + _y;
}
}
插一句话,看到这里,相信你应该明白最近园子里讨论比较多的所谓Task.Run
导致“内存泄漏”的真实原因了。
public class Test
{
private static int _y = 1;
public static void Bar()
{
Func<int, int> func = x => x + _y;
}
}
编译后等效 C# 代码
public class Test
{
// 匿名内部类
private class AnonymousNestedClass
{
// 缓存匿名类单例
public static readonly AnonymousNestedClass _anonymousInstance;
// 缓存委托实例
public static Func<int, int> _func;
static AnonymousNestedClass()
{
_anonymousInstance = new AnonymousNestedClass();
}
internal int AnonymousMethod(int x)
{
// 实际使用原来的静态字段
return x + Test._y;
}
}
private static int _y = 1;
public static void Bar()
{
if (AnonymousNestedClass._func == null)
{
AnonymousNestedClass._func =
new Func<int, int>(AnonymousNestedClass._anonymousInstance.AnonymousMethod);
}
Func<int, int> func = AnonymousNestedClass._func;
}
}
class Program
{
static void Main(string[] args)
{
List<Func<int>> list = new List<Func<int>>();
for (int i = 0; i < 3; i++)
{
list.Add(() => i);
}
for (int i = 0; i < 3; i++)
{
Console.WriteLine(list[i]());
}
Console.WriteLine(list.Distinct().Count());
}
}
这种场景下,类似于上述的 CASE 2。我们通过下面的编译后等效代码来理解下每次都输出三的原因。
class Program
{
// 匿名内部类
private class AnonymousNestedClass
{
public int _i;
internal int AnonymousMethod()
{
return _i;
}
}
static void Main(string[] args)
{
List<Func<int>> list = new List<Func<int>>();
AnonymousNestedClass anonymousInstance = new AnonymousNestedClass();
for (anonymousInstance._i = 0;
anonymousInstance._i < 3;
anonymousInstance._i++)
{
// 退出循环时,anonymousInstance._i会变成3
// 每次委托实例的Target都是同一个对象
// 所以最后调用这三个委托的时候,都会得到相同的结果
list.Add(new Func<int>(anonymousInstance.AnonymousMethod));
}
for (int i = 0; i < 3; i++)
{
Console.WriteLine(list[i]());
}
}
}
那如果最后想要顺利地输出0 1 2,该怎么做呢。
class Program
{
static void Main(string[] args)
{
List<Func<int>> list = new List<Func<int>>();
for (int i = 0; i < 3; i++)
{
// 加个中间变量就可以了
int tmp = i;
list.Add(() => tmp);
}
for (int i = 0; i < 3; i++)
{
Console.WriteLine(list[i]());
}
Console.WriteLine(list.Distinct().Count());
}
}
相当于变成了这样
class Program
{
// 匿名内部类
private class AnonymousNestedClass
{
public int _tmp;
internal int AnonymousMethod()
{
return _tmp;
}
}
static void Main(string[] args)
{
List<Func<int>> list = new List<Func<int>>();
for (int i = 0; i < 3; i++)
{
// 每个委托的Target不一样,最后的执行结果也就不一样了
AnonymousNestedClass anonymousInstance = new AnonymousNestedClass();
anonymousInstance._tmp = i;
list.Add(new Func<int>(anonymousInstance.AnonymousMethod));
}
for (int i = 0; i < 3; i++)
{
Console.WriteLine(list[i]());
}
}
}
有人问我,Task老师,发生甚么事了? 我就用了一个Lambda 表达式。
标签:delegate ted ++ 循环 闭包 tap toc 比较 tac
原文地址:https://www.cnblogs.com/blurhkh/p/14123511.html