
文章目录开篇那些年我们踩过的坑比老板的饼还深坑1async/await用成异步自杀场景还原为啥会这样正确姿势坑2DbContext当单例数据库连接池哭晕在厕所场景还原为啥会这样正确姿势坑3LINQ的N1查询数据库巡回演出场景还原为啥会这样正确姿势坑4try-catch当if-else用异常吞得比吸尘器还干净场景还原为啥会这样正确姿势坑5字符串拼接用GC累到想辞职场景还原为啥会这样正确姿势坑6构造函数参数多到能开杂货铺场景还原为啥会这样正确姿势坑7IDisposable不当回事内存泄漏找上门场景还原为啥会这样正确姿势坑8ConcurrentDictionary当普通字典用线程安全了个寂寞场景还原为啥会这样正确姿势坑9配置到处硬编码改个地址改到怀疑人生场景还原为啥会这样正确姿势坑10日志要么不记要么记成流水账坑10.1不记日志坑10.2记太多日志正确姿势结语坑是踩不完的但能少踩一个是一个目前国内还是很缺AI人才的希望更多人能真正加入到AI行业共同促进行业进步增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow教程通俗易懂高中生都能看懂还有各种段子风趣幽默从深度学习基础原理到各领域实战应用都有讲解我22年的AI积累全在里面了。注意教程仅限真正想入门AI的朋友否则看看零散的博文就够了。开篇那些年我们踩过的坑比老板的饼还深话说写C#后端这事儿吧说难不难说简单也不简单。就跟学骑自行车似的看着别人骑得飞起自己一上去就摔个狗吃屎。今天咱不聊虚的直接扒一扒那些让无数新手和部分老司机半夜惊醒、疯狂翻日志的10个经典大坑。这些坑吧我敢打赌90%的.NET开发者至少踩过其中5个。你要是都没踩过那建议你赶紧去买彩票运气太好了。废话不多说系好安全带咱们开车。坑1async/await用成异步自杀场景还原你兴高采烈的写了段异步代码publicstringGetData(){varresultGetDataFromApiAsync().Result;// 嘿嘿这样就不用async了returnresult;}然后程序就在生产环境优雅地卡死了。用户疯狂刷新CPU占用飙升你疯狂翻日志找原因最后发现——死锁了。为啥会这样这跟.NET的历史包袱有关。在老的ASP.NET.NET Framework时代同步上下文SynchronizationContext是个霸道总裁它说你必须在我这儿执行完才能走。然后你的.Result就在那儿干等着上下文也被占着两个人互相瞪眼到天荒地老。虽然现在.NET Core/.NET 5已经没这个问题了但WinForms、WPF、还有某些老库里面这坑依然健在。而且吧这种写法就像在高速公路上逆行虽然有时候没车但撞一次就升天。正确姿势要么一路async到底publicasyncTaskstringGetDataAsync(){varresultawaitGetDataFromApiAsync();returnresult;}要么真不想async就用GetAwaiter().GetResult()虽然也不推荐但至少不会死锁。最好的办法是——别混着用要么全同步要么全异步。坑2DbContext当单例数据库连接池哭晕在厕所场景还原你发现每次请求都创建DbContext太麻烦脑子一热services.AddSingletonAppDbContext();// 一劳永逸然后上线一周数据库管理员拿着菜刀站在你工位旁边——连接数爆了内存泄漏了查询慢得像蜗牛。为啥会这样DbContext可不是省油的灯它里面缓存着实体状态、变更追踪还是个IDisposable。你把它设成单例等于让全家老少共用一个牙刷短期看是省钱了长期看全是病。而且EF Core的DbContext不是线程安全的并发请求一来直接给你抛个InvalidOperationException说context正在被使用。正确姿势services.AddDbContextAppDbContext(optionsoptions.UseSqlServer(connectionString),ServiceLifetime.Scoped);// 默认就是Scoped别瞎改Scoped生命周期完美匹配HTTP请求请求结束自动释放干净又卫生。如果你的逻辑需要在后台任务用DbContext那就手动using var context scope.ServiceProvider.GetRequiredServiceAppDbContext()千万别偷懒。坑3LINQ的N1查询数据库巡回演出场景还原你写了个看似无害的查询varordersdbContext.Orders.ToList();foreach(varorderinorders){Console.WriteLine(order.Customer.Name);// 这里每次循环都查一次数据库}本地测试100条数据嗖嗖的快。上线后有10000条订单页面加载时间直接突破天际用户以为电脑死机了。为啥会这样这就是经典的N1问题。第一次查询拿了N条订单然后每次访问order.Customer都触发一次数据库查询。1N次查询数据量一上来直接爆炸。EF Core的延迟加载Lazy Loading就像个自动续费的会员看着方便不经意间就掏空你的钱包数据库性能。正确姿势用Include一次性拿齐varordersdbContext.Orders.Include(oo.Customer).ToList();或者干脆用投影只拿需要的字段varordersdbContext.Orders.Select(onew{o.Id,CustomerNameo.Customer.Name}).ToList();.NET 9的EF Core还优化了Split Queries复杂查询可以拆成多个SQL但那是后话。先记住循环里别访问导航属性这是铁律。坑4try-catch当if-else用异常吞得比吸尘器还干净场景还原你怕代码出错于是try{varresultint.Parse(userInput);}catch{// 啥也不干就当没这回事}或者更绝的try{// 一堆业务逻辑}catch(Exceptionex){Console.WriteLine(ex.Message);// 打印一下然后继续跑// 不抛出不返回业务继续}然后用户反馈为啥我点了按钮没反应你看着满屏的日志一脸懵逼——异常被吃了流程断了但程序还在跑。为啥会这样异常机制不是用来处理业务逻辑的它是用来处理意外的。你把FormatException当验证用就像用消防车去送外卖——能用但大材小用而且代价高昂。异常抛出的代价很高栈展开、堆栈跟踪一套下来性能掉一截。更可怕的是吞异常错误被吃了程序继续跑数据可能已经脏了你还在那儿傻乐。正确姿势能用验证就别用异常if(!int.TryParse(userInput,outvarresult)){returnBadRequest(请输入数字);}真要catch至少记录日志并重新抛出或者直接不catch让上层处理try{// 业务逻辑}catch(Exceptionex){logger.LogError(ex,处理订单失败订单ID: {OrderId},orderId);throw;// 重新抛出或者返回错误结果别让程序继续瞎跑}.NET 9还引入了Exception.Data的改进可以更好地附加上下文信息。记住异常是最后的防线不是第一道防线。坑5字符串拼接用GC累到想辞职场景还原你写了个日志拼接stringlog;for(inti0;i10000;i){log$[{DateTime.Now}] 处理第{i}条记录\n;}然后发现内存占用飙升GC频繁触发程序卡成PPT。为啥会这样字符串在.NET里是不可变的immutable。每次都创建一个新字符串复制旧内容追加新内容扔掉旧对象。循环10000次创建了10000个字符串对象GC收拾残局累到吐血。虽然.NET 9对字符串内插$做了优化但那是编译器层面的的底层逻辑没变。正确姿势用StringBuildervarsbnewStringBuilder();for(inti0;i10000;i){sb.AppendLine($[{DateTime.Now}] 处理第{i}条记录);}varlogsb.ToString();或者如果是简单拼接用string.Join或string.Concat。在.NET 9里字符串内插在编译时会优化成Append调用但前提是别用。另外如果是记录日志直接用ILogger的结构化日志logger.LogInformation(处理第{Index}条记录时间:{Time},i,DateTime.Now);别自己拼字符串既慢又容易泄露敏感信息。坑6构造函数参数多到能开杂货铺场景还原你的Controller或者Service长这样publicclassOrderService{privatereadonlyIUserRepository_userRepo;privatereadonlyIOrderRepository_orderRepo;privatereadonlyIPaymentService_paymentService;privatereadonlyIEmailService_emailService;privatereadonlyISmsService_smsService;privatereadonlyILoggerOrderService_logger;privatereadonlyIConfiguration_config;// ... 还有十几个publicOrderService(IUserRepositoryuserRepo,IOrderRepositoryorderRepo,IPaymentServicepaymentService,IEmailServiceemailService,ISmsServicesmsService,ILoggerOrderServicelogger,IConfigurationconfig,// ... 无穷无尽){_userRepouserRepo;_orderRepoorderRepo;// ... 赋值到手软}}这叫构造函数爆炸也叫依赖注入地狱。类干了太多事违反了单一职责原则。为啥会这样新手容易把Service写成万能类订单相关的一切都塞进去。然后依赖越来越多构造函数参数越来越多最后new一个对象要传20个参数写单元测试时想死的心都有。正确姿势拆分服务遵循单一职责publicclassOrderCreationService{}publicclassOrderPaymentService{}publicclassOrderNotificationService{}或者使用IServiceProvider按需获取但别滥用会隐藏依赖publicclassOrderService{privatereadonlyIServiceProvider_serviceProvider;publicOrderService(IServiceProviderserviceProvider){_serviceProviderserviceProvider;}publicasyncTaskProcessOrder(){varpaymentService_serviceProvider.GetRequiredServiceIPaymentService();// 使用}}更好的办法是引入MediatR或者类似库用CQRS模式解耦。构造函数参数超过5个就该警惕了超过10个必须重构。坑7IDisposable不当回事内存泄漏找上门场景还原你读了某个文件publicbyte[]ReadFile(stringpath){varfsnewFileStream(path,FileMode.Open);varbuffernewbyte[fs.Length];fs.Read(buffer,0,(int)fs.Length);returnbuffer;// fs没释放溜了溜了}或者用了HttpClientpublicstringCallApi(){varclientnewHttpClient();returnclient.GetStringAsync(https://api.example.com).Result;}然后程序跑久了句柄数飙升内存只增不减最后OutOfMemoryException给你来个惊喜。为啥会这样非托管资源文件句柄、网络连接、数据库连接不会自动释放。虽然.NET有垃圾回收GC但GC不管非托管资源它只回收托管内存。FileStream、HttpClient虽然它其实可以复用但新手往往每次都new、SqlConnection这些都是吃资源的怪兽用完了必须释放。正确姿势using语句是救命稻草publicbyte[]ReadFile(stringpath){usingvarfsnewFileStream(path,FileMode.Open);varbuffernewbyte[fs.Length];fs.Read(buffer,0,(int)fs.Length);returnbuffer;}// 这里自动调用Dispose或者老写法using(varfsnewFileStream(path,FileMode.Open)){// ...}// 自动释放HttpClient特殊一点应该用单例或者IHttpClientFactoryservices.AddHttpClient(api,c{c.BaseAddressnewUri(https://api.example.com/);});然后注入IHttpClientFactory使用。别每次new HttpClient会耗尽端口虽然.NET Core 2.1后SocketHttpHandler改善了但坏习惯还是改了吧。坑8ConcurrentDictionary当普通字典用线程安全了个寂寞场景还原你知道多线程要用线程安全集合于是privatereadonlyConcurrentDictionarystring,int_cachenew();publicvoidUpdateCache(stringkey,intvalue){if(!_cache.ContainsKey(key)){_cache[key]value;// 这里有问题}else{_cache[key]value;// 更新}}看起来用了ConcurrentDictionary但ContainsKey和索引器操作不是原子的多线程下还是会出问题。为啥会这样ConcurrentDictionary提供了线程安全的操作但不代表你组合多个操作就自动线程安全了。上面的代码ContainsKey检查完另一个线程可能已经插入了然后你覆盖了或者两个线程同时进入if分支。这就像给门上了锁但检查屋里有没有人跟进屋是两个动作中间可能有人溜进去。正确姿势用原子操作// 添加或更新_cache.AddOrUpdate(key,value,(k,oldValue)value);// 获取或添加_cache.GetOrAdd(key,kComputeValue(k));// 条件删除_cache.TryRemove(key,outvarremovedValue);如果需要复杂逻辑用TryGetValue配合锁或者直接用Interlocked操作。记住ConcurrentDictionary的单个操作是线程安全的组合操作不是。坑9配置到处硬编码改个地址改到怀疑人生场景还原你的代码里到处都是varapiKeysk-1234567890abcdef;// 密钥直接写代码里varconnectionStringServerlocalhost;DatabaseMyDb;...;// 连接字符串写死然后测试环境连生产库了密钥泄露了或者被GitHub扫描到自动发邮件提醒你密钥泄露了。为啥会这样硬编码是最省事的但也是最坑的。环境一多开发、测试、预发布、生产配置就变来变去。而且代码里的配置没法在不重新编译的情况下修改灵活性为零。正确姿势用IConfiguration和Options模式// Program.csbuilder.Services.ConfigureApiSettings(builder.Configuration.GetSection(ApiSettings));// 配置类publicclassApiSettings{publicstringApiKey{get;set;}publicstringBaseUrl{get;set;}}// 使用publicclassSomeService{privatereadonlyApiSettings_settings;publicSomeService(IOptionsApiSettingsoptions){_settingsoptions.Value;}}appsettings.json里放默认配置环境变量覆盖敏感信息{ApiSettings:{BaseUrl:https://api.example.com}}敏感信息用User Secrets开发时或环境变量生产时dotnet user-secretssetApiSettings:ApiKeysk-123456.NET 9还增强了配置验证可以在启动时检查配置是否合法早发现早解决。坑10日志要么不记要么记成流水账坑10.1不记日志代码里全是Console.WriteLine上线后想查问题发现没有上下文没有请求ID没有堆栈只有一行出错了。坑10.2记太多日志每个方法入口都logger.LogInformation(进入方法A)出口logger.LogInformation(离开方法A)日志文件几分钟滚一个真正有用的信息被淹没在日志海洋里存储成本飙升查询慢如蜗牛。正确姿势用结构化日志分级记录// 错误必须记logger.LogError(ex,支付失败订单:{OrderId}, 用户:{UserId},orderId,userId);// 警告业务异常logger.LogWarning(库存不足商品:{ProductId}, 当前库存:{Stock},productId,stock);// 信息关键流程节点logger.LogInformation(订单创建成功订单号:{OrderNumber},orderNumber);// 调试开发时用logger.LogDebug(计算结果:{Result},result);// 生产环境通常关闭Debug级别配合Serilog或者.NET 9内置的日志改进输出JSON格式方便Elasticsearch或者Seq分析Log.LoggernewLoggerConfiguration().WriteTo.Console(newJsonFormatter()).WriteTo.File(logs/app-.log,rollingInterval:RollingInterval.Day).CreateLogger();还有千万别记敏感信息密码、身份证号、银行卡号日志泄露比代码泄露更惨。结语坑是踩不完的但能少踩一个是一个写了这么多其实每个坑背后都是血泪史。C#这门语言吧语法糖多框架成熟看着简单但魔鬼都在细节里。这10个坑你要是能避开一半就已经超过80%的新手了。要是全避开了那恭喜你可以开始踩更高级的坑了比如分布式事务、微服务通信、K8s调度之类的。最后送大家一句话代码是写给人看的顺便给机器执行。别为了省事写出一堆看起来能跑的代码三个月后你自己都看不懂。祝大家写C#不写BUG写了BUG也能秒定位。咱们下篇文章见拜拜目前国内还是很缺AI人才的希望更多人能真正加入到AI行业共同促进行业进步增强我国的AI竞争力。想要系统学习AI知识的朋友可以看看我精心打磨的教程 http://blog.csdn.net/jiangjunshow教程通俗易懂高中生都能看懂还有各种段子风趣幽默从深度学习基础原理到各领域实战应用都有讲解我22年的AI积累全在里面了。注意教程仅限真正想入门AI的朋友否则看看零散的博文就够了。