尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Delphi/Object Pascal异常处理与finally资源释放实战
项目里最容易被写成形式主义的一块代码就是 finally 代码块。我见过太多 Object Pascal 工程try...finally 写得工工整整但真出了异常、真碰上提前 Exit、真遇到句柄耗尽该漏的还是漏。这篇笔记是我在 Delphi 11 上重刷 Object Pascal 第 9 章异常处理部分时结合这几年排查线上问题的记录整理出来的重点不在语法怎么写而在于 finally 到底在什么时候执行、什么情况下不执行、以及执行了却依然出错的原因。不管你是刚接触 Object Pascal 的新人还是写了多年 Delphi 却一直把 finally 当保险丝用的老手下面这些内容都能对得上号从执行路径的拆解到对象、文件句柄、锁、事务四类资源的落地模板再到批量导入时事务批大小的参数计算和排查速查表都可以直接拿去改改就用在项目里。1. 为什么 finally 值得单独拎出来讲1.1 一段看着很顺的代码是怎么漏资源的先看一段我早年写过的典型代码逻辑上没人挑得出毛病procedure TReportJob.ExportToFile(const AFileName, ATemplate: string); var FS: TFileStream; SL: TStringList; begin FS : TFileStream.Create(AFileName, fmCreate); SL : TStringList.Create; SL.LoadFromFile(ATemplate); // 模板文件被占用、编码不对都会在这里抛异常 SL.Text : BuildContent; SL.SaveToStream(FS); SL.Free; FS.Free; end;问题在于第二行和第三行之间那道看不见的裂缝。只要LoadFromFile抛异常控制流就直接从这一行飞出去后面两行Free永远等不到执行。单次调用看不出问题可这个导出任务在服务端是每分钟跑一次的常驻线程跑上两天TFileStream占用的原生文件句柄就堆到几百个最后Create直接报文件无法打开或者句柄无效而错误现场早就跑远了日志里只剩下一个莫名其妙的异常。这类坑的共同特征很一致分配点和释放点之间隔着若干行可能抛异常的语句。只要这段距离存在靠顺序执行做清理就是赌运气。finally 代码块存在的唯一理由就是把这段距离用语言机制焊死——无论中间怎么跳出去清理代码一定被执行一次且只执行一次。1.2 try...finally 和 try...except 是两条不同的轨道新手最容易混淆的地方是把 finally 当成捕获异常的地方。这两者的职责完全不重叠我在团队规范里直接用一句话区分except 负责收拾出了什么事finally 负责收拾留下的东西。对比项try...excepttry...finally主要职责捕获并处理异常决定是否继续传播无条件执行清理不关心发生了什么执行时机仅在异常发生时进入无论正常结束、异常、Exit 都进入是否吞掉异常不重新 raise 就吞掉不影响异常传播方向典型场景记录日志、转成业务异常、降级重试释放对象、关闭句柄、解锁、回滚事务常见误用用空 except 掩盖问题把业务逻辑写进 finallyObject Pascal 有一个很多人第一次写会卡住的语法限制同一个 try 块不能同时挂 except 和 finally。想要既捕获又清理只能嵌套而且顺序很有讲究procedure TImportJob.Run; var Conn: TImportConnection; begin Conn : TImportConnection.Create(GetConnStr); try try Conn.Open; DoImport(Conn); except on E: EImportError do begin Log.Warn(导入失败 E.Message); raise; // 转成上层能识别的异常别吞 end; end; finally Conn.Close; Conn.Free; Conn : nil; end; end;内层负责语义外层负责资源职责一刀切开。我试过把这个顺序反过来外层 except、内层 finally结果就是捕获到的异常信息里丢失了清理阶段的状态排查时非常被动。1.3 选型背后为什么我把清理逻辑全塞进 finally有人会问既然Free在过程退出时也可能由别的方式触发为什么不用别的手段兜底我踩过几次坑之后的结论是在 Object Pascal 里finally 是唯一一个不依赖程序员记性的清理机制。对象字段由析构函数清理那是对象级的生命周期接口变量由引用计数清理那是编译期插入的隐藏代码这两者都管不到过程内局部变量和原生句柄。而 finally 是显式写在过程里的、编译期确定会被生成的清理路径。它不依赖 GC不依赖引用计数不依赖你记得在每一处Exit之前补一行Free。另外还有一个实际收益把清理收拢到 finally过程体里就可以放心用Exit做早退。以前没有 finally 的代码早退意味着要在每个出口前写一遍清理写三遍就必然有一处写错。现在的规矩很简单——过程体里可以随便 Exit清理只在 finally 里写一遍。2. finally 的执行语义拆到底2.1 四条出口路径与 finally 的触发关系只知道finally 会执行是不够的得知道在哪些出口路径下它会执行。我按实际遇到的情况整理成下面这张表这是理解后面所有坑的基础出口路径finally 是否执行说明正常执行到 end执行最普通的情况过程体内 raise 异常向外传播执行异常继续往外走不改变方向过程体内 Exit执行先跑 finally再真正退出过程循环体内 Break / Continue 跳出到 try 之外执行若 try 在循环体内跳出即触发Halt 或进程级强制结束不执行进程被掐断栈不展开清理无从谈起线程被强制终止不保证属于不可依赖的路径务必避免前四条是我每天都在依赖的第五条是我在调试时用来解释文件被占用却没有任何报错的常见原因。有些开发者喜欢在出错时直接Halt结束程序看着干净利落实际上所有 finally 里的落盘、解锁、关句柄全部跳过下次启动就会出现残留锁文件、日志半截、数据未刷盘的情况。我的建议很直接业务代码里不要出现 Halt退出走正常的异常传播或主循环退出流程。2.2 嵌套与展开顺序从内到外像剥洋葱多层 try...finally 嵌套时释放顺序是严格的后进先出和栈的行为完全一致procedure TOrderService.Submit; var Lock: TOrderLock; Txn: TOrderTransaction; Detail: TOrderDetail; begin Lock : TOrderLock.Create(GetOrderId); try Lock.Acquire(3000); // 3 秒拿不到锁就抛 Txn : TOrderTransaction.Create(Conn); try Txn.BeginTransaction; Detail : TOrderDetail.Create; try BuildDetail(Detail); Txn.Insert(Detail); Txn.Commit; finally Detail.Free; // 第一个退内层对象 end; finally if Txn.InTransaction then Txn.Rollback; // 第二个退事务 Txn.Free; end; finally Lock.Release; Lock.Free; // 最后退锁 end; end;这个顺序不能乱。如果先释放锁再回滚事务中间会出现一个窗口期别的线程拿到锁读到了未提交的数据。反过来先回滚再解锁就安全得多。我在代码评审时看到锁的 Release 写在事务 finally 之前基本直接打回去重写。嵌套层数也有讲究。三层以上就开始难以阅读我一般的做法是把锁 事务 对象三层压缩成两层锁和事务交给一个封装好的TUnitOfWork统一管理业务代码只管最内层的对象。这样每层 finally 都有明确的语义不会出现这个 Free 到底该放哪一层的犹豫。2.3 在 finally 里写 Exit 或 raise 会发生什么这是我在第 9 章反复看了两遍才彻底弄明白的地方也是最容易写出的隐蔽 bug。在 finally 里写 Exit会把正在传播的异常吞掉。原因不复杂异常向外传播的机制需要 finally 块正常走完运行时才知道要不要继续往外扔。你在 finally 里执行Exit控制流直接离开过程异常对象就此失去传播的机会外层 except 什么也抓不到日志里一片安静业务却已经错了。写个最小复现procedure TWorker.Process; var Item: TWorkItem; begin Item : TWorkItem.Create; try Item.Execute; // 这里抛出 EDataError finally Item.Free; Exit; // 灾难EDataError 被彻底吞掉 end; end;调用方看到的是执行成功但什么也没做排查时能把人逼疯。我的规矩是finally 里只允许出现清理语句不允许出现 Exit、Continue、Break。如果确实需要根据清理结果决定流程把判断移到 try 外面去。在 finally 里 raise 新异常会覆盖原始异常。下面这段是另一个经典写法错误finally try FlushBuffer; except raise Exception.Create(刷盘失败); // 原始异常信息全部丢失 end; FS.Free; end;外层 except 抓到的只有刷盘失败至于最初到底是因为数据格式错误还是网络中断才走到这里的一点线索都没有。处理这类情况正确的姿势是清理阶段的异常单独记录、不覆盖主线finally try FlushBuffer; except on E: Exception do Log.Error(清理阶段异常已忽略 E.ClassName / E.Message); end; FS.Free; end;清理失败记日志主异常继续原样传播两条信息都在排查时才有完整链路。2.4 对象、接口、句柄三类资源的销毁顺序同一段代码里往往混着三种资源它们的释放规则完全不同混着写就会出问题。对象靠Free顺序是先创建的后释放。父对象持有子对象时如果子对象是字段交给析构函数如果是局部变量就在 finally 里手动释放而且必须在父对象释放之前。接口靠引用计数不需要也不能手动 Free。这里有个反直觉的点局部接口变量在过程出口时由编译器插入的清理代码递减引用计数即使你是从 try 块里 Exit 出去的也一样。所以给接口变量套 finally 写IFoo : nil通常是多余的。但有一个例外必须注意——接口之间存在循环引用时引用计数永远降不到零对象会一直挂着。这种情况我会显式在 finally 里断开其中一条引用var Parent, Child: INode; begin Parent : TNode.Create; Child : TNode.Create; Parent.Attach(Child); // 互相持有 try DoWork(Parent); finally Parent.Detach(Child); // 必须手动断链否则两个对象都泄漏 Parent : nil; Child : nil; end; end;原生句柄文件、套接字、系统内核对象则必须显式关闭Free一个包装类只是间接关闭包装类没写好的情况下句柄依然会漏。我一般把句柄的关闭放在包装类自己的Close里外层 finally 里先Close再Free双保险且语义清晰。3. 落地实操把 finally 用成肌肉记忆3.1 对象资源标准模板与 FreeAndNil 的取舍我团队里所有新代码的对象清理都统一成下面这个模板几乎没有例外procedure TCacheManager.Rebuild; var NewIndex: TCacheIndex; begin NewIndex : TCacheIndex.Create(FCapacity); try NewIndex.LoadFrom(FSourcePath); NewIndex.BuildBuckets; SwapIndex(NewIndex); // 成功才换入失败保留旧索引 NewIndex : nil; // 所有权已转移finally 不再释放 finally NewIndex.Free; // nil 上调用 Free 是安全的 end; end;这里有两个细节值得说。第一构造放在 try 外面。因为如果构造函数本身抛异常对象根本没建成try 还没建立finally 里的变量是未初始化的野指针Free会直接崩。第二NewIndex.Free对 nil 是安全的因为TObject.Free内部会先判断Self nil再调用Destroy。这也是我推荐用Free而不是Destroy的原因——Destroy对 nil 调用会直接访问违例。至于FreeAndNil我的用法是分场景的局部变量用Free就够过程一结束变量就没了置不置 nil 无所谓字段和跨方法共享的变量用FreeAndNil置 nil 能在后续误用时立刻暴露而不是对着一个已释放的内存踩下去。这条规则的实战价值很高我在排查释放后再访问这类问题时十次有八次是因为字段只Free没置 nil。还有一点容易被忽略赋 nil 表示所有权转移这个约定必须在代码里显式写出来。上面模板里的NewIndex : nil如果漏了SwapIndex之后对象被下一次 finally 释放掉缓存直接变成悬空指针而错误会在几分钟后的另一次读取时才爆出来跟现场完全对不上。3.2 文件与原生句柄Close 和 Free 要分开写TFileStream的常见写法是把Free放在 finally 里这没错但不够。因为Free会连带关闭底层句柄如果关闭过程中抛异常比如网络盘断开finally 里的Free抛出的异常会覆盖原始异常前面 2.3 节讲过这个坑。我的做法是把Close和Free拆开关闭失败只记日志procedure TLogArchiver.Archive(const SrcFile: string); var Src, Dst: TFileStream; Buf: array of Byte; Read: Integer; begin Src : TFileStream.Create(SrcFile, fmOpenRead or fmShareDenyWrite); try Dst : TFileStream.Create(SrcFile .gz.tmp, fmCreate); try SetLength(Buf, 64 * 1024); repeat Read : Src.Read(Buf[0], Length(Buf)); if Read 0 then CompressWrite(Dst, Buf, Read); until Read 0; finally try Dst.Free; except on E: Exception do Log.Error(归档写入流关闭失败 E.Message); end; end; finally try Src.Free; except on E: Exception do Log.Error(源文件流关闭失败 E.Message); end; end; end;嵌套的 try...except 看着有点啰嗦但这是我在处理网络盘、移动存储这类关闭也可能失败的场景时唯一放心的写法。本地磁盘当然可以直接Free但代码往往是复用的今天写本地明天改网络不如一次写对。3.3 锁与事务finally 是唯一可靠的解锁点临界区、互斥锁、数据库事务这三样东西有一个共同点忘记释放的后果比忘记 Free 一个对象严重得多——对象漏了只是内存涨锁漏了就是整个服务卡死。procedure TInventoryService.Adjust(const Sku: string; Delta: Integer); begin FInventoryLock.Enter; try FConn.StartTransaction; try ApplyDelta(Sku, Delta); WriteAuditLog(Sku, Delta); FConn.Commit; except on E: Exception do begin if FConn.InTransaction then FConn.Rollback; Log.Error(Format(库存调整失败 sku%s delta%d err%s, [Sku, Delta, E.Message])); raise; end; end; finally FInventoryLock.Leave; // 无论哪条路径都必须走到这里 end; end;注意InTransaction这个判断。很多数据库组件在连接异常断开后事务状态已经不明确了直接Rollback又会抛第二个异常原始信息就丢了。所以回滚前先判断回滚本身再包一层保护这是我在生产代码里固定下来的写法。锁还有超时问题。Enter无参数版本会一直等下去一旦某个线程因为死循环没释放整条服务线就僵在那里。我现在的习惯是统一带超时if not FInventoryLock.TryEnter(3000) then raise EResourceBusy.Create(库存锁等待超过 3 秒sku Sku);3 秒这个值是算出来的不是拍的。库存调整单次业务耗时实测 P99 在 180 毫秒左右乘以 10 倍留足余量约 1.8 秒再考虑高并发下的排队3 秒是个既能容忍正常排队、又不会让调用方干等太久的值。如果业务耗时变了这个参数要跟着重算。3.4 一次批量导入的参数计算与实测记录事务边界的划分是 finally 用得最多、也最容易算错的地方。我们有个从外部文件导入 10 万条记录的任务最初是每条一个事务跑完要 40 多秒后来改成全体一个事务快了但内存暴涨且失败时全部回滚。最后按批划分参数是这么定的单条插入实测耗时约 0.4 毫秒一次事务提交固定开销约 15 毫秒。设每批 N 条总条数 100000则提交次数为 100000/N提交总开销为15 × 100000/N毫秒插入开销恒为0.4 × 100000 40000毫秒。N 1提交开销 1500000 毫秒显然不能这么干实际测试因为批处理优化没有这么夸张但量级方向是对的N 500提交 200 次开销 3000 毫秒合计约 43 秒N 2000提交 50 次开销 750 毫秒合计约 40.8 秒N 10000提交 10 次开销 150 毫秒合计约 40.2 秒继续加大 N 收益递减但内存占用和锁持有时间线性上升。10 万条记录单条约 600 字节N 2000 时批数据约 1.2MB属于完全可控的量级N 10000 时 6MB在并发三四个任务时就开始吃内存了。最终选 2000。每批条数提交次数提交开销(ms)批数据内存失败回滚代价500200约 3000约 300KB极低200050约 750约 1.2MB低1000010约 150约 6MB中对应的代码骨架就是这样批循环 每批一个事务 finally 保证收尾procedure TImportJob.Execute; const BATCH_SIZE 2000; var Batch: TListTImportRow; Row: TImportRow; Count: Integer; begin Batch : TListTImportRow.Create; try Count : 0; while FReader.Next(Row) do begin Batch.Add(Row); if Batch.Count BATCH_SIZE then begin FlushBatch(Batch); Inc(Count, Batch.Count); Batch.Clear; end; end; if Batch.Count 0 then begin FlushBatch(Batch); Inc(Count, Batch.Count); end; Log.Info(Format(导入完成共 %d 条, [Count])); finally Batch.Free; FReader.Close; // 读取器必须关否则文件句柄挂着 FReader : nil; end; end; procedure TImportJob.FlushBatch(ABatch: TListTImportRow); begin FConn.StartTransaction; try InsertRows(ABatch); FConn.Commit; except on E: Exception do begin if FConn.InTransaction then FConn.Rollback; Log.Error(Format(批次写入失败本批 %d 条已回滚%s, [ABatch.Count, E.Message])); raise; end; end; end;FReader.Close放在 finally 而不是跟在循环后面是因为循环里任何一个FlushBatch抛异常都会让读取器失去关闭机会下次再导入同一个文件就是文件被占用。3.5 老代码迁移从裸清理到 finally 的六步走手上有大量历史代码没有 finally我的迁移流程是固定的按步骤来基本不会出事故先定位分配点搜索Create、Open、BeginTransaction、Enter列出每个过程里的资源清单。标出释放点把Free、Close、Commit、Leave的位置圈出来看是否和分配点在同一过程内。找出所有出口Exit、raise、循环里的Break每一个都要在脑子里过一遍会不会跳过释放点。加 try 包住使用区间从第一个使用语句开始到最后一次使用结束这一段整体进 try释放语句移到 finally。处理所有权转移过程中如果有对象交给别人管的情况在转移点后显式赋 nil并加注释。补回归用例重点是制造异常路径——故意让中间某一步失败验证资源确实被释放了用句柄计数或者内存快照验证。第 6 步是最容易被省略、也最能救命的。我一般会临时在第 4 步的 try 块中间插一句raise Exception.Create(test);跑一遍看任务管理器里的句柄数有没有回落回落了就说明写对了然后把测试语句删掉。4. 常见问题与排查技巧实录4.1 问题速查表下面这张表基本覆盖了我这几年遇到的 finally 相关问题按出现频率排序现象最可能的原因排查手段修复方向异常凭空消失上层无感知finally 里有 Exit / 封装代码吞了异常在 finally 首行加日志断点移除 finally 内的流程控制语句日志里只有清理阶段的错误finally 里 raise 覆盖了原异常看调用栈最内层是不是清理代码清理异常改为只记录不抛出句柄数持续上涨分配点在 try 外抛异常时未释放任务管理器或性能计数器观察句柄曲线把分配点挪进 try或改用嵌套结构对象已释放仍被访问偶发崩溃字段 Free 后未置 nil打开 FastMM 一类的内存诊断字段统一用 FreeAndNil服务卡死无响应锁在异常路径上未释放转储线程栈看谁持有锁锁的 Enter/Leave 必须进 try...finally事务已提交但数据不对回滚逻辑写在提交之后通读 finally 块顺序回滚必须在提交失败分支内清理代码本身抛异常关闭网络流、关闭已断开的连接单独 try 包住清理并记日志清理阶段异常降级为日志4.2 三个现场踩坑记录第一个坑是服务运行三天后必然挂。现象是导出功能全部失败报文件无法打开。查了两天最后发现是某个导出过程中模板文件读取失败时走了Exit早退而那一版代码的Free写在早退语句后面。修复方式很简单把整个使用区间包进 tryFree放 finally。但从这件事之后我在团队规范里加了一条硬性要求任何Exit出现的位置其上方必须有对应的 try...finally否则代码评审不通过。第二个坑更隐蔽。有个批量处理任务偶发少处理了最后一批数据日志显示任务成功完成。翻了很久才发现finally块里有个Exit是在某次优化时顺手加的——原作者的意思是如果读取器已关闭就提前退出清理流程结果它把外层抛出的异常吞了任务被当成成功。这个坑教育我finally 里的代码要当作没有控制流的纯清理来写条件判断可以有比如if InTransaction但不能有跳出语句。第三个坑跟性能有关。有一段代码在 finally 里写了比较重的操作每次都会把缓冲区刷新到磁盘并计算校验和导致整体耗时增加了近 30%。这个不怪 finally 本身怪的是把本该放在主流程里的收尾计算挪到了清理路径上。finally 里只放必须执行且开销小的操作重活要么提前做要么挪到专门的后置步骤里。4.3 调试与自检的几个小技巧第一用TStopwatch给关键 try 块计时如果发现某个过程的耗时和内部逻辑对不上八成是 finally 里有隐藏开销我上面那个 30% 的例子就是这么发现的。第二把清理阶段单独打上标记日志格式统一成cleanup: xxx出问题时按关键字过滤一眼就能看出异常路径走到了哪一步清理。这套标记在排查异常被吞类问题时特别有效因为你能通过缺失的清理日志判断出代码在哪一行飞出去了。第三写一个简单的资源泄漏自检函数在开发期周期性地打印当前进程句柄数和对象实例数跑长稳测试时挂上去。曲线应该是平的一旦发现有斜率就说明有 finally 漏了。第四Delphi 11 的调试器在异常断点上做得比较方便把抛出异常时中断打开再配合调用栈能很直观地看到 finally 有没有被执行——断点停在 finally 的首行说明路径是对的没停就直接跳到外层那就说明中途有出口绕过了它。5. 关于风格与取舍的一些个人习惯5.1 代码风格上的几条约定清理语句的书写顺序我统一成关闭 → 置空 → 释放也就是先Close断掉外部依赖再把引用置 nil 防止误用最后Free回收内存。这个顺序在多线程环境下很关键因为析构过程中对象的字段还在别的线程如果恰好拿到引用就会读到半死不活的状态。另一个约定是try 的范围要尽量小。有些人的写法是整个方法体包一个大 tryfinally 里堆七八行清理看着整洁实际上出问题时根本分不清是哪一块清理失败。我倾向于按资源配 try一个 try 对应一类资源嵌套三层也比一个巨型 try 好维护。还有一处细节清理代码里不要调用可能触发业务逻辑的方法。我见过在 finally 里调用事件回调的写法回调里又去操作已经释放了一半的对象结果就是随机崩溃。清理阶段就应该只做减法不做任何可能引发新分配的调用。5.2 什么时候我会主动放弃 finallyfinally 不是万能的。有几种场景我反而会避开它改用别的结构。一是纯粹的短生命周期值对象比如一个只在三行代码里用到的临时TStringList我会考虑改用接口化的临时对象或者记录类型让编译器在作用域结束时自动回收省掉一层缩进。二是确定不会抛异常的纯计算逻辑硬塞一个 try...finally 只会让人误以为这里有资源需要关心反而是噪音。三是涉及异步和回调的场景finally 只能覆盖同步执行路径回调里抛出的异常它管不着这种地方要靠状态机或者显式的会话管理而不是指望一个 finally 兜住全部。最后再分享一个我最近在做的小调整把异常处理的日志格式统一成入口 → 清理 → 出口三段式每段带同一个流水号。这样一段业务流程在日志里的轨迹非常清楚哪一步的清理没走到扫一眼就知道。这个改动本身不涉及任何技术难点但把排查效率提了一大截比在代码里多写十行注释有用得多。
RELATED

