有人问我,Task老师,发生甚么事了? 我就用了一个Lambda 表达式。

标签:delegate   ted   ++   循环   闭包   tap   toc   比较   tac   

目录
  • 前言
  • 预备知识,理解委托的构成
    • 引用实例方法的委托
    • 引用静态方法的委托
  • Lambda 表达式的实际编译结果
    • CASE 1 没有捕获任何外部变量的Lambda 表达式
    • CASE 2 捕获了外部方法局部变量的Lambda 表达式
    • CASE 3 实例方法中捕获了实例字段的Lambda 表达式
    • CASE 4 静态方法中的捕获了当前类型静态字段的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;
    }
}

聊一聊循环中的闭包

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

暂无评论

暂无评论...