正确使用和理解C#中的闭包
定義
我們把在Lambda表達式(或匿名方法)中所引用的外部變量稱為捕獲變量。而捕獲變量的表達式就稱為閉包。
捕獲變量
捕獲的變量會在真正調用委托時“賦值”,而不是在捕獲時“賦值”,即總是使用捕獲變量的最新的值。如下代碼所示,調用委托時,age的最新值為30,所以輸出的值也是30。
int?age?=?28; //定義委托 Func<int,?string>?consoleAge?=?i?=>?$"洋小豆今年{i}歲了"; age?=?30; //調用委托 string?outputMsg?=?consoleAge(age); outputMsg.Dump();輸出結果如下圖:
捕獲迭代變量
當捕獲的外部變量為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(); }輸出結果如下圖:
那么,如果我們期望輸出的結果為1,2,3那需要怎么修改呢?這里我們只需要在for循環內部使用局部變量即可(每次循環捕獲的是不同的變量),如下修改后的代碼:
for?(int?i?=?0;?i?<?3;?i++) {int?tmp?=?i;levyActions.Add(()=>?tmp.Dump()); }輸出結果如下圖:
看到這里大家應該基本明白怎么回事了吧!再想下,迭代除了可以使用for還可以使用foreach啊!那么,我們把上面示例中的for循環部分改造成foreach會怎么樣呢?
string[]?names?=?new?string[]?{"洋小豆",?"列位一分鐘",?"levy"}; List<Action>?levyActions?=?new?List<Action>(); foreach?(string?name?in?names) {levyActions.Add(()=>?name.Dump()); }輸出結果如下圖:
納尼?輸出的結果竟然跟我們上面的講解不一樣?不是應該輸出捕獲變量的最新值嗎?應該輸出3個“levy”啊!哈哈,這里是因為我的示例代碼是基于.net core3.0的,從C#5.0開始,foreach認為循環變量都應該是“新”的變量。所以,每次循環中創建委托時捕獲的變量都不是同一個變量。因而,輸出的值肯定也就不一樣了。有興趣的童鞋可以在C#5.0之前的版本下測試下,看看輸出的是不是3個“levy”。
背后原理
分析IL代碼我們可以得知,編譯器在背后生成了一個私有的密封類c__DisplayClass4_0,它將外部變量包裝成類的成員變量,而委托方法包裝成類的方法。所以,上述捕獲for迭代變量的示例代碼就可以修改成如下:
void?Main() {List<Action>?levyActions?=?new?List<Action>();c__DisplayClass4_0?local?=?new?c__DisplayClass4_0();for?(local.i?=?0;?local.i?<?3;?local.i++){levyActions.Add(()?=>?local.Main_b__0());}foreach?(Action?action?in?levyActions){action();} }private?sealed?class?c__DisplayClass4_0 {public?int?i;internal?void?Main_b__0(){i.Dump();} }涉及到外部變量i的地方都替換成了local.i,所以,每次循環修改的始終是c__DisplayClass4_0的成員變量i的值,那么調用委托時輸出的自然都是3了。
總結
以上是生活随笔為你收集整理的正确使用和理解C#中的闭包的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: REST 深度进阶
- 下一篇: Serilog 日志框架如何自动删除超过