马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PowerBuilder DataWindow 排序与过滤全解:SetSort / Sort / SetFilter / Filter 的表达式语法、Filter 缓冲区与还原顺序坑实测(PB10 基准 · PB12.5 实测)
阅读说明
① 适用版本:PB10 基准 · PB12.5 实测(两版全量编译均 0 错误,20 组行为用例双轨读数逐字一致,个别差异见「PB12.5 差异」一节)。
② 支持数据库:本文不涉及数据库——排序与过滤都发生在 DataWindow 已经取到数据之后,示例用 ImportString 喂数据,不需要连接任何数据库。
③ 操作系统与环境要求:Windows 7 及以上;PB10 或 PB12.5 任意一个能跑起来的开发环境即可;示例只用了 DataStore,不需要窗口也能运行。
④ 难度系数:★★☆☆☆(函数本身简单,坑在缓冲区语义与还原顺序)。
⑤ 其它阅读说明:需要知道 DataStore 的基本用法(Create、ImportString);缓冲区概念可先读本目录《DataWindow 三缓冲区详解》;文中每个结论都来自实机运行读数,不是照抄手册。
排序和过滤是 DataWindow 用得最多的两个数据整理动作。它们的 API 一共只有四个函数:SetSort、Sort、SetFilter、Filter。函数签名非常简单,但这次实机验证发现,真正决定代码对不对的,是它们背后的几条语义规则——比如「还原过滤时顺序不保证」「相同键的行顺序会被打乱」,这些坑手册上都不写。下面按实测结果逐条讲。
一、两步制:先 Set 定表达式,再执行
排序和过滤都是「两步制」:第一步 SetSort / SetFilter 只把表达式记录下来,第二步 Sort / Filter 才真正动数据。
- lds.SetSort('id D') // 第一步:只是记下「按 id 降序」,数据没动
- lds.Sort() // 第二步:真正排序
- lds.SetFilter('id > 2') // 第一步:记下过滤条件,数据没动
- lds.Filter() // 第二步:真正过滤
复制代码
这个设计的好处是表达式可以只设一次、反复执行。实测还有两个「未设置就执行」的行为:
- 没有 SetSort 直接 Sort():返回 1,数据保持原顺序不变;
- 没有 SetFilter 直接 Filter():返回 1,行数不变,数据不丢。
两版行为一致。也就是说 Sort / Filter 单独调用是安全的,不会把数据搞乱,但也不会有任何效果。
二、排序表达式语法:列名、方向、多列、列号
SetSort 的表达式是一条字符串,实测以下四种写法全部有效(返回 1):
| 写法 | 含义 | 实测结果 | | 'id D' | id 降序 | 生效,3/cc 排第一行 | | 'id A' | id 升序 | 生效 | | 'name A, id D' | 先按 name 升序,同 name 内按 id 降序 | 生效:aa(2)、aa(1)、bb(5)、bb(4) | | '#2 D' | 按第 2 列(列号)降序 | 生效,y 排在 x 前 |
要点:
- 方向只认 A / D 两个字母,跟在列名后面,用空格分隔。多列之间用逗号。
- 列号写法是井号加数字,例如 '#2 D' 表示第二列。列是按 DataWindow 里的列序号数,不是按表里的位置。
- 大小写不敏感:'id d' 与 'id D' 等价。
三、数值列按数值排,字符列按字典序排
这是新手最容易踩的坑之一。同样的内容放在不同类型的列里,排序结果完全不同:
- 数值列(long 等)按数值大小排:1、2、3;
- 字符列按字典序逐字符比:'10' 排在 '9' 前面(因为 '1' 的字符码比 '9' 小)。
实测字符列放 '9'、'10'、'b'、'A' 四个值,SetSort('name A') 后顺序是:
三条规则都在这行读数里:数字字符按码点比('1' < '9');大写字母排在小写前面('A' < 'b',ASCII 里 'A' 是 65、'a' 是 97);没有任何「自然数字排序」。如果业务上需要「按数值含义排字符列」,要么把列改成数值类型,要么自己补零对齐(比如都补成 4 位 '0009'、'0010')。
四、Sort 是不稳定排序:相同键的行顺序会被打乱
这是本轮实测里最重要的发现。先把「稳定排序」说清楚:稳定排序在遇到键值相同的两行时,会保持它们原来的先后顺序;不稳定排序则会打乱。
实测数据:三行 7/first、3/mid、7/second,按 'id A' 排序后:
两个 id=7 的行,second 反而排到了 first 前面,原始顺序被打乱了。换一组数据(7/aaa、3/mid、7/bbb)再测,结果同样是 bbb 排到 aaa 前面。两次都是 1:3、2:7、3:7 之外,同键内部顺序与原始相反——这不是巧合,PB 的 Sort 就是不稳定排序。
对业务的影响:如果你的数据是「先按入库时间排好,再按金额排序」这种依赖二次排序的场景,同金额的行顺序就不可预期。解决办法是把入库时间也放进排序表达式:'amount D, inputtime A',让键唯一化,而不是指望引擎替你保持原序。
五、设置失败的返回值,以及「读不回来」这件事
两个 Set 函数的返回值都是 1 成功、-1 失败,实测非法输入:
- SetSort('nosuchcol A') 返回 -1;之后调用 Sort() 仍返回 1,数据原样不动——Sort 不报错也不出错,只是拿一个无效表达式什么都没做;
- SetFilter('nosuchcol > 2') 返回 -1;之后 Filter() 同样返回 1、行数不变。
也就是说:Set 失败后接着执行是安全的(无副作用),但你必须自己检查 Set 的返回值,否则用户看到的会是「点了排序没反应」。
更要命的是「读不回来」。实测确认了两件事:
- GetSort() 这个函数不存在(编译期直接报 C0051 Unknown function name);
- Describe('datawindow.sort') 与 Describe('datawindow.filter') 在 SetSort / SetFilter、Sort / Filter 前后都返回 '!'('!' 在 DataWindow 表达式里表示属性不存在)。
结论:排序表达式和过滤表达式设进去就读不出来。如果你的程序需要「记住当前排序以便还原」,请自己用一个 string 变量把表达式存起来,不要指望从 DataWindow 里拿回来。
另外 SetSort('') 是合法的:返回 1,相当于清掉排序表达式,之后再 Sort() 数据保持当前顺序不变。
六、过滤表达式语法:比较、逻辑与字符串字面量
SetFilter 的表达式和 DataWindow 的验证规则、颜色表达式同源,支持比较运算、逻辑运算和列引用。实测两类写法:
- lds.SetFilter('id > 2') // 数值比较,直接写
- lds.SetFilter('name = "ee"') // 字符串比较,字面量必须用双引号
复制代码
第二条实测生效:name 等于 "ee" 的行被留下,其余进 Filter 缓冲区。注意字符串字面量用双引号——在 PowerScript 源码里写这个表达式时,外层字符串用单引号、内层用双引号,正好不冲突:lds.SetFilter('name = "ee"')。
常用的过滤表达式还有:
- 逻辑组合:'id > 2 AND name <> "bb"';
- 函数调用:'IsNull(id)'、'Len(name) > 3';
- 特殊值 '1=1':恒真,用于「取消过滤、取回全部行」(见第八节)。
七、Filter 缓冲区:被滤掉的行没有删,只是换了个地方住
Filter() 执行后,Primary(主)缓冲区只剩满足条件的行,不满足的行被移到 Filter 缓冲区,并没有被删除。这一点决定了两件事:
第一,行号变了。过滤后的 GetItemNumber / GetItemString 用的都是过滤后的新行号:实测四行 1/aa、5/ee、2/bb、7/gg 过滤 'id > 2' 后,Primary 只剩 2 行,第 1 行是 5/ee、第 2 行是 7/gg——原来第 2、4 行的数据。
第二,被滤掉的行还能读回来,用 GetItemString 的四参形态:
- lds.GetItemString(1, 'name', Filter!, false) // 读 Filter 缓冲区第 1 行的 name 当前值
复制代码
四参签名是(行号, 列, dwbuffer, 原始值标志),第四参传 false 读当前值、传 true 读检索时的原始值。实测四行数据滤掉两行后,Filter 缓冲区的第 1、2 行分别是 bb、aa——按它们原来的相对顺序存放。
还要注意:过滤态下的插入与删除都只作用于 Primary:
- InsertRow(0) 在过滤态实测插到 Primary 末尾(返回新行号 2,新行各列为 NULL);
- DeleteRow(1) 删的是 Primary 的第 1 行;
- Find('id = 1', 1, RowCount()) 按过滤后的新行号查找,找不到返回 0。
八、还原全部行的坑:'1=1' 能取回数据,但顺序不保证
过滤错了想反悔,标准做法是设一个恒真条件再 Filter:
- lds.SetFilter('1=1')
- lds.Filter()
复制代码
行数确实会回到过滤前的总数,实测四行数据还原后 RowCount()=4。但顺序不保证回到原始顺序。实测还原后的顺序是:
而原始顺序是 1 → 5 → 2 → 7。规则是:Primary 里剩下的行排在前面,Filter 缓冲区的行按它们的存放顺序追加在后面。如果还原之后你还要靠行号取特定行(比如「还原后第 1 行一定是原来第 1 行」),就会出逻辑错误。
对策有两条:要么还原后立刻按业务键再排一次序(把排序表达式存在自己的变量里,配合第五节「读不回来」的结论);要么数据量不大时干脆重新检索。
九、PB12.5 差异
本轮 20 组行为用例里,只有一处两版行为不同:
- 越界读 Filter 缓冲区:过滤后 Filter 缓冲区只有 2 行时,调用 GetItemString(3, 'name', Filter!, false) 读第 3 行——PB12.5 返回空串(静默);PB10 抛运行错误「Invalid DataWindow row/column specified」(独立运行验证时进程返回失败,读数为运行错误信息)。
其余 19 组用例(含排序语法、字典序、不稳定排序、还原顺序、InsertRow / DeleteRow / Find、非法表达式的返回值)两版读数逐字一致。为兼容两版,读 Filter 缓冲区前请先用可靠的行数信息约束行号,不要试探性越界读取。
十、完整示例:排序与过滤一条龙(PB10 / PB12.5 双版实测通过)
下面这段代码在 PB10 与 PB12.5 上分别编译 0 错误、运行结果一致,可直接放进应用对象或某个 NVO 的函数里执行。
- // —— 输入值(前段定义,逐行注释)——
- string ls_syn // 最小 DataWindow 语法串:id(数值) + name(字符) 两列
- string ls_data // 演示数据:四行
- datastore lds // 数据窗口容器(DataStore 不需要窗口)
- long ll_rc // 承接各步返回值
- string ls_err // 承接 Create 的语法错误信息
- string ls_out // 结果串
- // —— 构造与填数 ——
- ls_syn = of_syn() // 见下:返回最小语法串的函数
- ls_data = '1~taa~r~n5~tee~r~n2~tbb~r~n7~tgg'
- lds = create datastore
- ll_rc = lds.Create(ls_syn, ls_err)
- if ll_rc <> 1 then
- destroy lds
- MessageBox('错误', '语法串无效')
- return
- end if
- ll_rc = lds.ImportString(ls_data)
- if ll_rc < 1 then
- destroy lds
- MessageBox('错误', '数据导入失败')
- return
- end if
- // —— 排序:先 SetSort 定表达式,再 Sort 执行 ——
- ll_rc = lds.SetSort('id D') // 按 id 降序;务必检查返回值
- if ll_rc <> 1 then
- destroy lds
- MessageBox('错误', '排序表达式非法')
- return
- end if
- ll_rc = lds.Sort()
- // —— 过滤:先 SetFilter 定表达式,再 Filter 执行 ——
- ll_rc = lds.SetFilter('id > 2')
- ll_rc = lds.Filter()
- // —— 结果:主缓冲区只剩匹配行,被滤掉的行在 Filter 缓冲区 ——
- ls_out = 'rows=' + String(lds.RowCount())
- ls_out = ls_out + ' r1:' + String(lds.GetItemNumber(1, 'id')) + '/' + lds.GetItemString(1, 'name')
- ls_out = ls_out + ' r2:' + String(lds.GetItemNumber(2, 'id')) + '/' + lds.GetItemString(2, 'name')
- ls_out = ls_out + ' fb1:' + lds.GetItemString(1, 'name', Filter!, false)
- ls_out = ls_out + ' fb2:' + lds.GetItemString(2, 'name', Filter!, false)
- // —— 还原:'1=1' 取回全部行,但顺序不保证与原序一致 ——
- ll_rc = lds.SetFilter('1=1')
- ll_rc = lds.Filter()
- ls_out = ls_out + ' 还原后顺序:'
- ls_out = ls_out + String(lds.GetItemNumber(1, 'id')) + ','
- ls_out = ls_out + String(lds.GetItemNumber(2, 'id')) + ','
- ls_out = ls_out + String(lds.GetItemNumber(3, 'id')) + ','
- ls_out = ls_out + String(lds.GetItemNumber(4, 'id'))
- destroy lds
- MessageBox('排序与过滤结果', ls_out)
复制代码
of_syn 是返回最小语法串的函数,内容如下(自包含、不依赖任何 PBL):
- public function string of_syn ();
- string ls_syn
- ls_syn = 'release 10;~r~n'
- ls_syn = ls_syn + 'datawindow(units=0 processing=1)~r~n'
- ls_syn = ls_syn + 'header(height=68)~r~n'
- ls_syn = ls_syn + 'detail(height=80)~r~n'
- ls_syn = ls_syn + 'table(column=(type=long name=id dbname=~"t.id~") column=(type=char(10) name=name dbname=~"t.name~") retrieve=~"PBSELECT( VERSION(400) )~")~r~n'
- ls_syn = ls_syn + 'column(band=detail id=1 name=id x=~"14~" y=~"8~" height=~"64~" width=~"274~" tabsequence=10 border=~"0~" color=~"0~" alignment=~"1~")~r~n'
- ls_syn = ls_syn + 'column(band=detail id=2 name=name x=~"302~" y=~"8~" height=~"64~" width=~"251~" tabsequence=20 border=~"0~" color=~"0~" alignment=~"0~")~r~n'
- ls_syn = ls_syn + 'text(band=header name=id_t text=~"Id~" x=~"14~" y=~"8~" height=~"52~" width=~"274~" border=~"0~" alignment=~"2~")~r~n'
- ls_syn = ls_syn + 'text(band=header name=name_t text=~"Name~" x=~"302~" y=~"8~" height=~"52~" width=~"251~" border=~"0~" alignment=~"2~" )'
- return ls_syn
- end function
复制代码
本机实测输出:
- rows=2 r1:5/ee r2:7/gg fb1:bb fb2:aa 还原后顺序:5,7,2,1
复制代码
与第八节结论完全一致:主缓冲区 2 行匹配行,Filter 缓冲区 2 行被滤行,还原后顺序是 5,7,2,1 而不是原始的 1,5,2,7。
十一、坑清单(实测汇总)
- 排序过滤都是两步制:Set 只记表达式不动数据,执行靠 Sort / Filter;Set 失败(返回 -1)后执行是安全的无副作用空转,但必须自查 Set 返回值。
- 字符列排序是字典序:'10' 排在 '9' 前、大写排在小写前,没有自然数字排序;要按数值含义排就改列类型或补零。
- Sort 是不稳定排序:相同键的行顺序会被打乱,依赖二次排序的场景必须把次键写进表达式。
- 排序与过滤表达式都读不回来:GetSort 不存在(C0051)、Describe('datawindow.sort') 恒返回 '!';要还原请自己存表达式。
- SetSort('') 合法:清空排序,之后 Sort() 保持当前顺序。
- Filter 后行号重排:GetItemXxx / Find / DeleteRow 全部按过滤后的新行号;Find 找不到返回 0。
- 被滤的行在 Filter 缓冲区,按原相对顺序存放,用 GetItemString 四参形态(行, 列, Filter!, false)读取。
- 还原用 '1=1':行数回来,顺序不回来(Primary 剩余在前、Filter 缓冲区追加在后);要恢复原序就还原后再排序。
- 过滤态 InsertRow(0) 插到 Primary 末尾,新行各列为 NULL。
- 越界读 Filter 缓冲区两版行为不同:PB10 抛「Invalid DataWindow row/column」运行错误,PB12.5 返回空串;跨版本代码不要试探性越界。
- 未设置就执行是安全的:Sort / Filter 无表达式时返回 1、数据不动。
- ImportString 导入的行状态是 New:String(GetItemStatus(...)) 输出空串,判断行状态请用等式比较(= New!),不要用 String 转。
测试环境
- 硬件与系统:Windows Server(x64),本机双版对照。
- 软件:PB12.5(SNC 方式同机编译运行)、PB10(全量编译 + 运行时实跑)。
- 数据:全部用 ImportString 内存喂数,无数据库、无第三方运行库依赖。
静态检查与实机验证情况
- 源码 25 个 .sru(23 个行为探针 NVO + 1 个语法串辅助 NVO + 1 个主线示例 NVO nvo_dwso)全部 GBK / CRLF / 无 BOM / 带导出头,编码检查 bad=0;方法原型与实现逐对象对齐 0 错 0 提醒。
- PB10 与 PB12.5 双轨:导入 0 错、全量重编译 0 错;22 组独立运行用例 PB12.5 全 PASS,PB10 侧 21 PASS + 1 例按预期复现版本差异(越界读 Filter 缓冲区抛错,即第九节内容,该例本身就是证据用例)。
- 主线示例(第十节)双轨编译 0 错、运行读数逐字一致(rows=2 r1:5/ee r2:7/gg fb1:bb fb2:aa 还原后顺序:5,7,2,1)。
- 未实测项:无。文中全部结论均来自本轮实机运行读数。
|