Lambda 表達式詳解


前言

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM