ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Oracle空字符串陷阱:为何= ‘‘查不到数据,正确用IS NULL

Oracle空字符串陷阱:为何= ‘‘查不到数据,正确用IS NULL 空字符串这个坑我在Oracle上踩过不止一次而且每次都是线上问题、每次都是开发同事对着我说这SQL没问题啊我本地跑得好好的。等我把两张表的实际数据拉出来开发同事沉默了——空字符串压根不是空字符串它进了Oracle之后直接被当成NULL处理了。今天就把这个为什么彻底讲透后面附上我排查这类问题时的完整思路和规避方案。1. 查不到数据这个险我替各位踩过了先说一次典型的线上事故。业务系统里有个客户备注字段允许空值开发在需求里写查询所有没有备注的客户。实现的人很自然地写成了SELECT * FROM customer_info WHERE remark ;结果是什么一条数据都查不出来。当时我接到工单第一反应也是这SQL有啥问题直到我把库里真实的数据捞出来才发现那个字段里存的全是NULL而不是空字符串。Remark在Oracle里的实际值长这样SELECT remark, remark IS NULL, LENGTH(remark) FROM customer_info WHERE customer_id 1024;返回的结果是remarkremark IS NULLLENGTH(remark)(null)1(null)这时候写SQL的人通常会有两个疑问一是我明明没填内容界面传过来的也是空字符串为什么库里变成NULL了二是就算变成了NULL我用『 』判断有什么不对空字符串和NULL不是一个意思吗。这里要记住Oracle最反直觉的一条规则Oracle中VARCHAR2/VARCHAR/CHAR类型的空字符串会被隐式转换为NULL。语法上它不报错你INSERT一个进去它存进去的就是NULL你WHERE加一个 它实际的执行语义是 NULL。而NULL和任何值包括NULL自己做等值比较结果都是未知UNKNOWN不是TRUE也不是FALSE最终WHERE条件不成立自然一条也查不出来。这个特性坑人之处在于它不是报错而是静默地改变数据。INSERT和UPDATE不报错SELECT不报错但数据语义已经完全不同了。等你发现异常的时候库里已经有一堆原本应该是空字符串、实际是NULL的数据了。所以Oracle里判断这类字段的正确姿势就是标题里的答案用IS NULL。SELECT * FROM customer_info WHERE remark IS NULL;2. Oracle为什么这么设计空字符串和NULL的底层关系我在被这个规则坑了三四次之后专门去翻了Oracle官方文档和相关的Ask TOM逐渐理解了这条设计背后的逻辑。Oracle文档里有一句原话值得背下来Oracle Database currently treats a character value with a zero length as null. However, this may not continue to be true in future releases, and Oracle recommends that you do not treat empty strings the same as nulls.翻译过来就是Oracle目前把长度为零的字符值当作NULL处理但未来版本可能不这么干官方建议你别把空字符串和NULL混为一谈。2.1 从ANSI标准说起Oracle和标准SQL唱了个反调标准SQL里空字符串和NULL是两种完全不同的概念。空字符串是一个长度为0的字符串值是有值的NULL是未知/不存在的值是没值的。两者在逻辑上有着本质区别。主流数据库里MySQL、SQL Server、PostgreSQL都遵循这一标准就是NULL就是NULL井水不犯河水。但Oracle偏偏在早期版本里做了个特殊决定把空字符串当成NULL。Oracle这么做有历史渊源——早期的Oracle在存储层面就是用NULL来表示无内容的统一处理可以简化存储结构。这个设计从Oracle 7之前的时代就存在了一直沿用到今天Oracle自己也承认这不是最佳实践但为了向后兼容没有轻易改变。这个差异给程序员带来的直接后果就是在MySQL上正常运行的代码迁到Oracle上凡是涉及空字符串判断的几乎都会出问题。我在帮一个从MySQL迁移到Oracle的项目做适配时光这一条规则就排查出二十多个问题SQL全是WHERE field 或field 这种写法。2.2 LENGTH()和LENGTH(NULL)为什么结果不一样读者看到LENGTH()可能觉得奇怪既然被当成NULL那LENGTH()应该也返回NULL才对。实际上在Oracle中SELECT LENGTH() FROM dual; -- 返回 NULL没错LENGTH()在Oracle里的返回值就是NULL。因为在SQL引擎解析阶段已经被当成NULL传给函数了。这与LENGTH(NULL)完全一致。这就引出了一个小技巧如果你需要区分用户没填和用户填了但内容为空这两种情况在Oracle里靠SQL层面是分不开的必须在应用层做处理。一般我会在应用入口把用户输入的空字符串统一转成NULL再存入数据库或者反过来在展示层把NULL统一显示成空字符串。2.3 NOT NULL约束下的空字符串陷阱这里还有一个衍生坑在Oracle中如果某列约束为NOT NULL你试图往里INSERT空字符串会直接报ORA-01400错误提示cannot insert NULL into (...)。初接触Oracle的人会非常困惑——我传的是一个空字符串怎么提示我插入NULL我见过一个实际案例有个接口对接第三方系统对方返回的XML里某个字段是name/这种空标签映射后得到空字符串程序直接往Oracle表里INSERT结果每隔一段时间就报一批ORA-01400。开发查了半天一开始以为是代码Bug后来才明白是Oracle的空字符串转NULL规则和NOT NULL约束组合导致的。解决思路有两条一是在入库前判空把空字符串转换成NULL外的占位值比如空格字符串 注意这是一个空格不是空字符串二是修改表结构允许该列为NULL。至于选哪条要看业务语义。如果这个字段为空是有意义的业务状态那应该允许NULL如果是必填字段就应该在应用层拦截非法请求而不是让数据库承受这种歧义上报错误。3. 与其他数据库对比为什么换个数据库就变天了Oracle的空字符串等于NULL规则不是孤例但绝对是主流关系型数据库里最特立独行的存在。我整理了一张对比表方便各位在跨数据库开发时快速参考数据库是否等于NULLWHERE field 查询WHERE field IS NULL查询插入到NOT NULL列Oracle等于隐式转换查不到实际执行 NULL能查到报ORA-01400MySQL不等于能查到空字符串的记录只查NULL允许SQL Server不等于能查到空字符串只查NULL允许PostgreSQL不等于能查到空字符串只查NULL允许注意表格里Oracle那一行的行为WHERE field 查不到任何东西但WHERE field IS NULL却可能查到一大堆原本是空字符串的记录。这种看起来等值、实际等NULL的现象是所有从其他数据库转向Oracle开发的人最容易踩的第一个坑。MySQL和Oracle之间的差异尤其值得多说两句毕竟国内这两款数据库的使用面最广。在MySQL里和NULL在查询行为上虽然都表示没有但它们有本质区别-- MySQL中 SELECT COUNT(*) FROM t WHERE name ; -- 统计空字符串数量 SELECT COUNT(*) FROM t WHERE name IS NULL; -- 统计NULL数量两条SQL统计的不是一批数据。MySQL的COUNT(*)不会统计NULL但会统计空字符串索引对NULL和的处理也不同。而Oracle里你根本分不出这两者因为进去之后就变成了NULL。还有一个容易忽略的问题从Oracle导出数据到MySQL或反过来从MySQL同步到Oracle。MySQL里的空字符串同步到Oracle后会被转换成NULL如果你下游有判断该字段是否为空的逻辑很可能因同步前后数据语义变化导致报表数据不一致。我在做数据仓库同步时专门加了一层清洗规则把所有源端空字符串统一映射为一个特殊值比如字符串EMPTY避免源端不同数据库语义差异污染数仓。4. 真正容易出问题的五类场景原理搞清楚了下面从实战角度盘点那些最容易踩坑的场景尤其是结合存储过程、EBS这类企业级应用中常见的写法。4.1 存储过程里的空字符串判断Oracle的存储过程是重灾区。我见过太多PL/SQL代码里有这样的判断IF v_remark THEN -- 做一些业务处理 END IF;在PL/SQL的布尔逻辑里v_remark 等价于v_remark NULL结果永远不是TRUE。这段IF分支永远不会执行业务逻辑静默失效。正确写法是IF v_remark IS NULL THEN -- 做一些业务处理 END IF;如果你要兼容字段既可能是空字符串比如从外部系统传入也可能是NULL那就得先归一化处理IF NVL(v_remark, ) THEN -- 空字符串和NULL统一处理 END IF;这里用NVL把NULL归一到空字符串再和比较。注意NVL(v_remark, ) 在Oracle里是安全的因为这个在比较前也会转成NULL但同NVL搭配后实际执行的是NVL结果和NULL的比较结果全为NULL也是不成立不对这里要小心。让我重新想一下在Oracle SQL和PL/SQL中空字符串就是NULL。NVL(v_remark, )的语义是如果v_remark为NULL则返回而本身就是NULL。所以NVL(v_remark, )本质还是NULL。那NVL(v_remark, ) 就等于NULL NULL还是永远不成立。噢对这个写法其实是网上流传的写法但实际上行不通让我仔细推敲。Oracle中确实有人用NVL(column, ) 注意是含一个空格的字符串来统一处理因为有一个空格的字符串不是空字符串不会被转成NULL。这是可行的。如果写NVL(column, )因为就是NULL所以NVL等于什么都没做还是NULL。然后NULL 还是NULLNULL NULL仍然是NULL条件不成立。所以NVL(v_remark, ) 这个写法是不能用的很多人会搞混。那怎么正确统一判断通常有几种方式方式一v_remark IS NULL只能判断NULL如果传入的是非Oracle来源的在绑定变量层面也会被转成NULL所以直接IS NULL可能就够了但不够保险。方式二使用NVL(v_remark, ) 注意这里占位的是单个空格不是空字符串。方式三使用COALESCE(v_remark, ) 同理。方式四在应用层就统一转换成NULL。其实还有更稳妥的TRIM(NVL(v_remark, )) 或者NVL(TRIM(v_remark), ) 这样能同时把全是空格的情况也算进去。这才是判断字段是否为空含NULL和空字符串和全空格的完整写法。所以在博文里我要强调NVL(col, )这行不通的因为就是NULL需要用 这样的占位符。同理在EBS这类大型ERP系统中很多核心表都有大量可空字段开发报表时经常要写查找没有值的数据的逻辑。如果存储过程或报表SQL里用了 在EBS环境中几乎必然漏数据。我记得之前处理过一个Oracle EBS WIP模块的非标工单查询问题用户要求展示物料备注为空的非标工单开发写的是WHERE wip_entity_attr_value 结果漏掉了一半工单因为那些工单的备注值是NULL而不是空字符串。改成IS NULL后数据才完整。4.2 EBS、成本管理和报表查询中的隐藏坑Oracle EBS是老牌ERP系统底层全是Oracle数据库空字符串问题在EBS里被放大得特别厉害。因为EBS表单界面上很多看起来是空的字段存到数据库里有时是NULL有时可能是空格有时是空字符串——具体取决于接口代码怎么写。写报表SQL或做二次开发时如果没有对字段做归一化处理对账就对不平。举一个我做Oracle EBS二次开发时的典型案例在WIP模块做非标工单的成本归集查询需要按工单的某个描述字段过滤无描述的工单。当时需求方觉得无描述就是等于空字符串但实际数据里既有NULL又有空字符串甚至还有几个字段值是多个空格的情况。页面显示都是空的但SQL写 只能匹配到那些真正存了空字符串的记录其实Oracle中也被转成NULL了写IS NULL能匹配NULL而多个空格的记录两种都匹配不到。最后只能用TRIM(字段) IS NULL来做统一判断。这个坑在EBS报表开发里特别常见。如果是用Oracle EBS的Form或REST接口传参也会遇到类似问题前端传空字符串后端PL/SQL收到的是NULL然后和NOT NULL约束打架或者写入接口直接报错。处理经验是接口层要做一次空值归一化无论传进来的是NULL、还是多个空格统一转成业务约定的默认值。4.3 唯一索引与多行空值另一个经常被忽略的场景是唯一约束或唯一索引。在Oracle中如果某列允许NULL多个NULL值在唯一约束下是可以共存的因为Oracle默认NULL不等于NULL多行NULL不会触发唯一性冲突。这个行为和空字符串转NULL一结合就产生了一个很微妙的坑如果你把某列定义为唯一列又往里插入了多行空字符串因为空字符串都变成了NULL所以都能插入成功不受唯一约束限制。这在MySQL里就不一样MySQL的InnoDB里唯一索引允许NULL但也允许多个NULL实际上MySQL的NULL处理也是允许多个的除非用的是唯一索引的变体。等下MySQL也是允许多个NULL的这个例子其实不足以区分。更关键的差异是如果你的业务希望空字符串不能重复在MySQL里你可以靠唯一索引拦住因为 ! 所以唯一索引会把两个拦住报冲突但在Oracle里等于NULL唯一索引拦不住。这是我实际遇到过的一个数据重复问题用户的手机号字段允许为空但非空的不能重复。MySQL时代靠唯一索引就拦住了迁移到Oracle后空手机号的用户可以无限插入还发现不了。后来业务妥协了空手机号统一用字符串0或者一个特殊占位值才能继续用唯一索引做约束。4.4 外键关联和JOIN条件中的NULL不匹配如果两个表都用空字符串表示无JOIN时也会有问题。比如订单表的优惠券ID字段如果没有使用优惠券存的是在Oracle里变成NULL另一张优惠券表里没有对应记录INNER JOIN自然会漏掉这些没使用优惠券的订单。开发人员如果没意识到这一点统计使用了优惠券的订单时不会出错但统计未使用优惠券的订单时就会用NOT EXISTS或LEFT JOIN IS NULL逻辑要特别小心。更隐蔽的坑是外键约束子表某外键列插入Oracle把它转成NULL如果该外键列没有加NOT NULL约束Oracle的NULL外键不会去父表匹配即允许悬空这在业务上可能意味着这个关联关系没有值也可能意味着脏数据。我在数据质量检查时经常用下面这条SQL来排查外键悬空SELECT child_table.* FROM child_table WHERE child_table.fk_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM parent_table WHERE parent_table.id child_table.fk_id);但如果业务上无关联存的是空字符串而不是NULL因为Oracle把转成NULLIS NOT NULL就不会匹配到它们它们成了假的有效关联下游统计时会计入一些本不该计入的数据。这就是典型的静默错误。4.5 动态SQL拼接中的长度丢失还有一个和动态SQL相关的细节在拼接SQL时如果你拼进去的变量是空字符串结果会出现语法层面看起来正常、执行层面却丢失条件的情况。例如用PL/SQL动态拼接v_sql : SELECT * FROM orders WHERE 11 AND status || v_status || ;如果v_status是空字符串NULL拼出来的SQL变成了SELECT * FROM orders WHERE 11 AND status 注意这个在Oracle中解析后是NULLstatus NULL永远不成立查询结果会意外地变成一条也查不到。比起直接的逻辑错误这种动态SQL出错更难定位因为你打印出来的SQL字符串看起来完全正常但Oracle内部已经把它翻译成NULL比较了。排查时如果发现动态SQL结果和预期不一致优先检查拼接进来的变量有没有可能是空字符串。规范做法是用绑定变量而不是字符串拼接绑定变量能少掉一大类这类问题。5. 从一次慢查询到全表扫描NULL条件引发的执行计划漂移把空字符串和NULL混为一谈不仅影响数据正确性还会影响执行计划。这里分享一个比较隐蔽的性能案例也是我在Oracle 11g上处理过的真实问题。一张订单明细表订单状态字段有索引。开发在条件里传了空字符串SQL大概是SELECT * FROM order_detail WHERE status ;因为被转成NULL这条SQL等价于SELECT * FROM order_detail WHERE status NULL;问题就出在优化器如何处理col NULL。在Oracle里status NULL永远不会为TRUE优化器通常能识别出这个条件恒为假理论上会直接返回0行甚至不访问表。但实际遇到的情况不是这样我们那次是动态SQL拼出来的条件优化器在解析动态SQL时没法提前折叠条件加上统计信息不准确它选择了一个看起来合理的路径走了全表扫描。这张表业务量大几个亿行全表扫描一跑就是十几分钟。我后来做的优化方案是把动态拼接改成绑定变量并在应用层对空字符串做转换统一传NULL。把查询条件里的空字符串判断改成status IS NULL并且给优化器提供了更精确的统计信息收集了histogram因为该列NULL值占比极高。如果业务上确实需要用 的逻辑比如外部系统传参在PL/SQL里先做一次条件归一化把转成NULL后再执行查询。如果要在条件里同时兼容空字符串和NULL建议使用函数改写WHERE TRIM(status) IS NULL但注意在status列上有函数就会抑制普通索引。这种写法适合小表或者配合函数索引CREATE INDEX idx_order_detail_trim_status ON order_detail (TRIM(status));这是Oracle特有的能力MySQL等数据库对函数索引的支持各不相同移植到别的库时要谨慎。总的来说我建议优先在应用层把数据归一化而不是在SQL层用各种函数绕来绕去——否则索引没法用性能迟早出问题。6. 怎么避免下次再踩——规范化改造与应用层防御讲了这么多原理和案例下面整理一套我实际用于团队规范和代码评审的检查清单按事前预防、事中审查、事后修正三个维度来落地。6.1 统一字段语义应用层禁止直接传入空字符串在项目开发规范里我明确要求所有对Oracle的写入操作空字符串在进入SQL前必须统一转换成NULL由专门的工具函数处理。读取数据展示时由前端或中间层把NULL转成空字符串显示数据库层不负责这种转换。存储过程入参同理在过程开头用IF v_param THEN v_param : NULL; END IF;做归一化注意实际写的时候v_param 这个判断本身也是永远不会成立的正确的归一化写法要考虑这一点比如使用NVL或直接由Java/Python端转好再传入。这里有个特别重要的点不要把归一化写在Oracle存储过程内部的IF判断里因为IF v_param 这个判断本身就因为空字符串转NULL而失败。我之前见过一个开发小哥试图在存储过程里加这道防线写了IF in_param THEN in_param : NULL; END IF;然后这个IF分支永远不执行他百思不得其解。原因就在于左边的in_param如果是空字符串已经在底层转成NULL了NULL 的结果是UNKNOWNIF条件不成立。正确做法是在Java、Python、C#这类宿主语言层把空字符串统一转成NULL再传给存储过程或者使用下面这种能真正生效的方式-- PL/SQL 中安全的归一化写法用空格占位不能写空字符串 in_param : NULLIF(in_param, );NULLIF(a, b)函数返回NULL当a等于b时。但这里的b如果用又会被转成NULLNULLIF(in_param, )实际等价于NULLIF(in_param, NULL)只会返回in_param还是没用。所以必须用 之类的占位值。这种看似绕圈的写法恰恰说明了Oracle空字符串规则的顽固性——在PL/SQL内部你连空字符串的表达能力都没有。6.2 SQL编写规范禁止使用 判断空值在代码评审阶段我加了一条硬性规则Oracle项目的SQL代码中凡是出现 或者! 的情况必须标注并改为IS NULL / IS NOT NULL。这是因为 永远匹配不到任何数据! 也永远匹配不到任何非空字符串数据因为它等价于! NULL但实际上还会漏掉所有NULL值配套的还有一条不要用 、! 来做非空判断原因同上。我之前见过一个统计逻辑统计填写了手机号的用户数写的条件是WHERE phone ! 结果MySQL迁Oracle后这个数直接变成0因为所有非NULL的phone和NULL比较结果为UNKNOWN一律不返回。正确写法是WHERE phone IS NOT NULL。如果要同时排除空字符串、满空格、NULL用WHERE TRIM(phone) IS NOT NULL或者把数据清洗干净后只用IS NOT NULL。6.3 用正则和工具审计存量SQL存量系统里有大量历史SQL不可能靠人工一条条看。我给团队用过几种方案用静态扫描工具比如公司内部的SQL审核平台或者自己写正则搜索 、! 、 这类模式。对筛选出的SQL统一走人工复核确认业务语义后再改写。在测试环境构造空字符串、NULL、空格三种测试数据跑一轮回归观察查询结果是否符合预期。正则示例常见IDE或脚本中可用[!]\s*这个正则基本能覆盖绝大多数、!、的写法再结合代码仓库的提交历史可以在几个小时内把存量问题扫出来。当时我审计一个三十多张核心表的报表系统用类似方法扫描出60多处可疑写法最终确认有20多是真Bug这些Bug平时不报错、不影响主流程但每个季度出报表时数据就悄悄不对。6.4 表结构设计与数据清洗的兜底方案如果已经有一堆存量假空字符串数据需要做一次数据修正。核心是明确业务语义然后把数据统一到一种形态。我的建议分两步确认哪些字段空值在业务里有特殊含义和业务方对齐空的统一表达是NULL还是占位值。执行数据修正SQL例如把含多个空格的字符串统一置为NULL或相反UPDATE customer_info SET remark NULL WHERE TRIM(remark) IS NULL AND remark IS NOT NULL;执行前务必先SELECT预览影响行数确认业务接受。这类UPDATE在上线前要有完整备份和回滚方案因为一旦把业务上有意义的空格也清了数据就无法还原。另外如果是要根治表设计阶段就该明确三范式和空值策略哪些字段允许NULL哪些字段不允许为NULL但允许保持空若允许空字符串则统一用特定默认值如 或NULL来占位。虽然这种设计有点丑但考虑到Oracle的怪癖它是能保证唯一约束、外键约束、索引都正确生效的务实手段。7. 跨库兼容写法一套代码同时兼容Oracle和MySQL最后聊一个很多人关心的点项目可能要同时支持Oracle和MySQLSQL怎么写才能两边都不踩空字符串的坑我的实践是避免在SQL里直接比较空字符串把逻辑上提到应用层。具体来说在Java/MyBatis里这样处理if (StringUtils.isEmpty(param)) { query.setRemark(null); // 统一转成 null } else { query.setRemark(param); }然后SQL里只写remark IS NULL或remark #{remark}。这样Oracle和MySQL的行为都正确Oracle中NULL是NULLMySQL中NULL还是NULL。避免写remark 这种两边行为不一致的代码。如果业务要求严格区分空字符串和NULL两种状态那么在MySQL里可以直接区分。在Oracle里区分不了至少SQL层区分不了必须通过额外标识字段来记录比如加一列remark_is_null NUMBER(1)。从架构角度讲我更倾向于承认Oracle的这个限制而不是和它硬刚。业务上空字符串和NULL的差异绝大多数场景下是不需要区分的如果真需要区分用扩展字段别指望SQL层做区分。至于那些既有Oracle又有MySQL的ETL/BI团队还有一个实用技巧在抽取层默认把MySQL里的空字符串转成NULL这样两边数据进数仓后语义统一下游分析不会因为源端不同产生歧义。8. 最后的实战经验排查空字符串问题时的完整链路遇到类似问题不要慌我一般按下面几条顺序排查基本能在十几分钟内定位根因。第一看数据。执行一条简单的查询把可疑字段的原始值、IS NULL判断、LENGTH函数结果同时打出来SELECT col, col IS NULL AS is_null_check, LENGTH(col) AS len_col FROM your_table WHERE rownum 100;如果col列显示(null)is_null_check为1len_col为(null)基本可以确认该字段在库里已存为NULL。这里注意LENGTH(NULL)返回NULL别把NULL长度误认为0。第二看入口。如果是应用写入导致的查应用的ORM日志或接口层日志看实际传给数据库的SQL绑定参数是什么。常用工具如SQL Monitor、Oracle的v$sql_bind_capture视图都可以看绑定变量值SELECT sql_id, name, value_string FROM v$sql_bind_capture WHERE sql_id 你的SQL_ID;如果显示value_string为空但数据库中存储为NULL那要么是应用层没做转换要么是Oracle隐式转换已经发生可以进一步在应用层打断点确认。第三看执行计划。如果怀疑性能受到影响用EXPLAIN PLAN或DBMS_XPLAN看执行计划里对条件列有没有走索引。对 和IS NULL的执行计划做对比能直观看出优化器对这两者的处理差异。注意即使优化器把 等价成 NULL并识别为恒假某些复杂场景如OR条件、子查询展开、动态SQL下它仍然可能选择全表扫描所以性能排查时不能想当然。第四写验证SQL。把改写前后的查询结果做diff确认 查不到、IS NULL能查到但数量要对得上业务预期。如果数量对不上要警惕数据里存在多个空格不可见字符等情况这时需要动用正则或DUMP函数看字段的真实字节内容SELECT col, DUMP(col) FROM your_table WHERE rownum 100;DUMP(col)会返回每个字符的ASCII码序列比如空格字符串的ASCII码是32而空字符串在Oracle中已经不存在。这个工具在排查不可见字符导致看起来是空、实际非空的问题时非常有用。最后说一条个人经验凡是涉及Oracle的字符串判空写完SQL先问自己一句——这条SQL换到MySQL还能跑出同样结果吗如果不能多半是踩了Oracle的空字符串转换坑趁早改写。
返回列表