ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#匿名方法与Lambda表达式:从委托到闭包的完整解析

C#匿名方法与Lambda表达式:从委托到闭包的完整解析 匿名方法这个东西很多人刚接触C#的时候会觉得有点玄乎尤其是看到写着写着突然冒出一段delegate或者的代码不明白为什么好好的方法不直接写非要搞这种“没有名字”的玩意儿。但等你真正写过一段时间的业务代码尤其是跟LINQ、事件、异步打交道之后你就会发现这俩家伙几乎是每天都在用只是你可能没意识到而已。这篇文章我打算把C#里的匿名方法和Lambda表达式从头到尾捋一遍不光是讲语法更重要的是讲它们到底解决了什么问题、背后的机制是什么、有哪些坑是新手甚至老手都会踩的。不管你是刚开始学C#、准备面试还是写了几年代码想把这部分基础补扎实这篇内容应该都能给你一些参考。1. 为什么要引入匿名方法先聊聊委托这个老朋友1.1 从委托说起一切都是为了“把方法当参数传”理解匿名方法之前必须先理解委托。C#里的委托说白了就是一种“类型安全的函数指针”它的作用是把一个方法当作参数传给另一个方法或者把一个方法存起来、在合适的时机再调用。你在WinForm里给按钮写Click事件、在LINQ里写Where(x x.Age 18)、在线程里写Task.Run(() DoSomething())本质都是在跟委托打交道。传统的写法是这样你有一个方法比如判断一个人是否成年你想把它传给一个封装的过滤方法去用那么你得先声明一个委托类型再写一个匹配签名的方法然后实例化委托最后传进去。像这样// 声明委托类型 delegate bool AgePredicate(int age); // 写一个普通方法 bool IsAdult(int age) { return age 18; } // 使用 AgePredicate predicate new AgePredicate(IsAdult); var result Filter(predicate);在早期的C#版本里这种写法是家常便饭。但问题也很明显为了一个简单的判断逻辑你得定义委托类型、定义方法、再实例化委托三件事分开写代码量一下子就上来了。尤其是当这种小逻辑只用一次的时候专门给它起个名字、写成一个独立方法显得特别笨重。就好比你只是临时需要一个螺丝刀拧一下螺丝结果非要专门去买一套工具箱放在那里。1.2 匿名方法的诞生第一次“没有名字”的尝试为了解决上面这个痛点C# 2.0引入了匿名方法。它允许你直接在需要委托的地方内联一段代码逻辑不需要预先定义一个有名字的方法。用匿名方法来重写上面的例子就是AgePredicate predicate delegate(int age) { return age 18; };delegate关键字后面不跟方法名直接给参数列表和方法体。这样做的好处是显而易见的逻辑写在使用它的地方代码结构紧凑了你不需要为了一个一次性逻辑专门跳去其他地方定义一个方法。对于当时的开发者来说这是体验上的一次飞跃。但匿名方法也有它自己的局限。语法上还是略显啰嗦delegate关键字加参数类型加花括号写起来还是有仪式感。而且它在表达上不够“函数式”读起来总觉得跟自然语言差了一层。所以后来C# 3.0又引入了Lambda表达式在匿名方法的基础上做了大幅度的简化。1.3 手工敲代码的体验我第一次用匿名方法的心路历程我记得第一次接触匿名方法是在写一个排序比较器。那时候用的是ListT.Sort需要传一个ComparisonT委托。传统写法得先在类里面定义一个方法写完感觉特别割裂——排序逻辑就在眼前可方法体远在几十行之外。后来偶然看到同事代码里写了delegate(int x, int y) { return x.CompareTo(y); }当时愣了一下还能这么写实际敲了一遍之后最大的感受是“代码的局部性”变好了。逻辑和调用都在同一个位置阅读代码的时候思路不用跳来跳去。不过说实话匿名方法用久了还是会觉得参数类型写起来烦尤其是泛型类型很长的时候。比如delegate(Dictionarystring, Listint x, Dictionarystring, Listint y)这种写法简直是一场灾难。这也让我特别期待Lambda表达式的出现。2. Lambda表达式的语法演进与背后的执行逻辑2.1 从delegate到语法简化的几个关键点Lambda表达式本质上就是匿名方法的进一步演进版。用替代了delegate关键字同时大量依赖编译器的类型推断能力让代码更简洁。同样是判断成年人的逻辑用Lambda写就是一行AgePredicate predicate age age 18;这个过程中编译器帮我们做了很多事。age的类型是从委托类型AgePredicate的参数类型推断出来的返回值类型也是从委托的返回类型推断的。Lambda表达式有几种形态单参数时可以省略括号比如上面的age 多参数时必须带括号比如(x, y) x y没有参数时写空括号比如() Console.WriteLine(hi)当方法体有多行语句时需要用花括号包起来并且明确写returnFuncint, int, int calculate (x, y) { int sum x y; Console.WriteLine($sum is {sum}); return sum; };从形式上来看Lambda表达式相比匿名方法去掉了delegate关键字、去掉了大部分可推断的类型标注代码变得像是一个数学表达式一样紧凑。这也是为什么它叫“表达式”而不是“方法”。2.2 表达式树与Func/Action委托家族的配合Lambda表达式有一个非常关键的分叉口它可以被编译成委托Func、Action这些也可以被编译成表达式树ExpressionT。这两者在运行时表现完全不同。当你把Lambda赋给一个Funcint, int类型的变量时编译器生成的是IL代码相当于一个匿名方法直接在内存里执行。当你把Lambda赋给ExpressionFuncint, int类型的变量时编译器不再生成可执行的IL代码而是生成一棵表达式树——一种用对象来表示代码结构的数据结构。这个树可以被分析、被修改、被翻译成别的语言。最典型的应用就是EF Core和LINQ to SQL。你在查询里写Where(u u.Age 18)EF Core拿到的是表达式树它会分析这棵树的结构把它翻译成SQL的WHERE [Age] 18然后扔给数据库执行。如果Lambda被编译成了委托EF Core就只能把整个数据集加载到内存再过滤性能差别是天壤之别。所以你会经常在ORM文档里看到“如果方法是IQueryableT传入ExpressionFuncT, bool如果是IEnumerableT传入FuncT, bool”。这两者的区别就是Lambda表达式“一条路走到底”的分叉点理解了这个你对C# Lambda的理解就超过了大多数人。2.3 闭包的秘密Lambda为什么可以“捕获”外部变量Lambda表达式里不光可以使用自己的参数还可以直接使用定义它的作用域里的局部变量。这个机制叫“闭包”。比如int threshold 18; Funcint, bool isAdult age age threshold; threshold 20; Console.WriteLine(isAdult(19)); // 输出 False注意闭包捕获的是变量本身而不是变量在捕获那一刻的值。所以上面的例子中虽然定义时threshold是18但使用委托的时候threshold已经变成了20结果是False。这是一个非常经典的坑很多人以为Lambda会把外部变量的值“快照”下来实际上它捕获的是变量的“引用”跟这个变量同生共死。闭包在循环中的应用更要小心。经典的场景是ListAction actions new ListAction(); for (int i 0; i 3; i) { actions.Add(() Console.WriteLine(i)); } foreach (var action in actions) action(); // 输出会是 3 3 3因为在C# 5之前for循环的i在闭包中是同一个变量循环结束后i的值是3所以所有Lambda打印出来的都是3。C# 5之后foreach的迭代变量改成了每次循环创建新变量这个问题在foreach里被修复了但for仍然是同一个变量依旧会踩坑。正确写法是for (int i 0; i 3; i) { int copy i; actions.Add(() Console.WriteLine(copy)); }这个坑在线程池、任务、事件订阅场景里真的是防不胜防我写这篇文章的时候都忍不住多提醒一句凡是循环里创建Lambda立刻检查闭包捕获的变量是不是修改后再用的。3. 实战匿名方法和Lambda的六大高频场景3.1 LINQ中的灵魂角色Where、Select、OrderByLINQ是Lambda表达式最广为人知的舞台。拿Where来说它接收的就一个FuncT, bool或者ExpressionFuncT, bool。表达式里面的逻辑千变万化但写起来非常顺滑var adults people.Where(p p.Age 18) .OrderByDescending(p p.Salary) .Select(p new { p.Name, p.Age }) .ToList();写这段代码的时候闭包让Where里可以用外部的过滤条件类型推断让p不需要声明类型匿名类型又让Select可以只挑需要的字段。整个链路下来代码非常接近SQL观感同时又保持了强类型的特性。这四者叠加是LINQ体验好的核心原因。实操提醒使用IQueryable的时候尽量把能过滤的条件都放在Where里传递给数据库不要在Select或ToList之后再做内存过滤。因为你一旦调用了ToList()后续的Lambda就被编译成委托在内存中执行了前面的表达式树就被“切断”了。3.2 事件订阅与退订Lambda也能“反注册”事件订阅是Lambda用得非常多的地方。给按钮加事件、给控件绑定回调、给自定义事件写响应逻辑都用得上btn.Click (sender, e) MessageBox.Show(按钮被点击了);但这种写法有个隐患如果事件被多次订阅的时候绑定的是不同的Lambda实例你无法用-把它准确移除。因为每个Lambda表达式在编译后都会生成一个单独的委托实例你用创建的委托和后来用-时写的Lambda是两个不同的实例无法匹配。我见过有人写了btn.Click - (sender, e) MessageBox.Show(按钮被点击了);以为能退订结果订阅还在按钮点一次弹两次框。正确做法是把Lambda存到一个局部变量里EventHandler handler (sender, e) MessageBox.Show(按钮被点击了); btn.Click handler; // 后面要退订 btn.Click - handler;顺带说一个内存泄漏相关的经验如果你在一个生命周期长的对象上订阅了另一个生命周期短对象的事件短对象无法被GC回收因为长对象的委托列表里还持有短对象方法的引用。这种情况要特别注意在不需要的时候退订事件否则就是典型的闭包/委托导致的内存泄漏。3.3 线程与任务Task.Run里写Lambda的注意事项Task.Run、Task.Factory.StartNew以及其他多线程相关API在编写时也大量使用Lambda。典型代码如下Task.Run(() { for (int i 0; i 100; i) { Console.Write(i); } });这里要注意的事情有三件。第一Lambda里访问UI控件的问题——在WinForm和WPF里跨线程访问控件会抛异常或者行为未定义你需要用Invoke或SynchronizationContext回到UI线程。第二闭包捕获循环变量的坑在这种场景下同样存在尤其当你用for循环启动多个任务的时候复制一份局部变量再传给任务几乎是必须的。第三异步Lambda的写法是async () await SomeMethodAsync()要注意Task.Run里返回Task的Lambda表示它本身是异步任务你想等它完成得await而不是直接忽略返回的Task。我之前写过一次性启动十个并行下载任务的代码一开始图省事直接在for循环里用i给每个任务定位文件编号结果所有文件全写到了最后一个编号上。排查半天最后发现就是闭包捕获了同一个i。这种体验很痛苦的但也是成长最快的时候。3.4 泛型委托与Lambda组合拳Func和Action的妙用C#中内置了泛型委托Func和Action它们跟Lambda搭配起来特别灵活。Func用于“有返回值”的场景Action用于“无返回值”的场景。它们有多个重载比如FuncT1, T2, TResult表示两个参数一个返回值的委托ActionT1, T2表示两个参数无返回值的委托。一个比较实用的例子是“重试机制”。假设你要执行一个可能会偶发失败的操作比如发请求、写日志、调用第三方API你希望失败后重试几次public static T RetryT(FuncT action, int maxRetryCount 3, int delayMilliseconds 500) { int retryCount 0; while (true) { try { return action(); } catch (Exception ex) when (retryCount maxRetryCount) { retryCount; Console.WriteLine($第 {retryCount} 次重试异常信息{ex.Message}); Thread.Sleep(delayMilliseconds); } } } // 使用方式 var data Retry(() apiClient.GetData());这段代码的关键点是你不需要为每次调用都写一个专门的方法只要把业务逻辑写成一个Lambda传进去就行。而且整个重试机制的“骨架”是通用的业务逻辑可以随意替换。这种模式在项目中非常常见比写死业务逻辑的重试代码要清爽得多。3.5 排序比较器与自定义规则Lambda替代麻烦的方法实现前面提到过ListT.Sort()可以用ComparisonT委托也可以用IComparerT接口实现。接口实现的方式比较繁琐你得写一个类实现Compare方法还要在排序的地方new一个出来。用Lambda就非常直接ListPerson people GetPeople(); people.Sort((a, b) a.Name.CompareTo(b.Name)); // 多条件排序 people.Sort((a, b) { int result a.Department.CompareTo(b.Department); if (result 0) { result b.Salary.CompareTo(a.Salary); } return result; });LINQ的OrderBy提供了类似的能力而且语法更像是SQL风格相比之下List.Sort是原地排序不产生新集合在内存紧张的场景下更有优势。两者我都会用具体看需求。Lambda在这里的价值在于把排序规则写在排序调用旁边看一眼就知道规则是什么。3.6 结合第三方库OpenCVSharp、OCR、Excel这些场景的影子从热词里能看到有人在搜C#结合OpenCVSharp做角落检测、做OCR、操作Excel。这些场景里Lambda表达式同样无处不在。比如OpenCVSharp中处理像素、遍历轮廓时经常要传处理方法OCR识别结果解析时用LINQ和Lambda筛选识别出的文本块Excel操作中遍历行、筛选列、动态构造数据表也都离不开Lambda。举个例子用OpenCVSharp查到轮廓之后通常需要过滤掉太小的轮廓var contours new Mat(); Cv2.FindContours(binaryImage, contours, out _, RetrievalModes.External, ContourModes.RetrieveExternal); var validContours contours .Where(c Cv2.ContourArea(c) 500) .OrderByDescending(c Cv2.ContourArea(c)) .ToList();这里Where里的Lambda就是过滤逻辑索引进来了之后每个轮廓对象经过委托判断是否保留。没有Lambda的话你得写一个专门的方法接收轮廓对象返回布尔值维护成本高得多。正是因为存在这种高频率的“一次性逻辑”匿名方法和Lambda在C#生态里成了基石级能力。4. 避坑指南与性能细节这些坑我帮你踩过了4.1 foreach与for循环的闭包差异C#各版本的行为变化上面已经提过循环闭包的问题这里再展开细说版本差异。C# 5之前foreach的迭代变量在闭包中也是被共享的循环里的每个Lambda捕获的是同一个变量。C# 5开始编译器在每次迭代时创建新的局部变量所以foreach里的Lambda不再踩坑了。但for始终是同一个变量哪怕到了C# 12也还得自己复制一份。这个版本差异经常被面试官拿来当考点实际上在真实开发中也确实会碰到。如果你维护的代码运行在旧版.NET Framework上或者你为了兼容老环境把语言版本降级了那么foreach的行为也会变。我自己的习惯是只要是循环里创建Lambda不管for还是foreach一律先复制一份局部变量省得纠结。4.2 Lambda表达式树vs编译委托机制上的性能差异很多人会有疑问Lambda用起来方便性能到底行不行答案是绝大多数情况下它编译后的委托和普通方法委托没有本质区别。但如果你是在性能敏感的路径上比如游戏循环、高频消息处理、每帧计算需要留意几点。第一闭包会生成额外的类如果Lambda里捕获了很多外部变量每次执行可能会产生一个临时对象增加GC压力。第二表达式树相比委托执行速度要慢一些因为它需要解释执行不能直接调用。第三频繁创建新的Delegate实例也会增加内存分配。这些通常都不是瓶颈但如果你在循环里创建了成千上万个Lambda并且每个都捕获了变量那就值得优化了。一个比较常见的优化手段是把经常使用的Lambda缓存到一个静态只读字段里避免每次创建新实例。比如private static readonly Funcint, bool IsEven x x % 2 0;提示性能调优要基于证据别为了微小的性能差别牺牲代码可读性。先用Stopwatch或者性能分析工具测确认是热点再优化。4.3 匿名方法调用局部函数谁优谁劣如何取舍C# 7.0以后引入了局部函数local function它也能在方法内部定义并使用看起来跟Lambda很相似。区别在哪里局部函数可以像普通方法一样使用ref、out、params可以被递归调用且不会自动捕获外部变量除非你确实引用了它们。Lambda则更适合作为参数传递、表达式的场景。两者的取舍原则是如果你只是想封装一段逻辑并立即在同一个方法里调用局部函数更合适如果你要把逻辑作为参数传给其他方法比如LINQ、事件处理器、任务调度那就用Lambda。我在实际项目中写过一段用yield return做迭代器的逻辑想在里面用递归展开树形结构试了半天Lambda因为不能递归除非提前声明委托变量而且会有初始化顺序问题后来改用局部函数一下子就通了。这就是很典型的选择标准。4.4 可读性与维护性不是所有地方都适合用LambdaLambda虽好但并不是越多越好。一个特别复杂的Lambda参数多、嵌套深、占了十几行读起来可能比写一个带名字的方法还困难。这里我的经验是如果Lambda的方法体超过三行或者内部的算法逻辑需要写注释解释那就该考虑把它抽成一个独立的方法。命名的好处是有语义方法名本身就是文档。比如下面这个Lambdavar result list.Where(x { bool valid x.IsActive; if (x.Type Gold valid) { valid x.Score 60; } else if (x.Type Silver) { valid x.Score 80; } return valid; });这种逻辑写在一个Lambda里虽然能跑但别人阅读成本很高。把它改成Where(x IsQualified(x))再加一个命名方法IsQualified代码的意图就清楚多了。代码是给人读的顺便给机器执行这个原则放在Lambda的取舍上同样成立。4.5 调试技巧与配合工具如何在断点里看清Lambda的值调试Lambda代码有个麻烦你没法直接在Lambda内部那行加断点其实是可以的。在Lambda表达式所在的行打上断点执行到这一行时Visual Studio会停在Lambda内部你可以在“局部变量”窗口里看到参数和捕获变量的值。如果是表达式体形式的Lambda箭头右边直接返回表达式你还可以通过“函数返回值”窗口直接查看返回结果。另一个技巧是配合表达式树调试。当你在IQueryable场景下怀疑翻译出来的SQL不对可以在调用ToQueryString()或者打开日志查看真实执行的SQL。EF Core提供了ToQueryString()扩展方法直接看翻译结果是非常实用的排查手段。我排查过一次日期比较的坑Lambda里写的是p.Birthday DateTime.Now翻译到SQL变成了 GETDATE()完全没问题。但如果你写的是p.Birthday DateTime.Now.Date翻译后可能变成 2024-01-01 AND 2024-01-02这种范围判断效果一样但SQL形态完全不同。这些东西不实际调试几次是根本体会不到的。5. 匿名方法、Lambda与其他语言特性的协同作战5.1 联合异步编程async Lambda的写法与陷阱C# 5.0引入async/await之后Lambda也可以变成异步形式。写法很简单FuncTaskint getCountAsync async () { var data await httpClient.GetStringAsync(url); return data.Length; };这种异步Lambda背后的机制是编译器把整个Lambda改造成一个异步状态机和普通async方法几乎没有区别。调试时你可能会看到Lambda内部生成的类名比如Mainb__0_0这种名字刚开始可能会疑惑它是什么其实就是编译器给Lambda生成的方法名。陷阱在于如果你在一个事件里写了async void风格的Lambda比如按钮点击事件async (s, e) await ...这个异步方法的异常不会像普通方法那样被捕获到调用栈里。一旦发生异常可能会直接导致进程崩溃或者异常丢失很难排查。解决办法是一律使用async Task方法事件处理器中的异常全用try-catch包裹。5.2 与Span 和高性能编码的配合有人可能觉得Lambda这种偏上层的语法跟SpanT这种高性能类型是两回事。实际上在很多高性能场景里Lambda配合ReadOnlySpanT也能发挥作用比如MemoryExtensions的扩展方法里就有IndexOf、SequenceEqual等但大部分这些方法本身接收的是span参数而不是委托。倒是SpanT配合stackalloc做低开销算法时通常你会想把比较逻辑写成委托这时候要小心如果委托里捕获了变量会破坏栈分配的性能优势。我在写一个图像处理的工具类时用Spanbyte处理像素数据比较逻辑用Lambda捕获了一个阈值变量结果发现每次调用性能掉了一些。后来把阈值做成参数传进去避免闭包分配性能又回去了。所以不是说性能场景不能用Lambda而是要注意闭包的GC开销。如果你追求极致性能就尽量减少闭包捕获把变量变成参数。5.3 结合Json、Configuration、数据库映射等实际场景热词里提到了C#处理JSON、读取配置文件、使用SqlBulkCopy批量写入数据库。这些场景中Lambda几乎已经成了标配。比如System.Text.Json在JsonSerializerOptions里配置Converters或者用JsonNode遍历数据都会接触Lambda。数据库方面Entity Framework的查询表达式树就是Lambda的主战场性能敏感时用Dapper这种轻量ORM写出来的SQL也要经常用到Lambda去映射实体。有一个我印象很深的项目经验做批量导入Excel数据读出来的数据要清洗、去重、再写入数据库。一开始用foreach一行行处理速度很慢。后来改成Parallel.ForEach加上Lambda速度快了很多但没注意到闭包捕获的共享状态会导致计数不准确。那个项目踩的坑让我彻底明白了“共享可变状态在多线程环境下的危险”。后来改用Interlocked原子操作和并发集合解决了问题这让我对并发场景下的闭包问题有了刻骨铭心的认识。5.4 试炼一个综合示例串联所有知识点我最后写一个综合性的示例把匿名方法、Lambda、泛型委托、闭包、异步等知识点串起来。假设你要做一个股票行情处理的模拟器对不同股票按价格过滤、排序、异步获取详情再汇总public class Stock { public string Code { get; set; } public decimal Price { get; set; } } public async Task ProcessStocks(IEnumerableStock stocks, decimal threshold) { var tasks stocks .Where(s s.Price threshold) .OrderByDescending(s s.Price) .Select(async s { decimal adjustedPrice await GetAdjustedPriceAsync(s.Code); return new { s.Code, s.Price, Adjusted adjustedPrice }; }); var results await Task.WhenAll(tasks); foreach (var r in results.OrderByDescending(r r.Adjusted)) { Console.WriteLine(${r.Code}: {r.Price} - {r.Adjusted}); } }这段代码里用到了Where过滤、OrderByDescending排序、Select投影出一个匿名类型、async异步Lambda、Task.WhenAll并发执行。这一套组合拳打下来代码简洁清晰而且每个环节的逻辑都集中在调用点附近。如果全用传统方法写你得定义多少种委托类型、多少个子方法可能十几个类都不够。这就是匿名方法和Lambda在现代C#开发里不可替代的原因。6. 常见问题速查与我的个人建议6.1 高频问题与解决方案一览症状原因解决方案循环里启动任务结果全是最后一个值闭包捕获了同一个外部变量每次循环复制一份局部变量再使用事件订阅后无法退订和-用了两个不同的委托实例用同一个变量保存委托实例再注册/退订LINQ查询内存过滤但数据量大很慢在ToList()后才Where丢失了表达式树翻译尽量在IQueryable阶段过滤不下推再过滤Lambda内部异常崩溃但外层捕获不到事件/异步里用了async void类型Lambda改用async Task并用try-catch捕获表达式树的Lambda报错不支持某方法表达式树只能解释一部分语法改用编译委托或者避免在表达式树里调用自定义方法调试时看不到Lambda内部局部变量断点没打在Lambda体内在Labbda所在行打断点按F10逐步进入6.2 做项目多年沉淀的几条实操建议第一Lambda是的可读性边界在三行左右。超过三行的逻辑能抽方法就抽方法。命名方法不仅是为了复用更是为了给逻辑起一个有语义的名字。第二遇到“一次性逻辑”优先考虑Lambda遇到“核心算法”优先考虑命名方法。这样代码既有局部性又有可读性。第三闭包的坑记住一句话Lambda捕获的是变量不是值。无论捕获的是循环变量还是外部局部变量只要它之后被修改了Lambda看到的都是最新的值。第四在团队协作的代码里尽量保持Lambda表达式中无副作用。不在Lambda内部做状态修改除了返回值外不影响外部变量。这样能大大减少并发环境下闭包带来的隐患。第五面试和学习阶段不要只背语法多去看看编译器生成的代码。用工具查看Lambda实际编译成的类和方法你会对闭包的实现有更立体的理解。6.3 关于进一步深挖的建议如果你看完这篇文章还想继续深入我建议按这个顺序学先把委托彻底搞明白包括自定义委托、内置泛型委托、委托的多播合并然后把匿名方法的历史背景和写法掌握再去把Lambda表达式的表达式树机制吃透特别是尝试自己写一个简单的表达式树遍历器来理解它的数据结构最后学异步和并发场景下的闭包陷阱。这条路走完你在C#语言机制的掌握上基本就到中高级水准了。我个人在实际带人的过程中发现很多人写业务写得不错但一遇到委托和Lambda就含糊主要原因就是对闭包和表达式树的理解不透彻。你只要把这两个东西搞明白很多看似高深的代码都能一眼看穿。匿名方法和Lambda并不难难的是真正理解它们的设计意图以及什么时候该用、什么时候不该用。希望这篇文章能帮你把这条路走得更顺一点。
返回列表