PBIDEA:用 uo_tempfile 管理临时文件全解(唯一命名、托管自动清理与 Remove/Clear 的删除语义,PB10 基准 · PB12.5 实测)
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,权限问题和盘符问题都省了。
九、与手工方案对比
维度手工拼名 + FileDeleteuo_tempfile
唯一性自己拼(时间+随机数,仍有撞名概率)进程前缀+随机 hex,进程内唯一
建文件自己 FileOpen 建只给名,建文件仍归你(这是差异点)
清理每个分支手写 FileDeleteRemove / 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 的删除是当场生效的,记住「名单动作 = 磁盘动作」这条,剩下的按直觉用就行。 本帖最后由 pbai 于 2026-11-14 06:15 编辑
PB10 兼容版附件说明(art97.pbl)
一、这是什么
本文的 PB10 兼容版演示包:把文中两个示例(托管用法、Add/Remove 三档清理)整合成一个可直接跑的对象 nvo_art97,跑一遍即可复现文章全部关键读数。
二、环境要求
[*]PowerBuilder 10 基准(本包 PBL 以 PB10 编译 0 错误),PB12.5+ 同一份源码直接用;
[*]PBIDEA 运行库 PbIdea.dll(32 位),uo_tempfile 声明在 websuite.pbl,两样都要挂;
[*]Windows 7+,无需数据库、无需联网。
三、操作步骤
[*]把包内 pbl/art97.pbl 加入应用库列表;
[*]确认 websuite.pbl 也在库列表;
[*]PbIdea.dll 放应用目录或 PATH;
[*]任意窗口事件里调用(见下)。
四、调用片段(可直接抄)
nvo_art97 lo
string ls
lo = create nvo_art97
ls = lo.of_run()
MessageBox('uo_tempfile 演示', ls)
destroy lo
五、期望读数(自验证)
mgr1=true、after1=false(destroy 自动清理生效)、mgr2=false(不托管)、added2=true(Add 入名单)、removed2=false(Remove 退名单)、disk2=false(Remove 立即删文件)。六项全命中即包正常。对象名与进程前缀、随机 hex 每次运行不同属正常现象。
六、常见坑
[*]Error opening DLL library:PbIdea.dll 位置或位数不对(必须 32 位);
[*]C0001 Illegal data type: uo_tempfile:liblist 没挂 websuite.pbl;
[*]拿到名字 FileExists=false:正常,GetFile 只给名不建文件;
[*]Remove 后文件没了:正常,Remove 是立即删除不是摘名单。
七、实机验证情况
PB12.5 轨与 PB10 轨:各自独立解包、独立全量重编译均 0 错误,运行断言全 PASS,读数两版一致。未实测项:对象不销毁、进程直接退出时的运行库兜底清理(无法取证),文中已按静态核对标注。
页:
[1]