在这里插入图片描述

引言

在 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 使用 letconst 来声明变量,分别对应可变和不可变。仓颉语言同样支持这两个关键字,用法几乎相同:

// 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 接收 noteisSelected 两个 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 的核心理念、状态驱动的渲染模型上高度一致,真正的差异集中在语法表象和语言特性层面。建议按以下顺序推进:

首先,熟悉 fnlet/var 的语义差异——这是最表层的变化。其次,掌握 match 表达式,它会重塑你处理条件逻辑的方式。最后,尝试用 @cj.ui 编写一个完整页面,感受语法差异带来的编码节奏变化。

8.3 生态展望

仓颉语言目前仍处于生态建设的早期阶段。cjpm 的包生态、IDE 插件的成熟度、调试体验的优化空间还很大。但语言设计本身已经展现出了不错的工程成熟度——类型系统扎实、空安全到位、语法简洁而不简陋。

随着 HarmonyOS NEXT 的推进和仓颉语言的持续迭代,它有望成为鸿蒙生态中与 ArkTS 并驾齐驱的核心开发语言。对于有技术敏感度的开发者而言,现在正是提前布局、深入探索的合适时机。


基于 HarmonyOS NEXT(API 12+)

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