S/4HANA ABAP新语法清单:从READ TABLE到内联声明实战
简介:面向SAP ABAP开发者的新语法速查与进阶资料,聚焦S/4HANA,明确不适用于ECC等老系统,适合已有基础ABAP能力、希望迁移旧式写法并提升代码可读性的读者。压缩包仅含1个docx文档,约1.8MB,以分类目录和示例代码组织知识点,便于按主题检索。内容覆盖DATA、FIELD-SYMBOL、NEW、REF、VALUE、BASE、FOR、LET、CONV、SWITCH、COND、CORRESPONDING与深拷贝、REDUCE、GROUP BY、FILTER等新关键字,并延伸到Open SQL常量、Host Variable/Expression、聚合表达式以及数字、字符串、日期时间、时间戳和时区内置函数。已有1168人学习,可作为日常开发查表、团队内训和语法迁移的参考。
1. S/4 项目里那堆 READ TABLE:这份按编号排的 ABAP 新语法清单能省多少事
去年接手一个 S/4HANA 的库存报表改造,两千多行程序里,一半是READ TABLE ... WITH KEY,另一半是带表头的内表配MOVE-CORRESPONDING。我想换成 ABAP 新语法重写,结果语法检查直接大面积报错——系统是 S/4,可我对哪些写法在哪版可用、哪些写法只是看起来更短却没好处,心里没底。这份 ABAP Syntax 2020.08.012 的手册正好补上这块:它按 New Keyword、Open SQL、Data Processing、特殊用法四块拆开,每个语法点都挂了一个ZDEMO_01_00x的可运行示例,从DATA(...)内联声明一直讲到TRY…CATCH…CLEANUP和定点算术。适合正在 S/4 上做老程序改造的 ABAP 开发,也适合刚接触 ABAP 7.40 之后写法的同学照着敲一遍。
2. 资源结构:四块内容和 ZDEMO_01_001 这套编号怎么对应
2.1 目录映射:从 DATA 到 TRY…CATCH 的四块拼图
手册的目录本身就是一张学习路径图,四大块依次是 New Keyword in ABAP(17 个新关键字)、Open SQL(从常量、主机变量一直到 Strict Mode)、Data Processing(字符串模板、格式化选项、字符串函数、数值函数、内表表达式)、Special Usages(异常处理、方法调用、Demo 输入输出、定点算术)。示例程序的命名规律是ZDEMO_章节号_序号,前缀 01 对应第一大块,序号从 001 递增,正文里每个语法点后面都标了对应的 demo 名,比如ZDEMO_01_001讲动态声明,ZDEMO_01_006讲BASE。
这套编号的好处是可以按块刷。我一般先把第一大块的 demo 全部跑一遍,因为它们之间是递进关系:DATA(...)声明出来的变量要喂给VALUE,VALUE又常和BASE、CORRESPONDING配合,NEW/REF则负责引用对象。断点从中间插进去很容易看不懂上下文。
| 编号段 | 主题 | 代表语句 |
|---|---|---|
| ZDEMO_01_001 ~ 005 | 声明与构造 | DATA(...)、FIELD-SYMBOL(...)、NEW、REF、VALUE |
| ZDEMO_01_006 ~ 018 | 表达式与集合 | BASE、FOR、LET、CONV、COND、REDUCE、FILTER、CORRESPONDING |
| 第二章示例 | 数据库访问 | 主机变量、聚合、内置函数、CAST、JOIN、OFFSET |
| 第三章示例 | 数据处理 | 字符串模板、格式化选项、字符串函数、内表表达式 |
| 第四章示例 | 特殊用法 | TRY…CATCH、RETRY、RESUME、CLEANUP |
提示:表格里的编号段是阅读顺序建议,不是官方分类;手册正文是按语法点排的,demo 只是配套验证。
2.2 建练习程序:SE38 与 ADT 两种打开方式
这份资料不是可导入的传输请求,而是一份语法手册加示例代码,所以第一步是把代码落到自己的练习系统里。最省事的做法是在$TMP本地包下建一个可执行程序(程序类型选「可执行程序」,也就是类型 1),把 demo 代码整段粘进去,激活后 F8 直接跑。用 ADT 的话新建 ABAP 类或者控制台程序都行,语法高亮和快速修复比 SE38 舒服不少。
建议先建一个环境探测程序,确认系统版本和新语法可用性,再往下刷 demo:
REPORT zdemo_env_check. " sy-saprl 是三位版本号,例如 750、754、756 DATA(lv_release) = sy-saprl. " 探针:构造表达式能激活,说明 7.40 之后的语法基础可用 DATA(lt_probe) = VALUE string_table( ( `ABAP` ) ( `SQL` ) ). " 探针二:内联声明 + 内表表达式 DATA(lv_lines) = lines( lt_probe ). DATA(lv_exists) = xsdbool( line_exists( lt_probe[ 1 ] ) ). WRITE: / 'Release :', lv_release. WRITE: / 'Lines :', lv_lines. WRITE: / 'Exists :', lv_exists.这段代码里sy-saprl是系统字段,不需要额外声明;VALUE string_table( ... )用标准类型string_table直接构造一张内表,省掉TYPES声明;xsdbool( )把布尔结果转成'X'/' ',因为 ABAP 的WRITE不认识abap_bool。三行输出就是判断依据:如果程序能激活,说明基础构造表达式没问题;如果激活时报「语句不被支持」,那就是语法版本不够,先去看下一节的版本边界。
建程序时有几个细节值得提前处理。一是程序名不要用ZDEMO_01_001这种带下划线的纯数字结尾的名字,某些系统对程序名的可读性检查会报警告,改成ZDEMO_NEW_SYNTAX_01更稳。二是如果用$TMP,对象不会进传输,重装系统就没了,长期练习建议建一个 Z 开发包。三是把每个 demo 单独建一个程序而不是全塞一个,因为内联声明的变量作用域到程序结束,混在一起很容易撞名。
2.3 版本边界:ECC 上会红叉的那几条语句
手册标题里写得很直接——适用 S/4,不适用 ECC 等老系统。这不是保守说法,是硬约束。ABAP 的语法演进以 7.40 为分水岭:构造表达式(NEW、VALUE、CONV、COND、SWITCH、REDUCE、FOR、FILTER)从 7.40 开始引入,7.40 SP08 之后补齐了FOR GROUPS、GROUP BY、LET、CORRESPONDING的部分增强,7.50 之后字符串函数、内表表达式、ABAP SQL 的内置函数才基本齐全,再往后的版本继续收紧 ABAP SQL 的严格模式检查。具体哪条语句在哪一版可用,别背,直接在目标系统里做语法检查(SE38 的检查、或者 ADT 保存时的实时校验)最快。
在 ECC 上最容易翻车的几类写法,我列一下,方便判断手上的系统能不能用:DATA(...)内联声明、FIELD-SYMBOL(...)内联字段符号、@转义的主机变量、INTO TABLE @DATA(lt)这种内联目标、REDUCE/FILTER/FOR GROUPS、字符串模板里的格式化选项(CURRENCY、ALPHA、DECIMALS)、ABAP SQL 里的CAST和内置函数、lines( )/line_exists( )这类内表表达式。这里面任何一条报「语句无法识别」,基本就是版本没到。
还有一类不是版本问题而是开关问题:ABAP SQL 的严格模式检查。同一个写法在开发机上是警告,在质量机上可能是错误,因为检查变式和版本不同。我的习惯是在练习程序里先把 Open SQL 那一段单独拎出来跑,跑通了再往业务程序里搬,不然后面排查起来不知道是语法问题还是数据问题。
3. New Keyword 实战:DATA(...)、VALUE、BASE、NEW/REF 的替换写法
3.1 内联声明:DATA(...) 与 FIELD-SYMBOL(...) 的适用边界
DATA(...)解决的是「声明一个只用一次的变量」这种噪音。老写法里一个方法开头能堆十几行DATA:,新写法把声明和赋值合并,读代码时视线不用来回跳。手册里第一个 demo 就讲了三种典型场景:赋值声明、SELECT内联目标、LOOP内联工作区。
" 1) 按字面量推导类型:字符型按长度推导,整型推导为 i DATA(lv_text) = 'This is a sentence'. DATA(lv_int) = 15. " 2) 类型池常量不需要 TYPE-POOLS 声明 DATA(lv_flag) = abap_false. DATA(lv_icon) = icon_material. " 3) SELECT 内联内表:字段类型与 SELECT LIST 一致 SELECT matnr, maktx FROM makt INTO TABLE @DATA(lt_makt) WHERE spras = @sy-langu. " 4) LOOP 内联工作区,替代带表头的内表 LOOP AT lt_makt INTO DATA(lwa_makt). WRITE: / lwa_makt-matnr, lwa_makt-maktx. ENDLOOP. " 5) 拆分字符串也能内联 SPLIT 'TEXT-01' AT '-' INTO DATA(lv_part1) DATA(lv_part2).第 1 组里的lv_text会被推导成变长字符或者定长字符(取决于赋值内容),lv_int推导成i;手册里专门提醒不建议用赋值来声明浮点数,因为推导不出来。第 3 组的@DATA(lt_makt)是内联内表,字段类型跟着SELECT LIST走,这就是后面很多坑的源头——数据库层类型和你的TYPES结构经常对不上。第 4 组是收益最明显的一处:有了内联工作区,就不需要带表头的内表,LOOP AT ... INTO wa里的wa只在循环里存在,方法嵌套时不会互相污染。
FIELD-SYMBOL(...)是同一套思路用在字段符号上,配合ASSIGNING和ASSIGN用:
" 循环里内联字段符号,直接改内表内容,不产生拷贝 LOOP AT lt_makt ASSIGNING FIELD-SYMBOL(<ls_makt>). <ls_makt>-maktx = to_upper( <ls_makt>-maktx ). ENDLOOP. " 动态字段名必须加括号,否则当成字面量处理 DATA(lv_tabname) = 'LT_MAKT'. ASSIGN (lv_tabname) TO FIELD-SYMBOL(<lt_any>). IF <lt_any> IS ASSIGNED. WRITE: / 'assigned'. ENDIF.ASSIGNING版本省掉了一次工作区拷贝,改字段直接落到内表行上,大内表循环时这个差别很明显。第二段的关键是括号:(lv_tabname)表示按变量里的名字动态解析,不加括号就是把字符串当静态名字。动态ASSIGN之后<lt_any>的静态类型是泛型,访问前务必用IS ASSIGNED判一下,否则就是GETWA_NOT_ASSIGNED的运行时错误,这个坑我在生产机上见过不止一次。
注意:手册里有一条实践备注值得抄下来——
DATA(...)声明出来的内表,在做CORRESPONDING传递时经常对不上字段,原因是内联推导的类型和你预期的结构类型不一致。凡是后面还要参与结构映射的内表,老老实实TYPES声明,别图省事。
3.2 VALUE 与 BASE:覆盖、重建与“保留旧值”的真实语义
VALUE是这套新语法里用得最多、也最容易被误解的一个。手册里对它的描述是「赋值语句」,用于给结构、内表、变量赋值,字段可以分开写,也可以整体赋值;给内表赋值时每一行要用小括号包起来。
TYPES: BEGIN OF ty_mat, matnr TYPE matnr, mtart TYPE mtart, text1 TYPE char50, END OF ty_mat. " 结构构造 DATA(ls_mat) = VALUE ty_mat( matnr = 'MATERIAL-001' mtart = 'FOOD' text1 = 'FIRST' ). " 内表构造:预定义类型用 # 代替 DATA(lt_mat) = VALUE STANDARD TABLE OF ty_mat( ( ls_mat ) ( matnr = 'MATERIAL-002' mtart = 'WATR' text1 = 'SECOND' ) ). " 直接在语句里用,省掉临时变量 APPEND VALUE #( matnr = 'MATERIAL-003' ) TO lt_mat. MODIFY lt_mat FROM VALUE #( mtart = 'FOOD' ) TRANSPORTING mtart WHERE mtart IS INITIAL.第一段是结构构造,没写的字段自动取初始值,这一点和CLEAR后再赋值的效果一样,但代码短得多。第二段是内表构造,( ls_mat )表示把已有结构整行塞进去,后面的括号是逐字段构造;VALUE #( )里的#复用左侧已知类型,写全了也没错,只是啰嗦。第三段的APPEND VALUE #( ... )和MODIFY ... FROM VALUE #( ... ) TRANSPORTING是我最常用的两个写法,前者省掉一个CLEAR wa+wa-xxx = ...+APPEND wa,后者在批量改字段时特别顺手——TRANSPORTING限定只改指定字段,WHERE限定行,不会误伤其他列。
真正需要说清楚的是「覆盖」这个词。手册的示例注释里写,VALUE会默认覆盖原有数据,但作者自己测试后加了句「覆盖一词并不准确,可以认为是重新赋值,原来有的不同的数据会被删除」。这句话是对的:VALUE是先按你给的值构造出一个新的完整对象,再整体赋给左侧,你没列出来的字段不会保留,全部回到初始值。所以下面这种写法是错的方向:
" 错误预期:以为只改 mtart,text1 会保留 DATA(ls_a) = VALUE ty_mat( matnr = 'M-1' mtart = 'FOOD' text1 = 'KEEP' ). ls_a = VALUE ty_mat( mtart = 'WATR' ). " 实际结果:matnr、text1 全被清空要保留旧值就得用BASE:
" BASE:以旧值为底,只覆盖显式列出的字段 ls_a = VALUE #( BASE ls_a mtart = 'WATR' ). " 结果:matnr = 'M-1',text1 = 'KEEP',mtart = 'WATR' " BASE 叠加内表:在已有行之后追加 lt_mat = VALUE #( BASE lt_mat ( matnr = 'MATERIAL-004' mtart = 'FOOD' ) ).BASE的参数就是「基底对象」,它先把基底的值拷进新构造的对象,再用后面列出的字段覆盖。结构赋值时它起到「局部更新」的作用,内表赋值时它起到「追加」的作用——注意是追加,不是替换,如果只想替换那就别加BASE。这两个语义差别很大,混淆了就是数据消失或者数据翻倍,都是难查的问题。
3.3 NEW / REF / CONV / EXACT:引用与类型转换的四种写法
NEW用来实例化引用对象,替代CREATE OBJECT。用法分两种:左边类型未知时在NEW后面显式写类名,左边已经声明过类型时用#。
" 场景:给 ALV 网格指定父容器 DATA(go_grid) = NEW cl_gui_alv_grid( i_parent = go_container ). " 左边类型已知,可以省略类名 DATA go_grid2 TYPE REF TO cl_gui_alv_grid. go_grid2 = NEW #( i_parent = go_container ).i_parent是构造器参数,必须是已经创建好的容器实例——手册的示例备注里提到这类 demo 测试没跑通,原因就是容器没建出来,go_container是空的。这不是语法问题,是运行时要保证的对象依赖顺序,跟CREATE OBJECT时代一模一样,只是写起来短了。我在实际项目里用NEW主要还是图省事,CREATE OBJECT ... EXPORTING那套写法在改造时会被检查工具标出来,一起替换掉也顺手。
REF用来创建数据引用,替代CREATE DATA,好处是不用提前声明引用类型,类型自动跟随被引用的变量:
DATA(lwa_work) = VALUE ty_mat( matnr = 'MATERIAL-001' mtart = 'FOOD' text1 = 'FIRST' ). DATA(lr_work) = REF #( lwa_work ). " 类型自动是 REF TO ty_mat lr_work->mtart = 'WATR'. " 只能通过 -> 访问 lr_work->text1 = 'SECOND'.这里有一条手册专门标注的注意事项:用了REF之后,引用变量不能用「引用名-字段」的方式访问,只能走->。写成lv_ref-matnr会直接语法错误,因为-是结构组件选择符,引用变量得用->解引用。刚切过来的时候这个错我犯过好几次,尤其是改造旧代码时把手写的CREATE DATA换掉,访问部分忘了改。
CONV和EXACT是类型转换的两面。CONV是宽松转换,能转就转;EXACT是严格转换,一旦发生舍入或者丢数据就抛异常,适合放在需要「数据不能悄悄变形」的地方。
DATA(lv_packed) = CONV p( '123.45' ). " 字符转 packed DATA(lv_int) = CONV i( lv_packed ). " 截断小数部分 " EXACT:有损转换抛异常,适合金额、数量这类字段 TRY. DATA(lv_exact) = EXACT i( '2.7' ). CATCH cx_sy_conversion_rounding. WRITE: / 'conversion would round'. ENDTRY.CONV p( )里的p是目标类型,不写长度时按默认规则推导;CONV i( )对小数字段是截断不是四舍五入,业务上要四舍五入得用ROUND或者round( )函数。EXACT的异常分支要按目标类型选:数值舍入对应cx_sy_conversion_rounding,字符截断对应cx_sy_conversion_lost_data这一支,用CATCH cx_sy_conversion_error兜底也行,但会盖住其他转换错误,不利于定位。
3.4 CORRESPONDING 与 MOVE-CORRESPONDING DEEP:结构映射不再靠手写赋值
两个结构之间同名字段批量赋值,老写法是MOVE-CORRESPONDING,新写法是CORRESPONDING。后者强在可以加映射规则、排除字段、指定基底。
DATA(ls_src) = VALUE ty_mat( matnr = 'M-1' mtart = 'FOOD' text1 = 'SRC' ). DATA(ls_dst) = VALUE ty_mat( matnr = 'OLD' text1 = 'DST' ). " 默认:只处理同名且类型兼容的字段 ls_dst = CORRESPONDING #( ls_src ). " 以目标旧值为底,只覆盖源里有值的字段 ls_dst = CORRESPONDING #( BASE ( ls_dst ) ls_src ). " 字段改名映射:目标 text1 取源的 matnr ls_dst = CORRESPONDING #( ls_src MAPPING text1 = matnr ). " 排除某字段 ls_dst = CORRESPONDING #( ls_src EXCEPT text1 ).第一段的「类型兼容」是个硬条件。CORRESPONDING只处理名字相同、类型可转换的字段,名字对上但类型不兼容(比如一个是CHAR10一个是NUMC10,或者内联推导出来的类型长度不同)就会被静默跳过。这就是手册提醒「内联声明的内表接不住CORRESPONDING」的实际表现:字段全是空的,但程序不报错,属于最难查的一类问题。我现在的做法是,凡是涉及跨结构映射的,源和目标都用显式TYPES定义,别让内联推导参与。
BASE ( ls_dst )这个写法解决的是「只更新源里非空字段」的需求,MAPPING解决字段名不一致,EXCEPT解决个别字段不能覆盖的场景。这三个加起来基本覆盖了日常的结构映射。
再往深一层是嵌套结构和内表。老的MOVE-CORRESPONDING只处理扁平结构,嵌套的内表是整体引用过去的(浅拷贝),改一个另一个跟着变。要深拷贝就得用DEEP:
TYPES: BEGIN OF ty_item, posnr TYPE posnr, qty TYPE menge_d, END OF ty_item, BEGIN OF ty_head, id TYPE char10, items TYPE STANDARD TABLE <p> <a href="https://download.csdn.net/download/qq_48677456/86776300" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>