相关推荐

PCB设计规范深度解析:从网络表到布局布线的工程机制与电磁学原理

PCB设计规范深度解析:从网络表到布局布线的工程机制与电磁学原理

简介:这份《华为PCB设计规范》面向PCB设计工程师、电子工程师及项目经理,提供一套可参照的企业级设计准则,帮助解决布局布线、评审流程与工艺要求缺乏统一标准的问题。资源为单个PDF文件,压缩包约248KB,内容按章节组织…

📅 2026/10/26 6:14:26
数码摄影中深度信息丢失的原因与还原技术

数码摄影中深度信息丢失的原因与还原技术

1. 相机丢失深度信息现象解析第一次用单反拍完照片导入电脑时,发现原本在取景器里层次分明的场景变得扁平化,那种立体感完全消失了——这就是典型的深度信息丢失。这种现象在摄影圈被称为"平面化诅咒",几乎所有从光学取景器转向数码…

📅 2026/10/26 6:14:26
Java工程师AI应用开发指南:LangChain4j从入门到生产实践

Java工程师AI应用开发指南:LangChain4j从入门到生产实践

1. 从Java工程师到AI应用开发:为什么LangChain4j值得你花时间如果你是一名写了几年Java的开发者,最近大概率会有一种隐隐的焦虑:身边做Python的同事开始聊RAG、聊Agent、聊大模型应用开发,而自己还在写CRUD、调MyBatis的XML、处理…

