成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

C#語法糖 聊聊閉包的底層玩法

開發 前端
捕獲是一個很抽象?的詞,一點都不接底氣,這里我用 面向對象? 的角度來解讀一下,這個問題本質上就是 棧變量? 和 堆變量 混在一起的一次行為沖突,什么意思呢?

有朋友好奇為什么將 閉包 歸于語法糖,這里簡單聲明下,C# 中的所有閉包最終都會歸結于 類 和 方法,為什么這么說,因為 C# 的基因就已經決定了,如果大家了解 CLR 的話應該知道, C#中的類最終都會用 MethodTable 來承載,方法都會用 MethodDesc 來承載, 所以不管你怎么玩都逃不出這三界之內。

這篇我們就來聊聊C#中的閉包底層原理及玩法,表面上的概念就不說了哈。

一:普通閉包玩法

1. 案例演示

放了方便說明,先上一段測試代碼:

static void Main(string[] args)
        {
            int y = 10;

            Func<int, int> sum = x =>
            {
                return x + y;
            };

            Console.WriteLine(sum(11));
        }

剛才也說了,C#的基因決定了最終會用 class 和 method 對 閉包 進行面向對象改造,那如何改造呢?這里有兩個問題:

  • 匿名方法如何面向對象改造

方法 不能脫離 類 而獨立存在,所以 編譯器 必須要為其生成一個類,然后再給匿名方法配一個名字即可。

  • 捕獲到的 y 怎么辦

捕獲是一個很抽象的詞,一點都不接底氣,這里我用 面向對象 的角度來解讀一下,這個問題本質上就是 棧變量 和 堆變量 混在一起的一次行為沖突,什么意思呢?

  1. 棧變量

大家應該知道 棧變量 所在的幀空間是由 esp 和 ebp 進行控制,一旦方法結束,esp 會往回收縮造成局部變量從棧中移除。

  1. 堆變量

委托是一個引用類型,它是由 GC 進行管理回收,只要它還被人牽著,自然就不會被回收。

到這里我相信你肯定發現了一個嚴重的問題, 一旦 sum 委托逃出了方法,這時局部變量 y 肯定會被銷毀,如果真的被銷毀了, 后續再執行 sum 委托自然就是一個巨大的bug,那怎么辦呢?

編譯器自然早就考慮到了這種情況,它在進行面向對象改造的時候,特意為 類 定義了一個 public 類型的字段,用這個字段來承載這個局部變量。

2. 手工改造

有了這些多前置知識,我相信你肯定會知道如何改造了,參考代碼如下:

class Program
    {
        static void Main(string[] args)
        {
            int y = 10;

            //Func<int, int> sum = x =>
            //{
            //    return x + y;
            //};

            //面向對象改造
            FuncClass funcClass = new FuncClass() { y = y };

            Func<int, int> sum = funcClass.Run;

            Console.WriteLine(sum(11));
        }
    }

    public class FuncClass
    {
        public int y;

        public int Run(int x)
        {
            return x + y;
        }
    }

如果你不相信的話,可以看下 MSIL 代碼。

