仓颉语言入门与 ArkTS 对比:HarmonyOS NEXT 的开发新范式

引言
在 HarmonyOS NEXT 的生态版图中,仓颉语言(Cangjie)正逐渐从幕后走向台前。作为华为自主研发的通用编程语言,仓颉不仅承载着构建下一代鸿蒙应用的技术使命,也在语法设计、类型系统和运行时理念上带来了不少让人眼前一亮的新思路。
本文面向有一定 ArkTS 开发经验的读者,系统梳理仓颉语言的核心概念与实战用法。通过与 ArkTS 的对照,帮助读者快速建立迁移直觉,理解两种语言在设计哲学上的异同,从而在实际项目中做出更合理的技术选型。
整篇文章以「能上手写代码」为目标,代码示例追求短小精悍、原理清晰,避免大段工程代码堆砌。读完本文后,你应该能够在本地搭起仓颉开发环境,写出第一个控制台程序,并理解声明式 UI 在仓颉中的基本写法。
一、仓颉语言简介:定位与背景
1.1 它是谁
仓颉(Cangjie,简称 CJ)是一款面向全场景智能设备的通用编程语言,由华为自主设计并开源。它的设计目标是提供一种安全、高效、表达力强的开发体验,覆盖从轻量级设备到复杂应用的全场景需求。
与 ArkTS 类似,仓颉也是一种静态类型语言,但两者并非简单的替代关系。在当前的 HarmonyOS NEXT 生态中,ArkTS 仍然是声明式 UI 开发的主力语言;仓颉则被定位为更底层的应用开发语言,未来有望逐步扩展到系统层和框架层的开发场景。
从发布时间线来看,仓颉语言的公开版本在 2024 年正式亮相,当前处于快速迭代期。API 12+ 阶段的仓颉已经具备较为完整的语言能力,包括泛型、模式匹配、空安全、异步编程等特性。
1.2 与 ArkTS 的关系
ArkTS 是 HarmonyOS 声明式 UI 的配套语言,构建在 ArkCompiler 之上,语法上与 TypeScript 有较强的血缘关系。它的核心优势在于与 ArkUI 框架的深度绑定——开发者使用 ArkTS 编写 UI 组件,框架负责渲染管线的高效调度。
仓颉则走了另一条路。它不依赖于 ArkUI,而是通过 @cj.ui 这一注解体系来声明 UI 结构。这意味着仓颉 UI 代码在编译阶段就能生成更接近原生指令的输出,理论上在性能敏感场景下有一定优势。
两者并非互斥。在同一个鸿蒙应用中,开发者可以同时使用 ArkTS 编写部分页面,用仓颉编写另一部分模块。但目前官方工具链对混合使用场景的支持仍在完善中。
1.3 应用场景
目前仓颉语言的主要应用场景包括:
- 控制台与工具类应用:轻量级脚本、数据处理工具、命令行工具。
- 声明式 UI 应用:通过
@cj.ui编写界面组件,构建完整的鸿蒙应用。 - 高性能模块:对运行时性能有较高要求的业务模块。
- 跨平台模块:仓颉语言在设计时考虑了跨平台兼容性,未来可扩展至非鸿蒙设备。
二、开发环境准备
2.1 安装 cjpm
仓颉语言的包管理与项目构建由 cjpm(Cangjie Package Manager)负责,类似于 Rust 的 Cargo 或 Node.js 的 npm。
安装方式非常简单,从仓颉官方 SDK 包中获取 cjpm 可执行文件,并将其路径加入系统 PATH 环境变量即可:
# 下载 SDK 包(请前往华为开发者联盟官网获取最新版)
# 将 cjpm.exe 所在目录添加到 PATH
$env:PATH += ";C:\path\to\cjpm"
cjpm --version
验证安装成功后会看到类似 cjpm 1.0.x 的版本输出。
2.2 创建第一个项目
使用 cjpm new 命令可以快速创建项目骨架:
cjpm new hello-cangjie --template console
cd hello-cangjie
项目结构如下:
hello-cangjie/
├── cjpm.toml # 项目配置清单
├── src/
│ └── main.cj # 入口文件
└── .cangjie/ # 编译产物目录(生成)
cjpm.toml 是仓颉项目的核心配置文件,定义了项目名称、版本、依赖等信息:
[package]
name = "hello-cangjie"
version = "0.1.0"
[dependencies]
可以看到,配置格式借鉴了 Rust 的 Cargo 风格,简洁清晰。
三、语法对比:变量声明、空安全与模式匹配
3.1 变量声明:let、var 与 ArkTS 的对照
ArkTS 使用 let 和 const 来声明变量,分别对应可变和不可变。仓颉语言同样支持这两个关键字,用法几乎相同:
// ArkTS
let name: String = "Alice"
const age: Int32 = 30
name = "Bob" // ✅ 可重新赋值
// 仓颉
let name: String = "Alice"
const age: Int32 = 30
name = "Bob" // ✅ 同样支持重新赋值
但仓颉引入了 var 关键字来声明可变变量,与 let 形成对比——let 声明的变量不可变,var 声明的变量可变:
let x: Int32 = 10
x = 20 // ❌ 编译错误:let 声明的变量不可变
var y: Int32 = 10
y = 20 // ✅ var 声明的变量可以重新赋值
这个设计让不可变语义的表达更加明确,不需要借助 const 的心智模型,降低了学习门槛。对于习惯了 JavaScript/TypeScript 的开发者来说,let 不可变的设定可能需要一点适应时间。
在类型推断方面,两者都支持省略类型标注,由编译器自动推导:
let message = "Hello Cangjie" // 编译器推断为 String
var count = 0 // 编译器推断为 Int32
3.2 空安全:?、! 与显式空检查
ArkTS 的空安全机制通过 ? 表示可空类型,通过 ! 进行非空断言:
// ArkTS
let name: string | null = "Alice"
let len: number = name!.length // 非空断言,若运行时 name 为 null 则崩溃
let upper = name?.toUpperCase() // 安全调用,为 null 时返回 undefined
仓颉的空安全设计与此一脉相承,但表达更为系统化。在仓颉中,每个引用类型都可以选择是否可空:
let name: String = "Alice" // 不可空 String
let nickname: String? = "Bob" // 可空 String,可赋值为 null
安全调用通过 ?. 实现,与 ArkTS 一致:
let upper = nickname?.toUpperCase() // 若 nickname 为 null,返回 null
显式非空检查使用 ?? 提供默认值,或者用 ! 断言:
let len: Int32 = nickname!.length // 运行时断言,与 ArkTS 相同
let display = nickname ?? "Guest" // 空合并运算符,nickname 为 null 时取 "Guest"
仓颉还引入了 match 表达式对可空类型进行解构处理,这是 ArkTS 中没有的原生语法,让空值处理更优雅:
let result: String? = getNickname()
match (result) {
Some(name) => println("Hello, ${name}")
None => println("No nickname found")
}
这里 match 的作用类似于 Rust 的 Option<T> 处理,将在下一节详细展开。
3.3 模式匹配:match 表达式
ArkTS 在较新版本中也引入了 match 表达式作为 switch 的增强替代,但仓颉的 match 语法更接近函数式语言的习惯用法,支持解构、卫语句(guard conditions)和多重模式。
基本的 match 用法:
let day: Int32 = 3
let label = match (day) {
1 => "Monday"
2 => "Tuesday"
3 => "Wednesday"
_ => "Unknown"
}
_ 是通配符模式,等同于 switch 中的 default。但 match 的威力远不止于此。
结构化模式匹配,可以直接解构元组和类:
let point = (10, 20)
match (point) {
(0, 0) => println("Origin")
(x, 0) => println("On X-axis: ${x}")
(0, y) => println("On Y-axis: ${y}")
(x, y) => println("Point(${x}, ${y})")
}
带卫语句的模式匹配,用 where 添加额外条件:
let score = 85
match (score) {
s where s >= 90 => println("Grade: A")
s where s >= 80 => println("Grade: B")
s where s >= 60 => println("Grade: C")
_ => println("Grade: F")
}
与 ArkTS 中 switch...case 的对比可以看出,仓颉的 match 表达式将值、模式和解构融为一体,写出的代码意图更清晰。ArkTS 的 switch 更偏向过程式风格,而仓颉的 match 则鼓励开发者用「模式」而非「流程」来思考问题。
四、函数与闭包
4.1 函数声明
仓颉用 fn 关键字声明函数,这一点与 ArkTS 用 function 或箭头函数形成鲜明对比。fn 的写法更接近 Rust 和 Kotlin 的风格:
fn add(a: Int32, b: Int32): Int32 {
return a + b
}
如果函数体是单个表达式,可以简写:
fn add(a: Int32, b: Int32): Int32 { a + b }
参数默认值与命名参数:
fn greet(name: String, prefix: String = "Hello"): String {
"${prefix}, ${name}!"
}
let msg = greet(name: "Alice", prefix: "Hi")
仓颉支持 命名参数调用,这让多参数函数的调用点可读性大大提升。ArkTS 本身不支持命名参数,需要通过 Options 对象来模拟类似效果。
4.2 闭包表达式
仓颉的闭包语法简洁有力,采用 {|参数| 表达式} 或 {|参数| { 语句 }} 的形式:
let square = {|x: Int32| x * x}
println(square(5)) // 输出 25
闭包作为高阶函数的参数使用:
let numbers = [1, 2, 3, 4, 5]
let evens = numbers.filter({|n| n % 2 == 0})
let doubled = numbers.map({|n| n * 2})
println(evens) // [2, 4]
println(doubled) // [2, 4, 6, 8, 10]
这种链式调用风格与 ArkTS 的数组方法链非常接近,上手几乎没有门槛。
五、声明式 UI:@cj.ui 的世界
5.1 从 ArkUI 到 @cj.ui
ArkTS 中,UI 通过 @Component 装饰器和 build() 方法组合构建。仓颉的 UI 层采用 @cj.ui 注解体系,概念上类似,但实现细节有所不同。
先看一个最简 UI 组件:
@cj.ui
struct HelloPage {
build() {
Column {
Text("Hello, Cangjie!")
.fontSize(24)
.fontColor("#333333")
}
.alignItems(HorizontalAlign.Center)
.padding(16)
}
}

