前言

最近 Task.Run 相关的话题在园子里讨论的比较热闹。其中有个比较重要的配角,传给 Task.Run 的委托。而这个委托是通过 Lambda 表达式 来构建的。那 Lambda 表达式到底是个什么?

本文例子基于 .NET Core 3.1 的编译结果反编译得出结论,不同版本的编译器的编译结果可能不一致,因此本文仅供参考。为节省篇幅和便于阅读,大部分例子只写出编译成的IL等效的C#代码,不直接展示IL。

本文不讨论的内容:

  1. Lambda 表达式如何构建表达式树。
  2. 闭包的概念。
  3. Lambda 表达式 的好基友们 匿名方法(delegate(int x){return x+1;} 这种) 以及 Local Function

    若需了解C#中如何引入闭包的概念以及Local Function和Lambda 表达式的区别,可参考我两年前的一篇博客

    本文仅代表作者本人现阶段的理解,若有不对的地方或不同的见解,欢迎留言。

预备知识,理解委托的构成

首先我们来看下一个委托是怎么被实例化的。

引用实例方法的委托

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 表达式的实际编译结果

不同场景下创建的Lambda 表达式会有不同的实现方式,这里指语法糖被编译成 IL 之后的真实形态。

为节省篇幅做出6个提前说明:

  1. 实例构造函数中Lambda 表达式的实现与普通实例方法实现一致。
  2. 静态构造函数中Lambda 表达式的实现与普通的静态方法实现一致。
  3. 静态类型的静态方法中Lambda 表达式的实现与非静态类型的静态方法实现一致。
  4. 不捕获外部变量时,实例方法中的 Lambda 表达式的实现与静态方法实现一致。
  5. 捕获外部方法中的局部变量时,实例方法中的 Lambda 表达式的实现与静态方法实现一致。
  6. Lambda 表达式,有无参数,有无返回值,实现一致。

去重后总结出下面4种基本CASE

CASE 1 没有捕获任何外部变量的Lambda 表达式

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表达式实质上变成了匿名类型的实例方法。开篇讲构建委托实例的例子的目的就在这了。

CASE 2 捕获了外部方法局部变量的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);
}
}

CASE 3 实例方法中捕获了实例字段的Lambda 表达式

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导致“内存泄漏”的真实原因了。

CASE 4 静态方法中的捕获了当前类型静态字段的Lambda 表达式

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;
}
}

聊一聊循环中的Lambda 表达式

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]());
}
}
}

