马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PBIDEA:用 uo_tempfile 管理临时文件全解(唯一命名、托管自动清理与 Remove/Clear 的删除语义,PB10 基准 · PB12.5 实测)
阅读说明
- 适用版本:PB10 基准(PB12.5 实测,两版行为一致);对象声明在 PBIDEA 的 websuite.pbl 中,运行需 PbIdea.dll 运行库。
- 支持数据库:本文不涉及数据库,纯本地文件操作。
- 操作系统与环境:Windows 7+;临时目录跟随系统的 TMP/TEMP 环境变量;无需联网。
- 难度系数:★☆☆☆☆
- 前置知识:PowerScript 基本文件读写(FileOpen / FileWrite / FileClose)。建议先读本系列 uo_file 一篇,对 PBIDEA 文件类对象有个整体印象。
一、临时文件的老大难:PB 原生连个临时文件函数都没有
写导出功能的人几乎都绕不开临时文件:把查询结果落成 csv 给用户看一眼、给第三方组件喂一个中间文件、解压一个包到临时目录再处理。这事在别的语言里一行就解决,在 PowerBuilder 里得自己造轮子。
造轮子之前,我们按惯例先做了一轮负向探测,把「原生有没有现成函数」这件事在两版环境里各编译一次验证了一遍:
- GetTempFile():PB12.5 与 PB10 双轨编译,一律报 C0051(Unknown function name);
- GetTempDirectory():同样双轨 C0051;
- 翻遍本机 PB12.5 帮助知识库(2138 个条目),没有任何临时文件相关的系统函数。
结论很干脆:PowerBuilder 原生没有临时文件 API。FileDelete 是有的(实测可用),FileOpen / FileWrite 也都在,但「给我一个不重名的临时文件名」「用完自动清掉」这两件事,原生帮不上忙。
于是手工方案通常长这样:拿系统时间加随机数拼一个名字、写到 GetCurrentDirectory() 或拼一个自以为是的临时目录、用完记得 FileDelete。三个痛点也就跟着来了:
- 重名。两个实例并发跑,拼出来的名字撞了,数据互相覆盖;
- 清理遗漏。正常分支记得删,异常分支忘了删,跑上几个月,应用目录里全是 xxx_tmp.csv;
- 无处安放。临时文件写哪儿都有讲究,写程序目录会越积越多,写系统盘根目录权限又不够。
PBIDEA 的 uo_tempfile 就是冲着这三件事来的。这一篇把它 8 个方法的真实行为全部实机跑一遍,重点讲清一个最容易踩的坑:Remove 和 Clear 不是「摘除登记」,是立即删文件。
二、uo_tempfile 全貌:一个管理器,八个方法
uo_tempfile 是非可视对象(NVO),声明在 websuite.pbl,运行依赖 PbIdea.dll。它没有公开属性,所有能力集中在八个方法上:
| 方法 | 签名 | 返回 | 实测行为 | | GetFile | 无参 | string(全路径) | 进程hex_32位大写hex,只给名不建文件 | | GetFile | (fileNameOrExt) | string | 以点开头当扩展名,否则当完整文件名 | | GetFile | (fileNameOrExt, addManager) | string | 第二参传 false 则不托管 | | Add | (fileName) | 无(subroutine) | 立即登记托管;重复 Add 无害 | | Remove | (fileName) | 无(subroutine) | 立即删除文件并摘除登记 | | Exist | (fileName) | boolean | 查托管名单,与磁盘无关 | | Clear | 无参 | 无(subroutine) | 立即删除全部托管文件 | | GetPath | 无参 | string | 系统临时目录,跟随 TMP/TEMP 环境变量 |
构造函数和析构函数内部已自动挂钩 Init / UnInit:create 时初始化管理器,destroy 时把仍在托管名单里的文件全部删掉。你不需要调用任何「初始化」动作。
名字长什么样
实测(同一进程里连调三次无参 GetFile):
- 三次返回各不相同,前缀是 8 位十六进制的进程标识,本进程内固定(例如 007580d8),后面跟 32 位大写十六进制随机串;
- 完整形态是全路径:临时目录 + 进程前缀 + 随机串,可以直接扔给 FileOpen;
- 换一个进程再跑,前缀就变了。也就是说名字在进程内唯一、跨进程必不同,并发安全这一点是白送的。
一个必须先说清的边界:GetFile 只给名,不建文件
实测最容易被误判的一件事:调用 GetFile 之后立刻 FileExists,结果是 false。它只负责生成一个保证唯一的名字,文件本身不存在,要你自己去写。所以「拿到名字」和「文件可用」是两步,写代码时别把 FileExists=false 当成出错了。
三、最小可用示例
下面这段代码可以直接放进窗口的 open 事件(或任何函数)里跑。输入值只有文件内容,前段定义好了;结尾用 MessageBox 输出全过程。
- // 托管用法:拿名 → 写文件 → 用 → destroy 自动清理
- uo_tempfile lt
- string ls_file, ls_report
- long ll_f
- lt = create uo_tempfile // 创建管理器,构造时已自动初始化
- ls_file = lt.GetFile('.csv') // 拿一个随机名、.csv 扩展名的临时文件全路径(此时文件尚不存在)
- ll_f = FileOpen(ls_file, LineMode!, Write!, LockWrite!, Replace!)
- FileWrite(ll_f, 'name,qty') // 写表头
- FileWrite(ll_f, 'apple,3') // 写数据行
- FileClose(ll_f)
- ls_report = '文件:' + ls_file
- ls_report = ls_report + '~r~n写完之后磁盘上:' + string(FileExists(ls_file))
- ls_report = ls_report + '~r~n管理器登记中:' + string(lt.Exist(ls_file))
- destroy lt // 析构即删除全部托管文件,这里 ls_file 被删掉
- ls_report = ls_report + '~r~ndestroy 之后:' + string(FileExists(ls_file))
- MessageBox('uo_tempfile 演示', ls_report)
复制代码
实际运行结果(本机实测):写完之后磁盘上 True、管理器登记中 True、destroy 之后 False。中间那个 csv 文件从生到死一行清理代码都没写,这就是托管的意义。
四、三种取名方式:无参、扩展名、完整文件名
GetFile 的第二形态参数名叫 fileNameOrExt,实测它确实是「二合一」语义:
| 调用写法 | 实测返回(截取文件名部分) | 语义 | | GetFile() | 007580d8_93B328A6…B6C | 随机主体,无扩展名 | | GetFile('.xlsx') | 00652df0_571AFFAC….xlsx | 随机主体,扩展名生效 | | GetFile('mydata.dat') | 00652df0_mydata.dat | 完整文件名保留 | | GetFile('rep97.xlsx') | 00652df0_rep97.xlsx | 同上,名字随意 |
规律一句话:参数以点开头 → 当扩展名用,主体仍然随机;否则 → 当完整文件名用。两种情况下,返回串前面都会拼上进程前缀(那个 8 位十六进制),所以就算你指定了 mydata.dat,实际落盘名字也是 00652df0_mydata.dat,不会真叫 mydata.dat。这一点在按名字找文件时要注意——别拿你传进去的名字去拼路径,要拿返回值。
第三形态再加一个 addManager 参数,传 false 表示「名字照给,但我不托管」:
- 不托管文件的 Exist 恒为 false;
- 对象 destroy 之后,托管文件被删,不托管文件原样保留(实测:托管侧 post=false,不托管侧 post=true)。
什么时候用不托管?这个临时文件你打算给别人、要挪走、或者要活过本对象的生命周期时,就别让管理器替你删。
五、Exist 查的是名单,不是磁盘
Exist(fileName) 的语义实测是这个路径是否登记在托管名单里,跟文件在不在磁盘上没有关系。两个证据:
- 一个自己手工创建、真实存在的文件,Add 之前 Exist=false(磁盘上有,名单上没有);
- GetFile 之后文件根本没建,Exist=true(名单上有,磁盘上没有)。
所以 Exist 不能当「文件存在判断」用——那是 FileExists 的活。它真正的用途是回答「这个文件还归不归我管」。
六、Remove 与 Clear:都是立即删除
这是全文最反直觉、也最容易出事故的一处。看名字容易以为 Remove 是「从名单里摘掉」、Clear 是「清空名单」,实测两者都是先把文件从磁盘上删掉,再摘登记:
- Remove(路径):立即删除该文件。实测:Remove 之后再查 FileExists,结果 false——注意这一步查在 destroy 之前,文件确实当场就没了;
- Clear():立即删除名单里全部托管文件。实测:Clear 之后两个托管文件的磁盘存在性都是 false;
- Remove 一个没登记过的路径、Add 一个不存在的路径,都不报错(subroutine 无返回值,实测无异常抛出),只是名单上没它的位置,destroy 时自然也不会去删一个不存在的东西。
把这三条合起来,清理策略就很清晰了。下面这段代码把三档清理各演示一遍,可直接放进窗口事件或函数里运行:
- // 三档清理:Remove 单删、Clear 全删、destroy 兜底
- uo_tempfile lt
- string ls_a, ls_b, ls_report
- long ll_f
- lt = create uo_tempfile
- ls_a = lt.GetFile('a.log', false) // 拿两个不托管的名字(先自己建出来)
- ls_b = lt.GetFile('b.log', false)
- ll_f = FileOpen(ls_a, LineMode!, Write!, LockWrite!, Replace!)
- FileWrite(ll_f, 'a') // 建第一个文件
- FileClose(ll_f)
- ll_f = FileOpen(ls_b, LineMode!, Write!, LockWrite!, Replace!)
- FileWrite(ll_f, 'b') // 建第二个文件
- FileClose(ll_f)
- lt.Add(ls_a) // 登记进托管名单
- lt.Add(ls_b)
- ls_report = '登记后 A=' + string(lt.Exist(ls_a))
- lt.Remove(ls_a) // 第一档:当场删 A
- ls_report = ls_report + '~r~nRemove 后 A:' + string(FileExists(ls_a))
- ls_report = ls_report + '~r~nRemove 后 B 还在:' + string(FileExists(ls_b))
- lt.Clear() // 第二档:全删,B 也当场消失
- ls_report = ls_report + '~r~nClear 后 B:' + string(FileExists(ls_b))
- destroy lt // 第三档:兜底(此时名单已空,无可删)
- MessageBox('三档清理演示', ls_report)
复制代码
实测输出:登记后 A=True、Remove 后 A:False(当场删)、Remove 后 B 还在:True、Clear 后 B:False(也当场删)。
七、生命周期:destroy 即清理,进程退出有兜底
托管文件的删除时机实测有两档:
- destroy 对象:析构钩子 UnInit 立即清名单,实测 destroy 之后托管文件全部消失;
- 进程退出:如果对象一直活着没 destroy,理论上来不及走析构,运行库在退出时也会做一轮清理。这一条在实机测试环境里无法直接观察(进程一退所有证据都没了),本段标注:未实测,仅静态核对源码注释(源码注释原文:退出时会自动删除)。
实践建议别赌第二条:临时文件的生命周期要短,用完 destroy 或显式 Clear,别让「退出兜底」变成常态。
顺带说一个边角:destroy 之后再访问对象的任何成员(比如 GetPath),直接报 Null object reference 运行时错误,双轨实测一致,TRY-CATCH 能捕到。这属于 PB 通用行为,列在这里只是提醒:管理器用完再 destroy,别销毁了还想接着用。
八、GetPath 与临时目录的选择
GetPath() 返回的就是它给文件安排的目录。实测返回值是 C:\Users\…\AppData\Local\Temp\…\ 这样的路径——跟着进程的 TMP/TEMP 环境变量走,不是硬编码某个目录。本机测试时进程的 TEMP 被指到了一个自定义目录,GetPath 就老老实实返回了那个目录。
这带来两个实用推论:
- 部署时如果需要把临时文件集中到某个盘(比如 D 盘空间大),可以在这个进程内设置 TEMP 环境变量(配套的 uo_sysenv 能做),GetPath 和 GetFile 就都跟着过去了;
- 拼自己的文件名时用 GetPath() + '你的名字' 而不要硬编码 C:\Windows\Temp,权限问题和盘符问题都省了。
九、与手工方案对比
| 维度 | 手工拼名 + FileDelete | uo_tempfile | | 唯一性 | 自己拼(时间+随机数,仍有撞名概率) | 进程前缀+随机 hex,进程内唯一 | | 建文件 | 自己 FileOpen 建 | 只给名,建文件仍归你(这是差异点) | | 清理 | 每个分支手写 FileDelete | Remove / Clear / destroy 三档自动 | | 异常路径 | try 外忘删就残留 | destroy 兜底,托管名单内的必被清 | | 临时目录 | 自己猜(硬编码风险) | GetPath 跟随 TMP/TEMP |
手工方案唯一「赢」的地方是自由:想删哪个删哪个、想什么时候删什么时候删。但 Remove / Clear / destroy 三档已经覆盖了手工的全部场景,而且不会忘。结论:除非临时文件要跨进程交接(那用不托管 + 自己管理),否则没有理由再手工拼。
十、坑清单(全部实测背书)
- GetFile 只给名不建文件:拿到路径立即 FileExists=false,是正常的,接着写就行;
- Exist 是名单语义:查「是否托管」,不查磁盘;判断文件在不在用 FileExists;
- Remove 立即删文件:不是摘名单,删完就没了,还想用就得重写;
- Clear 立即删全部:按下 Clear 之前确认名单里没有还要用的文件;
- destroy 即清理:托管文件在 destroy 那一刻全部消失,之后还想读?没门。要留活口的用 addManager=false;
- 指定了名字也会被加前缀:GetFile('mydata.dat') 落盘名是 进程hex_mydata.dat,找文件要用返回值,不要用传入值拼路径;
- 以点开头才是扩展名:GetFile('.csv') 随机主体 + .csv;GetFile('csv') 会得到一个叫 进程hex_csv 的无扩展名文件;
- GetFile('') 空串:按无扩展名处理,返回随机名,不崩溃、不报错;
- GetPath 跟随 TMP/TEMP:不是固定目录,程序在不同机器上返回值不同,别拿返回值做业务判断;
- Add 重复无害:同一文件 Add 两次,Exist 仍 true,destroy 只删一次;
- PB 原生没有 GetTempFile / GetTempDirectory:双轨编译 C0051 实证,帮助库亦无,不要花时间找原生替代;
- destroy 后访问必炸:Null object reference,TRY-CATCH 可捕;先 destroy 后使用的写法在代码评审时直接打回。
十一、小结
uo_tempfile 干的事很小,但把「临时文件」这个没人愿意认真处理的问题做成了三句话:GetFile 给你一个保证唯一的名字,Exist 告诉你它归不归管,Remove / Clear / destroy 负责收拾干净。唯一的语义陷阱是 Remove 和 Clear 的删除是当场生效的,记住「名单动作 = 磁盘动作」这条,剩下的按直觉用就行。 |