029、内表操作工作区与表头行
029、内表操作工作区与表头行上周五快下班时业务丢过来一个报表说某些行的金额被凭空改成了上一条的值。我拉下代码一眼看到几十年前的老古董DATA: BEGIN OF gt_list OCCURS 0, ...。断点走进循环gt_list-f1的值在调用一个内部子程序后变了。其实不用看子程序内部我知道问题就出在带表头内表这口老井上。后来网上查了下这玩意儿坑过的年轻人可以绕地球三圈今天把它掰开揉碎说说。工作区是什么表头行又是什么内表是一张行集合你总要有个临时变量去接收某一行才能对它操作。这个临时变量就是工作区Work Area。比如DATA wa LIKE LINE OF gt_itab. wa-col1 AAA. APPEND wa TO gt_itab.这里的wa就是一个独立工作区。它和内表本身没关系你爱叫什么叫什么。可是老代码设计了一种“省事”的写法直接给内表绑定一个隐含的工作区叫做表头行Header Line。声明方式一般是DATA: BEGIN OF gt_spfli OCCURS 10, carrid TYPE spfli-carrid, connid TYPE spfli-connid, cityfrom TYPE spfli-cityfrom, cityto TYPE spfli-cityto, END OF gt_spfli.或者DATA gt_spfli LIKE TABLE OF spfli WITH HEADER LINE.这样gt_spfli这个符号就变得精神分裂了当它出现在表操作的位置它表示整个内表当它出现在字段访问的位置它表示那一行隐藏的工作区。比如gt_spfli-carrid取得就是表头行里的字段而不是内表某一行。一个最经典的坑CLEAR 到底清了谁带表头内表有个配套口诀itab是行itab[]是表。所以你写了CLEAR gt_spfli.你以为你把内表清空了不对你只清掉了表头行内表里几万条数据原封不动躺在那里。真正清空内表要写REFRESH gt_spfli. 或者 CLEAR gt_spfli[].反过来你想清一下表头行再塞新值结果写了CLEAR gt_spfli[]数据全没了表头行还悬在旧值上。这种错误当时真的能让人一夜白头发。LOOP AT 的另一层语义对于带表头内表裸写LOOP AT gt_spfli.时每读出一行就会把这一行塞进表头行。循环体里你直接写gt_spfli-cityfrom是可行的因为后面跟着的是表头行。看起来好方便于是很多人天天这么写。但紧接着问题就来了。你在循环里想修改当前行写了LOOP AT gt_spfli. gt_spfli-cityfrom BEIJING. MODIFY gt_spfli. ENDLOOP.MODIFY gt_spfli确实会用表头行更新内表当前行。但如果你在这个循环里又嵌套了一个LOOP AT gt_spfli那乐子就大了。内层循环每跑一次表头行就被覆盖成内层当前行。内层循环结束后你再用gt_spfli-cityfrom取的是外层当前行吗不是那是内层循环最后一行的值。如果你紧接着写MODIFY gt_spfli很可能把错误的数据写到了错误的位置或者更糟因为SY-TABIX也变了直接改乱。我上次调试的那个报表就是这么挂的。外层LOOP AT gt_list循环里调了一个内部子程序子程序里又有LOOP AT gt_list。外层每次往下走gt_list-f1都被子程序的最后一轮循环溅一脸。最恶心的是这个 bug 不是必现只有子程序里内层循环超过一行才发作。用独立工作区就天下太平了吗独立工作区没有表头行所以不会出现内表名字一人分饰两角的问题。比如DATA wa LIKE LINE OF gt_spfli. LOOP AT gt_spfli INTO wa. wa-cityfrom BEIJING. MODIFY gt_spfli FROM wa. ENDLOOP.这里每一行都先装进wa你随便改wa里的字段最后用MODIFY ... FROM wa写回。内层循环来了怎么办只要内层循环也用INTO wa2或者INTO DATA(...)它动的是自己局部的工作区跟外层wa没有半毛钱关系。至少不会被那种“共享表头”的暗坑搞到自相残杀。但独立工作区也不是万能的。你在LOOP AT gt_spfli INTO wa之后执行了一个函数函数内部如果CHANGING了这个内表并且函数内部也有一个wa那还是各用各的。因为wa是局部变量生命周期和变量绑定不是和内表绑定的。除非你犯了另一个低级错误没有CLEAR wa就重新READ TABLE导致旧值残留。更让人无语的 SELECT INTO带表头内表还容易跟 SQL 语句发生化学反应。同样是SELECT carrid connid cityfrom cityto FROM spfli INTO gt_spfli. ENDSELECT.这里INTO gt_spfli写成这样结果不是把整个结果集装进内表而是每查一行就往表头行里塞一行内表本身一条也没有你想要的是SELECT carrid connid cityfrom cityto FROM spfli INTO TABLE gt_spfli.加上TABLE才会真正填充内表。所以带表头内表在SELECT语句里藏了个雷漏写TABLE不会语法报错编译也通过跑起来内表空空如也只留下表头行上最后一个查出来的值。这种雷在旧代码里特别多我都养成习惯了看到INTO后面没跟TABLE就先眉头一皱看看目标变量是不是带表头内表。如果是十有八九作者本意是想INTO TABLE。READ TABLE 不带 INTO 也有讲究老代码里还常写READ TABLE gt_spfli INDEX 1. IF sy-subrc 0. WRITE gt_spfli-carrid. ENDIF.因为带表头内表的READ TABLE可以不写INTO查到的行直接落进表头行后面就能通过内表名访问字段。听着挺爽但如果你在READ TABLE之后又调了别的代码那个代码也用了带表头内表或者做了类似操作你的表头行就可能被冲掉。再晚几步访问字段值已经变成别人的了。独立工作区的正确写法是READ TABLE gt_spfli INTO wa INDEX 1. IF sy-subrc 0. WRITE wa-carrid. ENDIF.或者干脆用新语法DATA(ls_spfli) gt_spfli[ 1 ].注意这里gt_spfli后面没加[]但因为是老内表带表头编译器知道取的是内表行不是表头行实际上新语法gt_spfli[ 1 ]自动指内表行不受表头行干扰这也是新语法干净的地方。但如果gt_spfli是带表头内表ls_spfli gt_spfli这种写法反而会把表头行复制给工作区又是一个烟雾弹。为什么新代码最好别碰表头行我的原则就一句话新代码一律禁用带表头内表。理由不是会写错而是代码读起来太累。你让新人看MOVE FLIGHT TO gt_spfli-carrid. APPEND gt_spfli.他会问gt_spfli到底是表还是变量你说既是表又是变量他只会更懵。等到他再维护这种代码出个 bug 需要蹲半小时才能明白gt_spfli-carrid可能指表头行也可能在某种语境下根本是无效数据。更关键的是现在 ABAP 有更优雅的写法DATA lt_spfli TYPE STANDARD TABLE OF spfli WITH EMPTY KEY. DATA(ls_row) lt_spfli[ 1 ]. LOOP AT lt_spfli INTO DATA(ls_data). ls_data-cityfrom BEIJING. MODIFY lt_spfli FROM ls_data. ENDLOOP. LOOP AT lt_spfli ASSIGNING FIELD-SYMBOL(fs_row). fs_row-cityfrom BEIJING. ENDLOOP. READ TABLE lt_spfli INTO DATA(ls_read) WITH KEY carrid LH.每一行都有清晰的归宿内表是内表工作区是工作区谁也不占谁的便宜。调试时变量名一目了然ls_data就是当前行副本fs_row就是内表行的引用不用再猜。如果你不得不接手老代码生产环境里总有那些OCCURS 0的老内表它们还活着甚至还在关键流程上跑着。你没法一下子全改掉但可以注意几个点第一分清CLEAR itab和CLEAR itab[]。清工作区用前者清内表用后者或REFRESH。你可以在调试器里输入itab看表头行输入itab[]看表体养成这个习惯。第二见到裸的LOOP AT itab或READ TABLE itab先问一句这里有没有可能覆盖表头行如果循环体里还调用了会操作同一内表/同一带表头内表的逻辑果断改成INTO wa或者ASSIGNING。第三MODIFY itab不带FROM的时候表头行就是你要写回的数据来源。这种写法一旦和SY-TABIX配合在嵌套循环里很容易出事。宁可多写一行MODIFY itab FROM wa也不要贪图那几行“简洁”。第四如果代码里出现SELECT ... INTO itab没有TABLE先不要急着改成INTO TABLE。因为原作者可能有自己的套路比如故意循环APPEND。你要看懂他到底想干嘛。不过一般这种代码离重写不远了。最后说句掏心窝的表头行是 ABAP 历史上为了省代码留下的鸡肋它表面上让你少声明一个工作区实际上把内表这个变量的语义搞得一团糟。你省下的那几行代码未来总会以十倍的加班时间还回来。新工程里看到WITH HEADER LINE直接划走老工程里看到它也尽量在小范围内慢慢拆掉。调通一个 bug 靠的是运气把这类隐患拔掉靠的是手艺。