作者:京东科技 杨拓
一、仓颉介绍
仓颉是一种通用编程语言,适用于各种应用开发,兼顾开发效率和运行性能,其优势如下:
1、高效编程:语法简明高效,支持多种范式编程。如插值字符串、主构造函数、Flow 表达式、match、if-let、while-let 和重导出等,减少冗余书写,提升开发效率。
2、轻量运行时库:标准库中除 core 包外(libcangjie-std-core < 1MB),其他包都可以按需链接加载。
3、轻量用户线程:仓颉使用用户态线程模型,每个线程都是轻量级的执行实体,拥有独立的执行上下文并且共享内存。创建线程内存开销仅 8KB,切换线程不进入内核态,线程栈支持动态扩缩容,耗时纳秒级。
4、轻量对象:每对象仅额外消耗一个机器字长记录类型信息;不需要计数字段;GC 使用转发表而非转发指针。
5、类型安全:作为静态强类型语言,仓颉通过编译时类型检查识别错误,降低运行时风险,且具备强大的类型推断能力,减少类型标注工作。
二、快速环境配置
1、下载
在鸿蒙开发者网站找到下载中心,可以看到DevEco仓颉插件,下载到本地。
最新版下载链接如下:点击此处下载最新版
2、安装
打开DevEco Studio,进入设置,在plugin标签下选择从本地磁盘安装插件,安装后重启IDE。
3、创建
操作与创建ArkTs工程一致。
三、特性介绍
1、Flow 表达式
Flow流表示对输入进行流式处理后得到输出,在仓颉中流操作符有两种:表示数据流向的中缀操作符 |> 和表示函数组合的中缀操作符 ~> 。
1.1、|>
表示对输入数据做一系列的处理,可以使用 |> 来简化。
语法形式就是e1 |> e2,其中 e2 是函数类型的表达式,e1 的类型是 e2 的参数类型的子类型.
示例1:
let gen: Array<Int64> = []
gen |> forEach{a: Int64 => println("${a}")}
示例2:
func split(words: Array<String>, separator: Rune): Array<String> {
words |> map { text =>
text.split(String(separator), removeEmpty: true)
} |> flatten |> collectArray
}
//示例3: 处理 Unicode 字符串
func split_unicode(text: String, sep: String) {
let indices = ArrayList<Int64>()
text.runes() |> enumerate |> fold(false) { state, e =>
let current = !sep.contains(e[1].toString())
if (state != current) { indices.append(e[0]) }
current
} |> { valid: Bool => if (valid) { indices.append(text.size) } }
let runes = text.toRuneArray()
let result = ArrayList<String>()
for (i in 0..indices.size:2) {
result.append(String(runes[indices[i]..indices[i + 1]]))
}
return result
}
1.2、~>
当两个单参函数进行组合时可以使用~>。
//示例:
func f(x: Int64): Float64 {
Float64(x)
}
func g(x: Float64): Float64 {
x
}
//等价于{ x: Int64 => g(f(x)) },会先对f求值,然后再对g求值,最后才会进行函数的组合
var fg = f ~> g
建议以上两个流操作符不要用于带默认值的命名参数,因为配置了默认值的命名参数必须给出命名实参才能用
2、变长参数
变长参数是各个语言都有的一种功能,但是在仓颉中当形参最后一个非命名参数是 Array 类型时,就可以作为变长参数使用,不需要特殊的语法形式
func sum(arr: Array<Int64>) {
var total = 0
for (x in arr) {
total += x
}
return total
}
main() {
println(sum())
println(sum(1, 2, 3))
}
3、扩展
仓颉扩展提供了对类和接口进行扩展的操作,不需要使用继承或者装饰器模式,就可以给类添加额外的属性和方法。这种场景的优势在于不需要破坏被扩展类型的封装性,就可以添加额外的功能。
可以添加的功能包括:
•添加成员函数
•添加操作符重载函数
•添加成员属性
•实现接口
//示例1:为整型扩展两个属性
extend Int64 {
public prop r: Index {
get() { Index.Row(this) }
}
public prop c: Index {
get() { Index.Col(this) }
}
}
//调用
2.r
//示例2:扩展静态方法
extend Expression {
static func fromTokens(tokens: List<Token>): Result<Expression, String> {
match (expressionFunc(tokens).map {t => t[0]}) {
case Some(e) => Ok(e)
case None => Err("Invalid Expression!")
}
}
}
//调用
Expression.fromTokens
4、if-let和while-let
仓颉中的条件控制和循环控制基本和ArkTs一致,除了将switch换为match外无区别,但是多了if-let和while-let两个特性表达式。
4.1、if-let
if-let 表达式首先会对条件中 <- 右侧的表达式进行求值,如果此值能匹配 <- 左侧的模式,则执行 if 分支,否则执行 else 分支。
main() {
let result = Option<Int64>.Some(2023)
if (let Some(value) <- result) {
println("操作成功,返回值为:${value}")
} else {
println("操作失败")
}
}
操作成功,返回值为:2023
仓颉没有null判空,所以提供了option(T)来判空,它里面的取值就是some(v)和none两种情况None为空,表示没有赋值。some表示有赋值
4.2、while-let
while-let 表达式同if-let一样,也是先会对条件中 <- 右侧的表达式进行求值,如果此值能匹配 <- 左侧的模式,则执行循环体,然后重复执行此过程。如果模式匹配失败,则结束循环。
public static func fromJson(r: JsonReader): FunctionCall {
var temp_name: String = "";
var temp_arguments: String = "";
while (let Some(v) <- r.peek()) {
match(v) {
case BeginObject =>
r.startObject();
while(r.peek() != EndObject) {
let n = r.readName()
match (n) {
case "name" => temp_name = r.readValue<String>();
case "arguments" => temp_arguments = r.readValue<String>();
case unkow => println("unkow key ${unkow}");
}
}
r.endObject();
break;
case _ => throw Exception("can't deserialize for FunctionCall");
}
}
return FunctionCall(temp_name, temp_arguments);
}
5、线程
仓颉中创建一个线程非常简单,只需要使用 spawn{} 即可开启一个新的线程,{}里面就是在新线程中执行的代码,并且可以使用Future
import std.sync.*
import std.time.*
//开启一个线程
let future = spawn { task() }
//执行内容
func task(): Int64 {
for (_ in 0..M) {
.....
}
return n
}
//使用fut.get()可以等待线程执行完成获取线程执行结果
future.get()
5.1、同步-原子操作
仓颉同样也提供了多种同步机制来确保数据的安全,例如原子操作、互斥锁和条件变量等
原子操作方面仓颉提供整数类型、AtomicBool 类型和AtomicReference引用类型的原子操作来保证同步操作。
let num = AtomicInt64(0);
let list = ArrayList<Future<Int64>>();
func testAtomic() {
for (i in 0..100) {
let fut = spawn {
sleep(Duration.millisecond)
num.fetchAdd(1)
}
list.append(fut)
}
for (f in list) {
f.get()
}
let result = num.load()
AppLog.info("result = ${result}")//输出100
}
5.2、同步-可重入互斥锁 ReentrantMutex
可重入互斥锁可以保证在任意时刻最多只有一个线程执行区块的代码,当一个线程尝试获取被其他线程持有的锁会被阻塞,直到别的线程释放锁才可以执行区块代码。使用ReentrantMutex需要注意以下两点:
1.在访问共享数据之前,必须尝试获取锁。
2.处理完共享数据后,必须进行解锁,以便其他线程可以获得锁。
import std.sync.*
import std.time.*
import std.collection.*
var num: Int64 = 0;
let list = ArrayList<Future<Int64>>();
let lock = ReentrantMutex()
func task() {
sleep(Duration.millisecond)
lock.lock()
num++
lock.unlock()
}
func testMutex() {
let list = ArrayList<Future<Unit>>()
for (i in 0..100) {
let fut = spawn {task()}
list.append(fut)
}
for (f in list) {
f.get()
}
AppLog.info("result = ${num}") //输出100
}
在日常使用中需要手动unlock相当不方便,而且也有可能在异常情况下锁无法释放的问题,为了解决这些问题,仓颉又提供一个 synchronized 关键字,搭配 ReentrantMutex 一起使用。
具体使用方式就是在 synchronized 上加一个 ReentrantMutex 对象即可,然后将同步代码写在synchronized{}中。一个线程在进入 synchronized 修饰的代码块之前,会自动获取 ReentrantMutex 实例对应的锁,如果无法获取锁,则当前线程被阻塞;而线程在退出 synchronized 修饰的代码块之前,会自动释放该 ReentrantMutex 实例的锁。
import std.sync.*
import std.time.*
import std.collection.*
var num: Int64 = 0;
let list = ArrayList<Future<Int64>>();
let lock = ReentrantMutex()
func task() {
sleep(Duration.millisecond)
// lock.lock()
//跟上面的示例一样,省去了加,释放锁的操作
synchronized(lock) {
num++
}
// lock.unlock()
}
func testMutex() {
let list = ArrayList<Future<Unit>>()
for (i in 0..100) {
let fut = spawn {task()}
list.append(fut)
}
for (f in list) {
f.get()
}
AppLog.info("result = ${num}") //输出100
}
四、和ArkTs互操作
现在一般的场景是在已有ArkTs库中使用仓颉,所以可以将仓颉代码封装为ArkTs库,提供给外部使用。
原理就是互操作宏解析被注解修饰的仓颉代码,会自动生成ArkTs声明文件和互操作层代码。
使用步骤:
1.在cj文件中,针对 class、interface 和函数,使用 @Interop[ArkTS] 进行修饰,被修饰的对象是希望被 ArkTS 调用的。
2.在 DevEco Studio 中的仓颉文件或者 module 名称右键选择“Generate Cangjie-ArkTS Interop API”,会在 cangjie 目录下生成 ark_interop_api 的声明文件。
3.ArkTS 侧添加依赖并 import ark_interop_api 即可使用。
仓颉代码:
import ohos.ark_interop.*
import ohos.ark_interop_macro.*
@Interop[ArkTS]
public func sub(a: Int64, b: Int64): Int64 {
return a - b
}
@Interop[ArkTS]
public class CjDemo {
public let name: String
@Interop[ArkTS, Invisible]
public var id: Float64 = 1.0
public init(str: String) {
name = str
}
public func add(a: Int64, b: Int64): Int64 {
return a + b
}
public func foo(): Float64 {
return 1.0
}
}
生成的代码:
export declare class CjDemo {
name: string
add(a: number, b: number): number
foo(): number
}
export declare interface CustomLib {
sub(a: number, b: number): number
CjDemo: {new (str: string): CjDemo}
}
使用:
let cjLib : CustomLib = requireCJLib("libohos_app_cangjie_entry.so") as CustomLib
console.log("result" + cjLib.sub(2, 1))
let class1: CjDemo = new cjLib.CjDemo("arkts call")
console.log("result " + class1.add(5,1))
五、后续规划
鸿蒙应用开发官方目前提供两种编程语言供选择:ArkTs和仓颉。从当前趋势来看,这两种语言是并行发展的,尚不存在某一方被替代的情况,因此对当前的开发工作没有影响,仓颉可以作为技术储备加以学习和掌握。
在开发初期,我们全部使用了ArkTs。然而在实际开发过程中,我们发现了一些痛点:
某些ArkTs的官方API存在性能问题,使得我们在进行性能优化时某些关键点较依赖系统发版。
ArkTs提供了TaskPool和Worker两种线程调用方式,但编写过程较为繁琐,线程间的数据传递存在限制且有性能损耗。
我们计划利用仓颉的优势来解决这些问题,以打造更为健壮的鸿蒙版京东金融应用。