马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PowerBuilder DataWindow 表达式引擎全解:Evaluate 取值、函数谱、聚合与 NULL 规则(PB10 基准 · PB12.5 实测)
阅读说明
1. 适用版本:基准版本 PB10;兼容并实测 PB 12.5。本文不涉及 PBIDEA 对象,无需安装 PbIdea.dll。
2. 支持数据库:本文示例不依赖任何数据库(用内存 DataStore 承载数据,结论与具体 DBMS 无关)。若你要把表达式接到真实数据窗口上,任何 PB 支持的 DBMS 都适用。
3. 操作系统与环境要求:Windows 7 及以上;PB 10 / 12.5 任一完整运行库即可,无需打印机、无需网络。
4. 难度系数:★★★☆☆(三颗星。读得懂 PowerScript 就能跟做,但表达式的函数谱与聚合语义需要按实测表对照记忆)。
5. 其它阅读说明:建议先读《PowerBuilder 动态 DataWindow 全解》(手写语法串建内存表)与《DataWindow 排序与过滤全解》(SetSort / SetFilter 的行为)。本文只讲"表达式语言本身"与 Describe 的 Evaluate 取值通道,数据窗口的列定义、缓冲区不重复展开。
一、先把结论摆在前面
DataWindow 里有一门独立的小语言,PB 文档叫表达式(expression)。它和 PowerScript 长得像,但不是同一套东西:函数名、参数个数、返回类型、甚至"有没有这个函数"都可能不一样。这门语言在六个地方共用同一套语法:计算域表达式、过滤表达式、排序表达式、校验规则、属性修饰表达式,以及本文主角 Describe("Evaluate(...)")。
把表达式当"能省一段循环的小工具"是常见的误判。它是唯一一条能在不写 PowerScript 循环的前提下,按行计算、按组聚合、按条件分支的通道——代价是它的失败几乎全是静默的。
实测结论先摊开:
| 序号 | 结论 | 实测读数 | | 1 | 取值通道只有一个:Describe("Evaluate('表达式', 行号)"),行号参数不可省 | 省掉行号 → 返回 ! | | 2 | 列可以用名字,也可以用列序号 #n,大小写不敏感,首尾空格容忍 | AMT * 2 / Amt * 2 / amt * 2 全为 20 | | 3 | 行号越界不报错,会取到别处的值 | 3 行数据下:行 0 得 20、行 9 得 60 | | 4 | 列序号越界则返回 ! | #0、#9 均为 ! | | 5 | 表达式里的字符串常量必须用双引号;PB 源码里要写成 ~" | 双写单引号那套(''abc'')全部失败 | | 6 | 聚合函数在 Evaluate 上下文里没有"累计"含义 | sum(amt) 与 sum(amt for all) 在第 2 行都是 60;累计要用 cumulativeSum | | 7 | 聚合函数不能用在过滤表达式里 | SetFilter 不报错、Filter() 返回 1,但结果为 0 行 | | 8 | NULL 会沿算术传染,但聚合函数会忽略它 | 三行中一行 amt 为 NULL:sum=50、count=2、avg=25 | | 9 | 比较区分大小写,不是 SQL Server 默认的不区分 | dept = "R1" 为 true,dept = "r1" 为 false | | 10 | 数值一律按 double 走 | 10 / 3 = 3.333333333333334 |
二、表达式引擎是什么:一门寄生在 DataWindow 上的语言
2.1 它出现的位置
| 位置 | 设置方式 | 表达式的作用 | | 计算域(computed field) | painter 里拖一个计算域;或 Modify("create compute(...)") | 每行算一个值 | | 过滤 | SetFilter(表达式) + Filter() | 决定哪一行进 Filter 缓冲区 | | 排序 | SetSort(表达式 A/D) + Sort() | 决定行序 | | 校验规则 | 列的 Validation Expression | 提交时校验值 | | 属性修饰 | 属性值写成表达式(颜色、可见性等) | 按行改外观 | | 取值通道 | Describe("Evaluate(表达式, 行号)") | 算完立刻拿回来,不落任何对象 |
前三项在本系列已有专文(排序过滤篇、分组统计篇)。本文把重心放在第五项——因为它是最省事、也最容易踩坑的一条:不用建计算域、不用动缓冲区,只在需要的那一刻算一个值回来。
2.2 Evaluate 的语法
- Describe ( "Evaluate ( 'expression', startrow )" )
复制代码
三层结构要数清楚,否则引号一定写错:
- 最外层:PowerScript 的字符串(Describe 的参数)。
- 中间层:Evaluate 的第一个参数,用单引号包裹的表达式字符串。
- 最内层:表达式内部的字符串常量,用双引号。在 PowerScript 双引号字符串里写双引号要转义成 ~"。
2.3 本文的测试台
下面的示例都用同一个自建内存表跑,不连数据库——这样任何人拿到代码就能复现,也把"表达式引擎"和"数据库"彻底解耦。
前置:新建一个非可视对象 nvo_expr(或直接用窗口上的按钮脚本)。示例数据是三个部门的三行金额:R1=10、R1=20、R2=30。
步骤:1) 在对象里贴入 of_new_ds();2) 调用后得到一个 datastore;3) 用 ImportString 灌三行数据;4) 依次执行后续示例。 - // 目的:手写语法串建一个两列内存表(dept 部门 / amt 金额),不依赖数据库
- datastore lds
- string ls_syn
- long ll_rc
- lds = create datastore
- // 语法串:release 头 + datawindow 全局段 + table 段(列定义)
- ls_syn = 'release 10; datawindow(units=0 processing=1) table('
- ls_syn = ls_syn + 'column=(type=char(10) updatewhereclause=yes name=dept dbname="dept") '
- ls_syn = ls_syn + 'column=(type=number updatewhereclause=yes name=amt dbname="amt")'
- ls_syn = ls_syn + ')'
- ll_rc = lds.Create(ls_syn)
- // 灌三行数据:制表符分隔、CRLF 换行
- lds.ImportString('R1' + Char(9) + '10' + Char(13) + Char(10) &
- + 'R1' + Char(9) + '20' + Char(13) + Char(10) &
- + 'R2' + Char(9) + '30' + Char(13) + Char(10))
- // 取值:第 1 行 amt 乘以 2
- MessageBox('建表结果', 'Create=' + String(ll_rc) + ' 行数=' + String(lds.RowCount()) &
- + ' 第1行amt*2=' + lds.Describe("Evaluate('amt * 2', 1)"))
- destroy lds
复制代码
这段跑出来是:Create=1 行数=3 第1行amt*2=20。
三、取值通道实测:列怎么引、行怎么给
3.1 列引用三种形态
| 写法 | 第 1 行结果 | 说明 | | amt * 2 | 20 | 按列名,最常用 | | #2 * 2 | 20 | 按列序号,从 1 起(amt 是第 2 列) | | AMT * 2 | 20 | 大小写不敏感 | | Amt * 2 | 20 | 同上 | | amt * 2 | 20 | 首尾与中间多余空格都被容忍 | | #0 + 1、#9 + 1 | ! | 列序号越界返回 ! |
先记住两条相反的规则:列序号越界会返回 !(有信号),行号越界不会(无信号,见 3.2)。二者不对称,是这套 API 最坑的地方之一。
3.2 行号越界:静默取到别处的值
三行数据,表达式 amt * 2:
| 行号 | 结果 | 实际取到 | | 1 | 20 | 第 1 行(amt=10) | | 2 | 40 | 第 2 行(amt=20) | | 3 | 60 | 第 3 行(amt=30) | | 0 | 20 | 回落到第 1 行 | | 9 | 60 | 回落到最后一行 |
这意味着:循环里算错行号不会报错,只会给你一个看起来很正常的值。所以按行取值的循环,行号务必来自 for ll_row = 1 to lds.RowCount(),不要用外部传入的、未经校验的行号。
另外,Evaluate 的行号参数不能省:
| 写法 | 结果 | | Evaluate('sum(amt for all)') | ! | | Evaluate('sum(amt for all)', 1) | 60 |
3.3 返回值与三种失败形态
Describe 失败时返回的东西不止一种,判定必须把三种都算上:
| 返回值 | 含义 | 例子 | | 空字符串 | 表达式合法但结果是 NULL | 对 NULL 值做 amt * 2 | | ? | 属性名/属性路径无效 | Describe('amt.Color')(手写语法串建出的 DW) | | ! | 表达式本身不成立(语法错、函数不存在、列名不存在) | fooBar(amt)、amt +* 2、nocol * 2 |
所以「表达式对不对」的判据不能是"结果是不是空",而必须是"结果里有没有 !"。写校验代码时,最稳的写法是:先 IsNumber(结果) 或用 Pos(结果, '!') = 0 判一次,再转数值。
四、字符串常量:只有一个正确写法
这是本轮最实用的发现。表达式里要用字符串,写错方式有一堆,写对的方式只有一种。
| 写法(PB 源码层面) | 展开后的表达式 | 结果 | | Evaluate('dept + ''-X''', 2) | dept + ''-X'' | !(双写单引号不是 PB 的转义) | | Evaluate('dept + ~"-X~"', 2) | dept + "-X" | R1-X ✅ | | Evaluate(~"dept + '-X'~", 2) | 整个表达式用双引号包 | R1-X ✅ | | Evaluate(~"'abc'~", 1) | 'abc' | abc ✅ | | Evaluate('number(~"12.5~")', 1) | number("12.5") | 12.5 ✅ | | Evaluate('if(amt > 15, ~"big~", ~"small~" )', 2) | if(amt > 15, "big", "small") | big ✅ | | Evaluate('left(dept, 1) + ~"!~"', 1) | left(dept, 1) + "!" | R! ✅ |
三条落地经验:
- 表达式内的字符串常量用双引号,PB 里写成 ~"。
- 如果你嫌 ~" 太乱,也可以把整个表达式用双引号包起来(第四种写法),此时表达式内部的字符串就用单引号——但这样你就不能再用双引号做 PowerScript 字符串定界符了,二选一。
- 别写 '' 来转义单引号——那是 SQL 的规矩,不是 PB 的,也不会被 DataWindow 表达式接受。
五、函数谱实测:有哪些、没有哪些
下面全部是逐条实测(每条单独一次 Describe,返回 ! 即判为不可用)。请当成对照表用,别凭 PowerScript 的经验猜。
5.1 数学
| 表达式 | 结果 | 可用 | | abs(-3) | 3 | ✅ | | ceiling(1.2) | 2 | ✅ | | int(1.9) | 1 | ✅ | | round(1.235, 2) | 1.24 | ✅ | | mod(7, 3) | 1 | ✅ | | sign(-2) | -1 | ✅ | | sqrt(16) | 4 | ✅ | | exp(0) | 1 | ✅ | | log(1) | 0 | ✅ | | logTen(100) | 2 | ✅ | | fact(5) | 120 | ✅ | | truncate(1.987, 2) | 1.98 | ✅ | | rand(10) | 3(随机) | ✅ | | max(1, 5) / min(1, 5) | ! | ❌ 两参数取大小不可用;max/min 只作聚合函数用 | | pi() | ! | ❌ | | real(3) | ! | ❌ | | floor(1.9) | ! | ❌ |
max(1, 5) 失败这条特别值得注意:PowerScript 里 Max(a, b) 是取两数较大者,表达式语言里同名函数却被当成聚合函数解析,直接不成立。要取两值较大,写 if(a > b, a, b)。
5.2 字符串
| 表达式(dept = “R1”) | 结果 | | len(dept) | 2 | | left(dept, 1) | R | | right(dept, 1) | 1 | | mid(dept, 1, 1) | R | | pos(dept, "1") | 2 | | upper(dept) | R1 | | lower(dept) | r1 | | trim(dept) | R1 | | leftTrim(" x") | x | | rightTrim("x ") | x | | fill("ab", 3) | aba | | match(dept, "R?") | true(返回布尔,不是 1/0) | | asc("A") | 65 | | char(65) | A | | lookUpDisplay(dept) | R1 | | replace(dept, "R", "X") | ! ❌ 表达式语言里没有这个名字 |
注意 match() 返回的是布尔,Describe 出来是 true / false 字符串。若拿去和 1 比较,会得到 false——这是把表达式结果接回 PowerScript 时的高频错误。
5.3 日期时间
| 表达式 | 实测结果 | | today() | 2026-11-13 05:24:45:174 | | year(today()) | 2026 | | month(today()) | 11 | | day(today()) | 13 | | dayName(today()) | Friday | | dayNumber(today()) | 6 | | relativeDate(today(), 7) | 2026-11-20 05:24:45:174 | | daysAfter(today(), relativeDate(today(), 7)) | 7 | | now() | 05:24:45:174 | | hour(now()) | 5 |
坑:today() 在表达式里返回的是带时分秒的 datetime,不是纯日期。所以直接和字符串 '2026-11-13' 比较会不等;要按天比较,请用 date(today()) 或直接比 year/month/day。
5.4 聚合与行状态
| 表达式 | 第 1 行 | 第 2 行 | 能否用 | | sum(amt) | 60 | 60 | ✅ | | sum(amt for all) | 60 | 60 | ✅ | | avg(amt for all) | 20 | 20 | ✅ | | count(amt for all) | 3 | 3 | ✅ | | max(amt for all) / min(amt for all) | 30 / 10 | 30 / 10 | ✅ | | first(amt for all) | 10 | 10 | ✅ | | cumulativeSum(amt) | 10 | 30 | ✅ | | percent(amt for all) | 0.1666… | — | ✅ | | cumulativePercent(amt for all) | — | 0.5 | ✅ | | getRow() | 1 | 2 | ✅ | | rowCount() | 3 | 3 | ✅ | | pageCount() | 1 | 1 | ✅ | | isRowNew() | true | true | ✅ | | isSelected(1) | false | false | ✅ | | sum(amt for group 1) | ! | — | ❌ 本环境无分组,直接不成立 | | sum(amt for page) | 空 | — | ⚠️ 不成立但返回空串,不是 ! | | pageSize() | ! | — | ❌ | | large(amt for all, 2) / small(amt for all, 2) | ! | — | ❌ |
case() 在本轮第一次探针里用了错误的引号写法,未取得有效读数;本文不给结论——用嵌套 if() 是等效且已验证的写法。
六、聚合语义:没有分组的 Evaluate 里,sum 就等于"全部"
这是最容易误判的一处。很多资料会说"计算域里 sum(amt) 是累计到当前行"。在 Evaluate 上下文里不成立:
| 表达式 | 行 1 | 行 2 | 行 3 | | sum(amt) | 60 | 60 | 60 | | sum(amt for all) | 60 | 60 | 60 | | cumulativeSum(amt) | 10 | 30 | 60 |
原因是 for 子句缺省时,表达式引擎按当前可用的分组上下文解释;Evaluate 拿不到分组上下文,于是退化成"全表"。所以:
- 想要"全部",写 for all,别省——省了今天对、明天放到分组窗口里就变累计。
- 想要"累计到当前行",用 cumulativeSum(),或用 sum(amt for all) 自己算区间。
- 想要"占比",percent(amt for all);想要"累计占比",cumulativePercent(amt for all),实测第 2 行 = 0.5(前两行 30 / 全部 60)。
分组窗口里 sum(amt) 的真实行为(累计)请看本系列《DataWindow 分组与汇总统计全解》,那篇是按分组环境实测的;本文只覆盖无分组上下文。
七、NULL 规则:传染与忽略并存
测试数据把第 1 行的 amt 留空(ImportString 遇到空字段即产生 NULL),三行金额为 NULL / 20 / 30。
| 表达式 | 结果 | 说明 | | isNull(amt) | true | 判定可用 | | amt * 2 | 空字符串 | NULL 沿算术传染,Describe 返回空串(不是 0、不是 !) | | string(amt) | 空字符串 | 同上 | | sum(amt for all) | 50 | 聚合忽略 NULL(20+30) | | count(amt for all) | 2 | 只数非 NULL | | avg(amt for all) | 25 | 50/2,分母也是非 NULL 数 | | if(isNull(amt), 0, amt) | 0 | 兜底写法,等价于 COALESCE |
这三条要连着记:算术传染、聚合忽略、计数只数非空。它们和 SQL 的行为一致,但和"PowerScript 里 IsNull 判定后继续算"的直觉不一致——PowerScript 里 NULL 参与运算会触发 NULL 传播但你能用 IsNull() 拦住,表达式里你只能靠 if(isNull(...), ...)。
顺便说清一点:SetNull() 在 DataStore 上不可用(两参数重载报 C0051: Unknown function name,三参数带 Number! 报 C0060: Illegal enumerated constant)。要在测试里造 NULL,走"导入空字段"这条路最干净。
八、比较、隐式转换与注释
| 表达式 | 结果 | 说明 | | dept = "R1" | true | 正常相等 | | dept = "r1" | false | 区分大小写,与 SQL Server 默认排序规则不同 | | dept > "R1"(第 3 行 R2) | true | 字符串按字典序比较 | | dept + amt | R110 | 字符串 + 数字:数字被隐式转成字符串拼接 | | string(amt) + dept | 10R1 | 同上,方向相反也可 | | amt /* note */ * 2 | 20 | 表达式里支持 C 风格块注释 | | not (amt > 5) | false | 逻辑非可用 | | amt > 5 and qty < 2 | true | and / or 可用 |
区分大小写这条在跨库移植时最容易出事:SQL 里 where code = 'abc' 可能匹配 'ABC',搬到表达式里做同样的判断就不匹配了。要在表达式里做不区分大小写的比较,先 upper() 两侧。
九、数值与精度
| 表达式 | 结果 | | 10 / 3 | 3.333333333333334 | | round(10 / 3, 4) | 3.3333 | | 1.005 * 100 | 100.5 | | 1234567 * 1000 | 1234567000 | | -amt | -10 | | string(1 / 3) | 0.3333333333333333 |
表达式引擎内部一律按 double 算(17 位有效数字的输出就是证据),没有 decimal 精度。所以:
- 用它做展示性计算(占比、累计、阈值判断)没问题。
- 用它做金额的加减乘除要小心:1.005 * 100 得到 100.5 看起来对,但更多位的小数会出现二进制误差。金额的精确计算请回到 PowerScript 用 decimal 做,或者干脆交给数据库。
- 输出到界面前统一 round(表达式, 2),别把 16 位小数直接显示给用户。
十、把表达式用到三处:Filter、Sort、计算域
10.1 过滤:能用条件,不能用聚合
前置:沿用第二节的 DataStore(三行:10 / 20 / 30)。
步骤:依次执行下面三段,观察 Filter() 的返回值和 RowCount()。 - // 目的:验证三种过滤表达式 —— 条件、字符串、聚合
- long ll_rc
- // 1) 用 if() 做条件过滤:金额 >= 20 的行
- lds.SetFilter("if(amt >= 20, 1, 0) = 1")
- ll_rc = lds.Filter()
- MessageBox('if 过滤', 'rc=' + String(ll_rc) + ' 行数=' + String(lds.RowCount()))
- // 2) 字符串函数参与过滤(注意双引号写法)
- lds.SetFilter('')
- lds.Filter()
- lds.SetFilter("amt >= 20 and left(dept, 1) = ~"R~"")
- ll_rc = lds.Filter()
- MessageBox('字符串过滤', 'rc=' + String(ll_rc) + ' 行数=' + String(lds.RowCount()))
- // 3) 聚合进过滤:不报错,但结果是 0 行 —— 不要这么写
- lds.SetFilter('')
- lds.Filter()
- lds.SetFilter('amt > avg(amt for all)')
- ll_rc = lds.Filter()
- MessageBox('聚合过滤', 'rc=' + String(ll_rc) + ' 行数=' + String(lds.RowCount()))
复制代码
实测读数:if 过滤 rc=1 行数=2、字符串过滤 rc=1 行数=2、聚合过滤 rc=1 行数=0。
第三段是本篇最该记的一条:聚合函数写进 SetFilter 不会报错,Filter() 照样返回 1,但留下一张 0 行的表。而按数据本该有 1 行(30 > 20)。这种"静默给错结果"比报错危险得多——上线后表现为"有时候列表空了"。
要用聚合做阈值判断,先在 PowerScript 里算出聚合值,再拼进过滤表达式:
- // 目的:先用表达式拿到全表平均值,再拼成普通比较式过滤
- string ls_avg, ls_filter
- long ll_rc
- ls_avg = lds.Describe("Evaluate('avg(amt for all)', 1)")
- ls_filter = 'amt > ' + ls_avg
- lds.SetFilter(ls_filter)
- ll_rc = lds.Filter()
- MessageBox('先算后滤', '条件=' + ls_filter + ' rc=' + String(ll_rc) &
- + ' 行数=' + String(lds.RowCount()))
复制代码
10.2 排序:表达式可用,未知列静默忽略
| SetSort 参数 | Sort() | 排序结果(原序 10 / 20 / 30) | | if(amt >= 20, 1, 0) A | 1 | 第 1 行 10、第 3 行 20(分组内顺序由引擎决定) | | left(dept, 1) A amt D | 1 | 第 1 行 30、第 3 行 10(同前缀内金额降序) | | nocol A | 1 | 顺序未变,无任何报错 |
nocol 那一行是第二个静默陷阱:列名写错时 Sort() 返回 1(成功),数据纹丝不动。所以排序后必须自己校验第一行/最后一行的值,别信返回值。
10.3 计算域:可以运行时创建,但属性必须给全
这是本轮推翻我之前假设的一条。Modify("create compute(...)") 是可以成功的,失败与写法有关:
| 写法 | 结果 | | create compute(c1 expression='amt * 2' band=detail) | Line 1 Column 18: incorrect syntax | | create compute(band=detail name=ca expression='amt * 2') | 同样的语法错 | | 带 x / y / height / width | 成功,Describe('ca.expression') 读回 amt + 1 | | 带全套(alignment / border / color / format + 位置尺寸) | 成功 |
也就是说:band、name、expression、x、y、height、width 是硬性必需,缺位置尺寸就报 incorrect syntax(报错列号会指向表达式参数附近,误导性很强)。
计算域建好后,值可以直接读:
- // 目的:运行时给 DataStore 加一个计算域 c_pct,读回第 1 行的百分比
- string ls_mod, ls_rt
- decimal ld_v
- ls_mod = 'create compute(band=detail name=c_pct '
- ls_mod = ls_mod + 'expression=~"amt / sum(amt for all) * 100~" '
- ls_mod = ls_mod + 'x=~"10~" y=~"10~" height=~"20~" width=~"80~"'
- ls_mod = ls_mod + ')'
- ls_rt = lds.Modify(ls_mod)
- ld_v = lds.GetItemNumber(1, 'c_pct')
- MessageBox('动态计算域', 'Modify 返回=[' + ls_rt + '] 第1行占比=' + String(ld_v, '0.00') + '%')
复制代码
实测:Modify 返回= 后为「」(空串即成功)、第1行占比=16.67%。
10.4 反向坑:手写语法串建出的 DW,属性面很窄
沿着 10.3 继续试就会撞墙。同一个 DataStore 上:
| 操作 | 结果 | | Modify("amt.Protect='1'") | Line 1 Column 13: incorrect syntax | | Modify("amt.Font.Weight='700'") | Line 1 Column 17: incorrect syntax | | Modify("amt.Color='255'") / 写成表达式 / 去掉空格 | 全部 incorrect syntax | | Modify("datawindow.grid.columns=1") | Line 1 Column 24: incorrect syntax | | Describe('amt.Color') | ? | | Describe('amt.Font.Weight') | ! |
结论:最小语法串建出的 DataWindow 只保留了"算"的能力,没有完整的属性槽。计算域的 create 能成,普通列属性赋值一律失败。所以:
- 要玩条件颜色、按行改字体这些,请在 painter 里做完整的 DataWindow 对象,别用最少语法串。
- 反过来说,如果你只需要"算",最小语法串恰恰是最干净的选择——它连数据库都不用连。
十一、性能:便宜到可以按行调用
5000 次循环取值的实测耗时(Cpu() 计时,粒度 16ms):
| 场景 | 5000 次耗时 | | Evaluate('amt * 2', 1) | 47 ms | | Evaluate('sum(amt for all)', 1) | 47 ms |
即单次约 0.01 ms。三点推论:
- 按行调用 Evaluate 完全可接受,几百行的循环不会成为瓶颈。
- 聚合与单值同价(3 行数据下),说明开销主要在字符串解析,不在计算本身。
- 真正的成本在于每行都重新解析一次表达式字符串。如果同一表达式要对几千行反复用,改成计算域(只建一次、由引擎批量算)更划算。
十二、十二条坑清单
| 编号 | 坑 | 症状 | 对策 | | 1 | 省掉行号参数 | 返回 ! | 永远给行号 | | 2 | 行号越界 | 静默取到第 1 行/最后一行 | 行号来自 RowCount 循环 | | 3 | 展开后字符串用单引号 | 返回 ! | 表达式内字符串用 ~" | | 4 | 用 '' 转义单引号 | 返回 ! | 那是 SQL 规矩,改双引号 | | 5 | sum() 不带 for | 以为是累计,其实是全部 | 明确写 for all | | 6 | 聚合进过滤表达式 | 不报错、0 行 | 先取值再拼条件 | | 7 | 排序用错列名 | 返回 1、顺序不变 | 排完校验首末行 | | 8 | NULL 参与算术 | 空串(不是 0) | if(isNull(x), 0, x) | | 9 | 把 match() 的 true 当 1 | 比较恒 false | 布尔单独判 | | 10 | 拿 today() 当纯日期 | 与日期串永不相等 | 用 date(today()) | | 11 | create compute 缺位置尺寸 | incorrect syntax | 补齐 x/y/height/width | | 12 | 用手写语法串设列属性 | 全部 incorrect syntax | 属性操作改用完整 DW 对象 |
十三、PB12.5 差异
本文所有探针均为双轨运行(PB10 与 PB12.5 各一遍),逐条读数完全一致,未发现任何版本差异:函数可用性、返回值格式、越界行为、NULL 规则、错误形态全部相同。
唯一能观察到的版本差异是 Describe('DataWindow.Syntax') 的返回长度:本期在同一个「含 1 个计算域」的语法串上实测为 PB10 885 字符 / PB12.5 1347 字符(裸最小串下的 657 / 1119 见动态 DataWindow 篇,两处差值都来自默认属性集不同)。这个差异不影响表达式求值——本文所有表达式读数在两版下逐字相同。
十四、实机验证情况
| 项目 | PB10 | PB12.5 | | 编译 | --pb-version 100 全量 rebuild,0 错误;30 个对象里 29 个成功入 PBL | --pb-version 125 全量 rebuild,0 错误;同样 29 个入 PBL | | 运行 | PBVM .pbtest 全部 PASS,读数逐条与本文表格一致 | 同左,读数逐字相同 | | 覆盖 | 29 个探针对象(e01–e23 系列,含 1 个故意写错的对象 e11b,用于证明 SetNull 不可用)+ 1 个示例对象 art96(4 个方法) | 同左,读数逐条一致 |
说明两点,避免夸大:
- 文中所有读数值来自 PBVM 实跑的输出文件,不是推演。表里标 ❌ 的函数,是在实跑里返回 ! 的,不是"文档里没写"。
- 文章示例代码末尾的 MessageBox 是给读者复制运行的输出口(PBVM 无界面,测试时改写成写文件)。除 MessageBox 一行外,其余逻辑与实跑对象逐字一致。
- case() 未取得有效读数(首次探针引号写法错误),本文未给结论;sum(amt for group 1) 与 for page 的失败是在无分组上下文下得到的,分组窗口里的行为见分组统计篇,本文不外推。
十五、和本系列其它文章的分工
| 文章 | 覆盖什么 | 本文不重复的部分 | | DataWindow 排序与过滤全解 | SetSort/SetFilter 的行为、Filter 缓冲区、还原顺序 | 表达式语法细节、函数谱 | | DataWindow 分组与汇总统计全解 | group 定义、计算域 Sum/Count/Avg、GroupCalc | 分组上下文里的累计语义 | | 动态 DataWindow 全解 | 手写语法串的完整结构、16 条建表坑 | 语法串只借来搭测试台 | | 本篇 | 表达式语言本体 + Evaluate 取值通道 + 函数谱 + 聚合/NULL/比较语义 | — |
一句话记住它:表达式是 DataWindow 的"内置计算器",好用在于能省循环,危险在于它几乎从不报错。 |