从编译器层面理解C#中的闭包的这个坑!
前言
在公眾號上看到一篇文章《正確使用和理解C#中的閉包》,里面提到了閉包的一個坑:
當捕獲的外部變量為for循環的迭代變量時,C#認為變量i是定義在循環體外的。所以,當添加委托集合的for循環執行完時,i的值已經變為3了;因此,我們在foreach中循環調用委托時,i的值就都是3了。
List<Action>?levyActions?=?new?List<Action>(); for?(int?i?=?0;?i?<?3;?i++) {levyActions.Add(()=>?i.Dump()); } foreach?(Action?action?in?levyActions) {action(); }那么,明明是循環體內定義的變量i,為什么會被認為定義在循環體外呢?
編譯器魔法——Lowering
我們知道,C#代碼最終會編譯成IL中間語言。
假設有一個數組:
int[]?arr?=?new[]?{?0,?1,?2?};我們可以有多種方式遍歷它:
//1 foreach?(var?i1?in?arr) {i1.Dump(); } //2 for?(var?i2?=?0;?i2?<?arr.Length;?i2++) {var?value?=?arr[i2];value.Dump(); } //3 var?i3?=?0; while(i3<?arr.Length) {var?value?=?arr[i3];value.Dump();i3++; }那么,是不是要對應準備3種IL語法呢?
其實不是,在編譯之前編譯器還會施展一個魔法:Lowering
大概意思是,讓編譯器從高級語言功能“降低”到同一語言中的低級語言功能。
怎么理解這句話呢?讓我們打開https://sharplab.io/
Roslyn編譯器實現
sharplab.io這個網站可以顯示.NET代碼(比如c#)的編譯中間過程和結果。
我們將上面的C#代碼復制到窗口左邊:
可以看到,編譯器會將foreach和for語法都轉換成while語法,這樣,編譯器最后只需要實現一種IL語法即可。
除了迭代以外,在roslyn編譯器中實現了很多的“Lowering”,比如:
異步重寫器
Lambda重寫器
狀態機重寫器
詳細列表你可以查看“https://github.com/dotnet/roslyn/tree/main/src/Compilers/CSharp/Portable/Lowering”下的代碼。
結論
現在,大家應該已經知道,for循環中的變量i實際會被轉換成while循環外定義的變量num,因此i在循環體作用域外也是有效的,導致了閉包的這個坑。
知道了原理,解決方案也很簡單,始終使用循環體內的變量即可:
for?(var?i?=?0;?i?<?3;?i++) {var?j?=?i;levyActions.Add(()?=>?j.Dump()); }如果你覺得這篇文章對你有所啟發,請關注我的個人公眾號”My IO“
總結
以上是生活随笔為你收集整理的从编译器层面理解C#中的闭包的这个坑!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 浅谈.Net异步编程的前世今生----T
- 下一篇: C# 使用 HelpProvider 控