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

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

typescript 复制代码
// ❌ DevEco Code 的 AI 自动补全出来的 CaseList 组件开头
@Component
export struct CaseList {
  @State cases: CaseItem[] = CaseService.getHotList()   // 编译报错
  @State keyword: string = ''
  @State filtered: CaseItem[] = this.filterCases()       // 编译报错,还顺带逻辑错

  filterCases(): CaseItem[] {
    return this.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 里。

typescript 复制代码
// ✅ 初始化挪进 aboutToAppear,声明处干干净净
@Component
export struct CaseList {
  @State cases: CaseItem[] = []      // 只给空数组
  @State keyword: string = ''
  @State loading: boolean = true

  aboutToAppear(): void {
    CaseService.getHotList()
      .then((list: CaseItem[]) => {
        this.cases = list
        this.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 这么改的:

typescript 复制代码
// ❌ 派生列表用 @State 存,源一变它不跟着变
@Component
export struct CaseList {
  @State cases: CaseItem[] = []
  @State keyword: string = ''
  @State filtered: CaseItem[] = []   // 看着像状态,其实是一次性快照

  aboutToAppear(): void {
    CaseService.getHotList().then((list: CaseItem[]) => {
      this.cases = list
      this.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

typescript 复制代码
// ✅ 派生数据用 getter,源 @State 一变它自动重算
@Component
export struct CaseList {
  @State cases: CaseItem[] = []
  @State keyword: string = ''

  get filtered(): CaseItem[] {
    return this.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,它专门干这个活儿:

typescript 复制代码
// ✅ API 12 状态管理 V2:@Computed 才是给派生状态准备的
import { Computed } from '@ohos.arkui.StateManagement'

@ComponentV2
struct CaseList {
  @Local cases: CaseItem[] = []
  @Local keyword: string = ''

  @Computed
  get filtered(): CaseItem[] {
    return this.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 协议,转载请注明出处。

相关推荐
神奇霸王龙1 小时前
国产音乐视频 Prompt 三段式屠夫榜:5 个国产视频模型实测对比
人工智能·ai·prompt·音视频·agent·ai编程·agi
OpenCSG1 小时前
Hugging Face遭遇AI Agent攻击:AI资产管理正在进入新阶段
人工智能·大模型
JaydenAI1 小时前
[A2A协议与实现-06]如何将一个MAF Agent发布为A2A服务[JSON-RPC]
ai·agent·sse·a2a·maf
神奇霸王龙1 小时前
AI视频Prompt结构化实战指南:可灵/万相/豆包
人工智能·ai·prompt·aigc·音视频·ai编程
aax12134531 小时前
VOC 集群治理工程实操|美丽蓝天绿岛项目 RTO 工艺选型、管控方案与申报要
大数据·数据库·人工智能
Scott9999HH2 小时前
【工控选型破局】拒绝盲目溢价与低质陷阱:基于 Modbus-RTU 状态诊断与自适应滑动中值滤波的 C++ 实战,兼论“性价比高又好用的仪器仪表厂家”选型之道
人工智能
To_OC2 小时前
调了一上午 DeepSeek 参数,我终于摸透了 temperature 和 Top K 的真实作用
人工智能·llm·deepseek
IvorySQL2 小时前
PG 日报|优化缓冲区批量扫描,降低多套接字并发竞争
数据库·人工智能·postgresql·开源·区块链
阳光是sunny2 小时前
LangGraph实战教程:defer延迟节点——让收尾工作自动排到最后
前端·人工智能·后端
70asunflower2 小时前
别再混淆 CUDA 的三个版本:`nvidia-smi`、`nvcc`、`torch.version.cuda` 一次讲清
人工智能