📅 2026/10/26 6:14:26
MORE NEWS

更多资讯

📰

基于YOLOv5的道路坑洼检测系统设计与实现

1. 项目概述:道路坑洼识别的现实意义与技术路径道路坑洼识别是智慧交通和城市基础设施维护中的重要课题。传统的人工巡检方式效率低下且成本高昂,尤其在雨雪天气后,路面损坏往往呈现爆发式增长。我在参与某市市政工程时亲眼见过,一…

📰

基于多目标粒子群算法的水下图像增强技术

1. 水下图像增强的挑战与机遇水下摄影在海洋勘探、水下考古、渔业监测等领域有着广泛应用,但获取高质量水下图像一直是个技术难题。光线在水中的传播特性与空气中截然不同,这导致水下图像普遍存在对比度低、颜色失真、细节模糊等问题。水对光线的吸收具有…

📰

EECS designlab7 光敏电阻机器人实验:从分压电路到CMax仿真与负载效应排查

简介:本资源为山东大学崇新学堂EECS design lab7实验报告文档,面向修读该课程的学生及对电路设计与仿真感兴趣的初学者。内容围绕机器人头部项目展开,涵盖电路原型工具使用、电压分压器与电位器原理、CMax仿真工具操作、光敏电阻光传感器电路…

📰

多线程编程核心概念与实践指南

1. 多线程编程入门指南刚接触多线程编程时,我完全被各种概念搞晕了。线程、进程、锁、同步...这些术语像天书一样。直到真正动手写代码,才明白多线程其实就像餐厅里的多个服务员同时为顾客服务。每个服务员(线程)都能独立工作,但又需要协调配…

📰

AI Agent如何优化项目结构管理与Vibe Coding实践

1. 项目概述:AI Agent如何优化项目结构管理在当今快节奏的软件开发环境中,Vibe Coding(氛围编程)作为一种新兴的编程理念,强调开发环境与工作流程的和谐统一。而AI Agent作为这一理念的技术载体,正在彻底改…

📰

鲲鹏ARM64麒麟V10源码编译安装Argon2与调参实战

密码哈希这件事,平时不怎么显眼,一旦要落地到一套新架构上,麻烦就全冒出来了。最近我在一套鲲鹏 ARM64 服务器配麒麟 V10 的环境里,把 argon2 从源码编译、安装、验证到集成完整走了一遍。标题里说的“鲲鹏 ARM 架构 麒麟 V10 安装…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