Lambda 表达式详解的更多相关文章

  1. Lambda表达式详解(例子详解)(转自:http://blog.csdn.net/damon316/article/details/51734661)

    Lambda表达式详解(例子详解)     lambda简介 lambda运算符:所有的lambda表达式都是用新的lambda运算符 " => ",可以叫他,“转到”或者 ...

  2. Java 8 Lambda 表达式详解

    一.Java 8 Lambda 表达式了解 参考:Java 8 Lambda 表达式 | 菜鸟教程 1.1 介绍: Lambda 表达式,也可称为闭包,是推动 Java 8 发布的最重要新特性. La ...

  3. 类型:.net;问题:C#lambda表达式;结果:Lambda表达式详解

    Lambda表达式详解   前言 1.天真热,程序员活着不易,星期天,也要顶着火辣辣的太阳,总结这些东西. 2.夸夸lambda吧:简化了匿名委托的使用,让你让代码更加简洁,优雅.据说它是微软自c#1 ...

  4. JAVA8之lambda表达式详解

    原文:http://blog.csdn.net/jinzhencs/article/details/50748202 lambda表达式详解 一.问题 1.什么是lambda表达式? 2.lambda ...

  5. Java8 Lambda表达式详解手册及实例

    先贩卖一下焦虑,Java8发于2014年3月18日,距离现在已经快6年了,如果你对Java8的新特性还没有应用,甚至还一无所知,那你真得关注公众号"程序新视界",好好系列的学习一下 ...

  6. Lambda表达式详解

    前言 1.天真热,程序员活着不易,星期天,也要顶着火辣辣的太阳,总结这些东西. 2.夸夸lambda吧:简化了匿名委托的使用,让你让代码更加简洁,优雅.据说它是微软自c#1.0后新增的最重要的功能之一 ...

  7. C# Lambda表达式详解,及Lambda表达式树的创建

    最近由于项目需要,刚刚学完了Action委托和Func<T>委托,发现学完了委托就必须学习lambda表达式,委托和Lambda表达式联合起来,才能充分的体现委托的便利.才能使代码更加简介 ...

  8. Lambda表达式详解(转载)

    原文链接:http://www.cnblogs.com/knowledgesea/p/3163725.html lambda简介 lambda运算符:所有的lambda表达式都是用新的lambda运算 ...

  9. (转)Lambda表达式详解

    本文转载自:http://www.cnblogs.com/knowledgesea/p/3163725.html 前言 1.天真热,程序员活着不易,星期天,也要顶着火辣辣的太阳,总结这些东西. 2.夸 ...

  10. Lambda表达式详解 (转)

    前言 1.天真热,程序员活着不易,星期天,也要顶着火辣辣的太阳,总结这些东西. 2.夸夸lambda吧:简化了匿名委托的使用,让你让代码更加简洁,优雅.据说它是微软自C#1.0后新增的最重要的功能之一 ...

随机推荐

  1. 跟我一起写 Makefile(十二)

    隐含规则 ---- 在我们使用Makefile时,有一些我们会经常使用,而且使用频率非常高的东西,比如,我们编译C/C++的源程序为中间目标文件(Unix下是[.o]文件,Windows下是[.obj ...

  2. 嵌入式ARM汇编详解

    文章目录 零.预备知识 1.ARM与X86 2.ARM中指令的执行 3.ARM的九种寻址方式 立即数寻址 寄存器寻址 寄存器间接寻址 寄存器偏移寻址 寄存器基址变址寻址 批量寄存器寻址 相对寻址 堆栈 ...

  3. NOIP 模拟 $29\; \rm 完全背包问题$

    题解 \(by\;zj\varphi\) 一道 \(\rm dp\) 题. 现将所有种类从小到大排序,然后判断,若最小的已经大于了 \(\rm l\),那么直接就是一个裸的完全背包,因为选的总数量有限 ...

  4. NOIP 模拟 $12\; \text{简单的填数}$

    题解 一个纯的贪心,被我搞成 \(dp\) 了,最后把错解删掉了,骗了 \(10pts\) 考虑如何贪心,设置一种二元组 \((x,l)\),\(x\) 表示当前值,\(l\) 表示当前最长连续长度. ...

  5. mysql如何查看是32位还是64位?

    mysql如何查看是32位还是64位? 1. mysql -V或mysql --version 或者 2. 进入mysql,输入命令:show variables like '%version_%';

  6. MySQL——分表,分库操作

    说明 大数据量并且访问频繁的表,将其分为若干个表.如果不分的话,进行一次查询就会将表锁住,导致不能进行其他操作,故分表.表分割垂直分割应用场景:热数据放一个表里,冷数据放一个表里.冷数据使用MyIsa ...

  7. mysql几种连接方式区别

    mysql的几种join 2017年03月19日 14:49:07 carl-zhao 阅读数:7845 标签: mysqlsqljoin 更多 个人分类: MySQL 版权声明:本文为博主原创文章, ...

  8. WPF 中的形状和基本绘图概述

    本主题概述如何使用 Shape 对象绘图. Shape 是一种允许您在屏幕中绘制形状的 UIElement 类型. 由于它们是 UI 元素,因此 Shape 对象可以在 Panel 元素和大多数控件中 ...

  9. java activity工作流

    java activity工作流 参考资料: 1.https://blog.csdn.net/jiangyu1013/article/details/73250902 2.https://blog.c ...

  10. 【转】SpringCloud学习

    Spring Cloud Alibaba与Spring Boot.Spring Cloud之间不得不说的版本关系   这篇博文是临时增加出来的内容,主要是由于最近连载<Spring Cloud ...