ArkTS 开发者应该对这个结构非常熟悉——同样有 Column、Text 等基础组件,同样采用链式调用设置属性。但 struct 配合注解的写法,比 ArkTS 的 class 更接近数据结构的本意。
5.2 状态管理
仓颉 UI 的状态管理围绕 @State、@Prop 和 @Link 等注解展开,与 ArkTS 的响应式设计一脉相承:
@cj.ui
struct CounterPage {
@State
count: Int32 = 0
build() {
Column {
Text("Count: ${this.count}")
.fontSize(32)
Button("Increment")
.onClick({|_| this.count += 1 })
}
.alignItems(HorizontalAlign.Center)
.padding(20)
}
}

@State 注解标记的状态变化会自动触发 UI 重新渲染。onClick 中的闭包 {|_| ...} 使用下划线 _ 表示省略参数,这与 Rust 的习惯一致——不需要参数时就不要参数名。
5.3 条件渲染与循环渲染
条件渲染使用 if 表达式而非 JSX 中的三元运算符,这体现了仓颉语言「表达式优先」的设计理念:
@cj.ui
struct ToggleView {
@State
showDetail: Boolean = false
build() {
Column {
Button("Toggle")
.onClick({|_|
this.showDetail = !this.showDetail
})
if (this.showDetail) {
Text("Here is the detail content.")
.fontSize(18)
.fontColor("#666666")
}
}
}
}
循环渲染使用 ForEach,语法与 ArkTS 基本一致:
@cj.ui
struct ListView {
let items: Array<String> = ["Apple", "Banana", "Cherry"]
build() {
Column {
ForEach(items, {|item: String|
Text(item)
.fontSize(20)
.padding(8)
})
}
}
}
整体而言,仓颉的 UI 体系与 ArkUI 在概念上高度对齐——组件树、属性链、状态驱动渲染的核心模型没有变化。差异主要体现在语法细节上:struct 替代 class、闭包写法不同、if 表达式替代三元运算符等。
六、编译与运行
6.1 编译流程
仓颉的编译流程分为两个主要阶段:前端编译 和 后端编译。
前端阶段将 .cj 源文件编译为中间表示(IR),进行语法分析、类型检查和语义优化。这一阶段可以捕获所有静态类型错误,包括空安全违规、类型不匹配等。
使用 cjpm build 触发完整编译:
cjpm build
编译产物默认输出到 .cangjie/target/ 目录。如果是控制台程序,会生成可直接运行的二进制文件。
6.2 运行与调试
运行控制台程序:
cjpm run
调试方面,仓颉 SDK 提供了基础的调试能力,支持断点设置和变量检查:
cjpm debug
这个命令会启动一个集成调试会话,开发者可以在关键代码行设置断点,逐步观察变量值的变化。对于初学者来说,调试工具的可用性已经足够应付日常学习和简单项目的排错需求。
6.3 依赖管理
在 cjpm.toml 中声明依赖后,运行 cjpm update 会自动拉取依赖包:
[package]
name = "demo"
version = "0.1.0"
[dependencies]
utils = "1.2.3"
http = "0.4.0"
仓颉的生态尚处于建设期,第三方包的丰富程度不及 npm 或 crates.io,但基础设施已经完备。随着社区参与度的提升,预计包生态会逐步繁荣。
七、实战示例:从零构建一个记事本应用
本节用一个完整的轻量级示例串联全文内容。项目包含两个部分:一个纯逻辑模块(记事本数据管理)和一个简单 UI 界面。
7.1 数据模型
首先定义一条笔记的数据结构:
struct Note {
let id: Int32
let title: String
let content: String
let createdAt: Int64 // 时间戳
}
let notes: Array<Note> = []
fn addNote(title: String, content: String): Note {
let newId = notes.size() + 1
let timestamp = currentTimeMillis()
let note = Note(id: newId, title: title, content: content, createdAt: timestamp)
notes.append(note)
return note
}
currentTimeMillis() 是仓颉标准库提供的工具函数,用于获取当前时间戳。构造 Note 实例时使用了命名参数,语义清晰。
7.2 控制台交互入口
写一个简单的命令行交互程序,演示 match 表达式和循环渲染到控制台输出的能力:
fn main() {
addNote("Meeting Notes", "Discuss Q3 roadmap")
addNote("Shopping List", "Milk, Bread, Eggs")
println("Your notes:")
for (i in 0..notes.size()) {
let note = notes[i]
println("[${note.id}] ${note.title}")
}
// 用 match 处理搜索结果
let searchResult = notes.find({|n| n.title == "Meeting Notes"})
match (searchResult) {
Some(note) => println("Found: ${note.content}")
None => println("Note not found")
}
}
这里展示了仓颉的几个实用特性:for (i in 0..n) 的范围循环语法、Array.find 配合闭包过滤、以及 match 对 Option 类型的处理。
7.3 简单 UI 组件
最后,写一个展示笔记列表的 UI 页面,完整演示 @cj.ui 的用法:
@cj.ui
struct NoteListPage {
@State
notes: Array<Note> = []
@State
selectedId: Int32? = null
fn loadNotes() {
this.notes = [
Note(id: 1, title: "Meeting Notes", content: "Q3 roadmap discussion", createdAt: 0),
Note(id: 2, title: "Shopping List", content: "Milk, Bread, Eggs", createdAt: 0)
]
}
build() {
Column {
Text("My Notes")
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin(bottom: 16)
if (this.notes.isEmpty()) {
Text("No notes yet.")
.fontSize(16)
.fontColor("#999999")
} else {
ForEach(this.notes, {|note: Note|
NoteCard(note: note, isSelected: this.selectedId == note.id)
.onClick({|_| this.selectedId = note.id })
})
}
}
.padding(16)
.onAppear({|_| this.loadNotes()})
}
}
@cj.ui
struct NoteCard {
let note: Note
let isSelected: Boolean
build() {
Column {
Text(this.note.title)
.fontSize(18)
.fontWeight(FontWeight.Medium)
Text(this.note.content)
.fontSize(14)
.fontColor("#666666")
.margin(top: 4)
}
.padding(12)
.backgroundColor(this.isSelected ? "#E8F4FD" : "#F5F5F5")
.borderRadius(8)
.margin(bottom: 8)
}
}
这个示例覆盖了 @State 状态管理、ForEach 循环渲染、条件渲染、onAppear 生命周期回调,以及组件间的属性传递(NoteCard 接收 note 和 isSelected 两个 prop)。代码行数控制在合理范围内,核心概念一目了然。
八、总结与展望
8.1 核心差异回顾
回顾全文,仓颉语言与 ArkTS 的核心差异可以归纳为以下几点:
| 维度 | ArkTS | 仓颉语言 |
|---|---|---|
| 关键字 | function, const, let |
fn, let, var |
| 类型推断 | 强 | 强 |
| 空安全 | ? + ! + ?. |
相同体系,增加 match 解构 |
| 模式匹配 | 基础 switch 增强 |
强大的 match 表达式 |
| 函数语法 | 函数表达式/箭头函数 | fn 关键字 + 闭包 `{|_ |
| UI 框架 | ArkUI(@Component) |
@cj.ui 注解体系 |
| 包管理 | ohpm | cjpm |
8.2 学习建议
对于 ArkTS 开发者来说,学习仓颉的路径其实相当平坦。两者在声明式 UI 的核心理念、状态驱动的渲染模型上高度一致,真正的差异集中在语法表象和语言特性层面。建议按以下顺序推进:
首先,熟悉 fn、let/var 的语义差异——这是最表层的变化。其次,掌握 match 表达式,它会重塑你处理条件逻辑的方式。最后,尝试用 @cj.ui 编写一个完整页面,感受语法差异带来的编码节奏变化。
8.3 生态展望
仓颉语言目前仍处于生态建设的早期阶段。cjpm 的包生态、IDE 插件的成熟度、调试体验的优化空间还很大。但语言设计本身已经展现出了不错的工程成熟度——类型系统扎实、空安全到位、语法简洁而不简陋。
随着 HarmonyOS NEXT 的推进和仓颉语言的持续迭代,它有望成为鸿蒙生态中与 ArkTS 并驾齐驱的核心开发语言。对于有技术敏感度的开发者而言,现在正是提前布局、深入探索的合适时机。
基于 HarmonyOS NEXT(API 12+)
更多推荐


所有评论(0)