我让 DevEco Code 的 AI 写 ArkTS,它把初始化塞进变量声明,arkts-no-side-effects 红了一整页

先上一段 AI 给我生成的代码,你就能懂我为什么说它"红了一整页":

// ❌ DevEco Code 的 AI 自动补全出来的 CaseList 组件开头@Componentexportstruct CaseList{@Statecases:CaseItem[]=CaseService.getHotList()// 编译报错@Statekeyword:string=''@Statefiltered:CaseItem[]=this.filterCases()// 编译报错,还顺带逻辑错filterCases():CaseItem[]{returnthis.cases.filter(c=>c.title.includes(this.keyword))}build(){// ... 省略}}

我盯着那两行红色波浪线愣了得有十秒。CaseService.getHotList()明明是个正常的异步方法,为什么声明个变量都不让?

说白了,ArkTS 的严格模式(默认开)有个挺反直觉的规矩:成员变量的初始化表达式必须是"编译期就能确定的纯值",你不能在那儿调函数、不能写带副作用的逻辑、连this.xxx都不行。TS 里随手写private list = load()没问题,搬到 ArkTS 这儿编译器直接掀桌子,报的就是arkts-no-side-effects-in-initialization

为什么鸿蒙要这么轴?因为它想在编译期就锁死"对象创建过程没有副作用",这样运行时行为可预测,也方便做并发和激进优化。代价就是我们写 TS 那套"声明即计算"的偷懒习惯得改。

你猜怎么着,我当时第一反应是"这 AI 是不是抽风了",还手动给getHotList()加了层括号想蒙混过关——没用,报错换了个位置照样红。

说起来,雷达鸭鸿蒙版那个案例瀑布流,最早就是让 DevEco Code 的 AI 生成的,第一版就在声明处调了接口,编译直接给我红了一页。


坑一:声明处调函数,编译直接掀桌子

根因上面说了。AI 按 TypeScript 肌肉记忆写初始化,但 ArkTS 不吃这套。修复其实就一句话:声明处只给字面量或空值,真正的初始化逻辑挪到aboutToAppear里。

// ✅ 初始化挪进 aboutToAppear,声明处干干净净@Componentexportstruct CaseList{@Statecases:CaseItem[]=[]// 只给空数组@Statekeyword:string=''@Stateloading:boolean=trueaboutToAppear():void{CaseService.getHotList().then((list:CaseItem[])=>{this.cases=listthis.loading=false}).catch(()=>{this.loading=false})}build(){if(this.loading){Loading()return}List(){ForEach(this.cases,(item:CaseItem)=>{CaseCard({item})},(item:CaseItem)=>item.id)}}}

这个小改动能救你一晚上。我承认我一开始特别不习惯——TS 写久了,谁没事把初始化都塞生命周期里——但 ArkTS 就这么定死的,你拧不过它。

提一句,如果你非要在声明处给个"算出来的值",那只能是编译期常量或者字面量,比如private pageSize: number = 20这种,调方法一律不行。

还有个变体坑顺带说一下:AI 有时候不死心,把初始化塞进constructor()。ArkTS 的@Component结构体压根没给你随便写构造函数去干这种活儿的口子,正经路子就是aboutToAppear。我早期还真被它这么坑过一回,编译倒是过了,跑起来状态全空,查了半天才发现赋值写进了根本不会被调用的地方。


坑二:派生状态用 @State 存,源变了它装死

第一个坑好歹编译器会红脸提醒你。第二个坑才叫恶心:它不报错、不白屏,就是"行为不对",review 肉眼根本看不出来。

还是那个列表页,加了搜索框之后 AI 这么改的:

// ❌ 派生列表用 @State 存,源一变它不跟着变@Componentexportstruct CaseList{@Statecases:CaseItem[]=[]@Statekeyword:string=''@Statefiltered:CaseItem[]=[]// 看着像状态,其实是一次性快照aboutToAppear():void{CaseService.getHotList().then((list:CaseItem[])=>{this.cases=listthis.filtered=list// 只在初始化时算一次})}onSearch(value:string):void{this.keyword=value// AI 忘了同步更新 filtered,列表纹丝不动}build(){List(){ForEach(this.filtered,(item:CaseItem)=>{CaseCard({item})},(item:CaseItem)=>item.id)}}}

你搜"咖啡",keyword变了,cases没变,filtered更没变——因为它只在aboutToAppear里被赋值过一次,从此变成一张静态快照。用户输入半天,页面像死了一样。

根子在于@State只认自己那块内存的引用变化。你第一次this.filtered = list换了引用,它通知渲染;之后你只动keywordcasesfiltered的引用纹丝没动,框架自然觉得"这哥们没改过我,别重画"。AI 的逻辑是"初始化时算好就完事",它压根没意识到派生数据是要跟着源活的。

这种 bug 我搜了俩小时 Stack Overflow 都没找到对症的答案,末了是自己打了日志才发现filtered压根没被重新算。

正解有两种。最稳的、哪儿都能跑的是把派生数据写成只读 getter,每次build自动重算,根本不占@State

// ✅ 派生数据用 getter,源 @State 一变它自动重算@Componentexportstruct CaseList{@Statecases:CaseItem[]=[]@Statekeyword:string=''getfiltered():CaseItem[]{returnthis.cases.filter((c:CaseItem)=>c.title.includes(this.keyword))}onSearch(value:string):void{this.keyword=value// 只改源,filtered 在 build 时自己重算}build(){List(){ForEach(this.filtered,(item:CaseItem)=>{CaseCard({item})},(item:CaseItem)=>item.id)}}}

要是你在 API 12 以上、想用状态管理 V2 那套更正式的机制,可以把派生状态标成@Computed,它专门干这个活儿:

// ✅ API 12 状态管理 V2:@Computed 才是给派生状态准备的import{Computed}from'@ohos.arkui.StateManagement'@ComponentV2struct CaseList{@Localcases:CaseItem[]=[]@Localkeyword:string=''@Computedgetfiltered():CaseItem[]{returnthis.cases.filter((c:CaseItem)=>c.title.includes(this.keyword))}build(){ForEach(this.filtered,(item:CaseItem)=>{CaseCard({item})},(item:CaseItem)=>item.id)}}

@Computed的好处是它只在依赖的@Local变了才重算,比每次build都跑一遍 getter 省点开销。但我个人更常用上面那个 getter 写法——不是所有项目都升到了 V2,老项目里@ComponentV2和 V1 装饰器混用容易出更难查的坑。

等一下,这里我漏说一个前提:@Computed必须配合@ComponentV2@Local用,跟 V1 的@State不是一套东西,别在老组件里硬塞。


收个尾

我现在让 DevEco Code 的 AI 写 ArkTS 组件,第一件事就是扫一眼变量声明处有没有偷偷调函数——只要看到= xxx()我就直接打回去重生成。这个习惯是拿两晚上的编译报错换来的。

说实话,我现在更信自己手写初始化,AI 补全的组件我一律先过一遍声明处。你要是也常被这类"能编译但行为诡异"的坑咬,建议把这条直接写进团队的 ArkTS 编码规范——别等上线了才发现有个列表死活搜不出来。

你写 ArkTS 的时候,AI 还给你埋过什么看起来能跑、实则埋雷的写法?评论区聊聊,我看看还有多少坑没踩完。


我是老三,10+ 年软件开发经验,软件设计师、人工智能应用工程师。现在主要做鸿蒙应用开发(ArkTS 北向)和 Web 前端,也在折腾 AI 自动化。偶尔在 CSDN 写点鸿蒙 / AI 方向的实战笔记。

本文遵循 MIT 协议,转载请注明出处。