.method private hidebysig static 
 void Main (
  string[] args
 ) cil managed 
{
 // Method begins at RVA 0x2050
 // Code size 43 (0x2b)
 .maxstack 2
 .entrypoint
 .locals init (
  [0] class ConsoleApp1.Program/'<>c__DisplayClass0_0' 'CS$<>8__locals0',
  [1] class [System.Runtime]System.Func`2<int32, int32> sum
 )

 IL_0000: newobj instance void ConsoleApp1.Program/'<>c__DisplayClass0_0'::.ctor()
 IL_0005: stloc.0
 IL_0006: nop
 IL_0007: ldloc.0
 IL_0008: ldc.i4.s 10
 IL_000a: stfld int32 ConsoleApp1.Program/'<>c__DisplayClass0_0'::y
 IL_000f: ldloc.0
 IL_0010: ldftn instance int32 ConsoleApp1.Program/'<>c__DisplayClass0_0'::'<Main>b__0'(int32)
 IL_0016: newobj instance void class [System.Runtime]System.Func`2<int32, int32>::.ctor(object, native int)
 IL_001b: stloc.1
 IL_001c: ldloc.1
 IL_001d: ldc.i4.s 11
 IL_001f: callvirt instance !1 class [System.Runtime]System.Func`2<int32, int32>::Invoke(!0)
 IL_0024: call void [System.Console]System.Console::WriteLine(int32)
 IL_0029: nop
 IL_002a: ret
} // end of method Program::Main


.class nested private auto ansi sealed beforefieldinit '<>c__DisplayClass0_0'
 extends [System.Runtime]System.Object
{
 .custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
  01 00 00 00
 )
 // Fields
 .field public int32 y

 // Methods
 .method public hidebysig specialname rtspecialname 
  instance void .ctor () cil managed 
 {
  // Method begins at RVA 0x2090
  // Code size 8 (0x8)
  .maxstack 8

  IL_0000: ldarg.0
  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 int32 '<Main>b__0' (
   int32 x
  ) cil managed 
 {
  // Method begins at RVA 0x209c
  // Code size 14 (0xe)
  .maxstack 2
  .locals init (
   [0] int32
  )

  IL_0000: nop
  IL_0001: ldarg.1
  IL_0002: ldarg.0
  IL_0003: ldfld int32 ConsoleApp1.Program/'<>c__DisplayClass0_0'::y
  IL_0008: add
  IL_0009: stloc.0
  IL_000a: br.s IL_000c

  IL_000c: ldloc.0
  IL_000d: ret
 } // end of method '<>c__DisplayClass0_0'::'<Main>b__0'

} // end of class <>c__DisplayClass0_0

二:循環下閉包玩法

為了方便說明,還是先上一段代碼。

static void Main(string[] args)
        {
            var actions = new Action[10];

            for (int i = 0; i < actions.Length; i++)
            {
                actions[i] = () => Console.WriteLine(i);
            }

            foreach (var item in actions) item();
        }

然后把代碼跑起來:

我相信有非常多的朋友都踩過這個坑,那為什么會出現這樣的結果呢?我試著從原理上解讀一下。

1. 原理解讀

根據前面所學的 面向對象 改造法,我相信大家肯定會很快改造出來,參考代碼如下:

class Program
    {
        static void Main(string[] args)
        {
            var actions = new Action[10];

            for (int i = 0; i < actions.Length; i++)
            {
                //actions[i] = () => Console.WriteLine(i);

                //改造后
                var funcClass = new FuncClass() { i = i };
                actions[i] = funcClass.Run;
            }

            foreach (var item in actions) item();
        }
    }

    public class FuncClass
    {
        public int i;

        public void Run()
        {
            Console.WriteLine(i);
        }
    }

然后跑一下結果:

真奇葩,我們的改造方案一點問題都沒有,咋 編譯器 就弄不對呢?要想找到案例,只能看 MSIL 啦,簡化后如下:

IL_0001: ldc.i4.s 10
  IL_0003: newarr [System.Runtime]System.Action
  IL_0008: stloc.0
  IL_0009: newobj instance void ConsoleApp1.Program/'<>c__DisplayClass0_0'::.ctor()
  IL_000e: stloc.1
  IL_000f: ldloc.1
  IL_0010: ldc.i4.0
  IL_0011: stfld int32 ConsoleApp1.Program/'<>c__DisplayClass0_0'::i
  IL_0016: br.s IL_003e
  // loop start (head: IL_003e)
   IL_0018: nop
   IL_0019: ldloc.0
            ...
  // end loop

如果有興趣大家可以看下完整版,它的實現方式大概是這樣的。

static void Main(string[] args)
        {
            var actions = new Action[10];

            var funcClass = new FuncClass();

            for (int i = 0; i < actions.Length; i++)
            {
                actions[i] = funcClass.Run;

                funcClass.i = i + 1;
            }

            foreach (var item in actions) item();
        }

原來問題就出在了它只 new 了一次,同時 for 循環中只是對  i 進行了賦值,導致了問題的發生。

2. 編譯器的想法

為什么編譯器會這么改造代碼,我覺得可能基于下面兩點。

  • 不想 new 太多的類實例

new一個對象,其實并沒有大家想象的那么簡單,在 clr 內部會分 快速路徑 和 慢速路徑,同時還為此導致 GC 回收,為了保存一個變量 需要專門 new 一個實例,這代價真的太大了。。。

  • 有更好的解決辦法

更好的辦法就是用 方法參數 ,方法的字節碼是放置在 CLR 的 codeheap 上,獨此一份,同時方法參數只是在棧上多了一個存儲空間而已,這代價就非常小了。

三:代碼改造

知道編譯器的苦衷后,改造起來就很簡單了,大概有如下兩種。

1. 強制 new 實例

這種改造法就是強制在每次 for 中 new 一個實例來承載 i 變量,參考代碼如下:

static void Main(string[] args)
        {
            var actions = new Action[10];

            for (int i = 0; i < actions.Length; i++)
            {
                var j = i;
                actions[i] = () => Console.WriteLine(j);
            }

            foreach (var item in actions) item();
        }

2. 采用方法參數

為了能夠讓 i 作為方法參數,只能將 Action 改成 Action<int>,雖然你可能要為此掉頭發,但對程序性能來說是巨大的,參考代碼如下:

static void Main(string[] args)
        {
            var actions = new Action<int>[10];

            for (int i = 0; i < actions.Length; i++)
            {
                actions[i] = (j) => Console.WriteLine(j);
            }

            for (int i = 0; i < actions.Length; i++)
            {
                actions[i](i);
            }
        }

好了,洋洋灑灑寫了這么多,希望對大家有幫助。

責任編輯:武曉燕 來源: 一線碼農聊技術
相關推薦

2023-09-01 10:00:17

2011-08-05 09:33:30

Func局部變量作用域

2011-05-23 13:54:04

閉包

2021-10-26 13:18:52

Go底層函數

2025-01-10 08:15:22

C#異步底層

2009-05-13 14:15:09

PHP 5.3閉包匿名函數

2022-05-30 16:19:26

C#多態底層虛方法

2009-11-23 14:17:50

PHP 5.3閉包語法

2016-06-02 15:10:12

SwiftSelector

2020-12-08 07:51:53

Java語法糖泛型

2009-08-19 15:38:59

C#代碼

2022-02-14 08:04:02

Go語法糖編譯器

2021-01-30 11:12:21

C#List數據

2024-05-15 09:11:51

委托事件C#

2024-10-21 16:59:37

C#編程多線程

2009-08-27 11:43:31

C#語法

2025-04-08 00:07:37

語法糖C#代碼

2023-10-09 07:11:03

排序算法序列

2024-01-22 09:51:32

Swift閉包表達式尾隨閉包

2024-09-29 09:28:38

Action?C#
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 91视频88av | 嫩草伊人| 日日夜夜精品视频 | 日本二区在线观看 | 国产亚洲精品精品国产亚洲综合 | 亚洲视频免费在线 | 日韩欧美视频网站 | 国产农村妇女毛片精品久久麻豆 | 欧美视频一区二区三区 | 亚洲视频手机在线 | 亚洲精品电影在线观看 | 欧美成人h版在线观看 | 精品二 | 日韩欧美不卡 | 欧美亚洲视频在线观看 | 国产精品久久久久久吹潮 | 欧美日韩一区二区在线 | 粉嫩av久久一区二区三区 | 中文字幕精品一区二区三区精品 | 日本aⅴ中文字幕 | 一本一道久久a久久精品综合蜜臀 | 亚洲精品乱码久久久久久按摩 | 日韩视频免费在线 | 91在线精品播放 | 亚洲精品久久久久久下一站 | 不卡在线视频 | 免费观看色 | 丁香婷婷久久久综合精品国产 | 欧美日韩亚洲二区 | 国产精品一区二区无线 | 久久人爽爽人爽爽 | 日韩电影中文字幕 | 999re5这里只有精品 | 狠狠的干| 欧美第一页 | 激情a| 国产精品美女久久久久aⅴ国产馆 | 在线免费91| 黄色网址免费看 | www.国产精品 | 午夜精品久久久久99蜜 |