《京东金融APP的鸿蒙之旅系列专题》鸿蒙新特性篇:Hello, 仓颉 World

京东云开发者
• 阅读 139

作者:京东科技 杨拓

一、仓颉介绍

仓颉是一种通用编程语言,适用于各种应用开发,兼顾开发效率和运行性能,其优势如下:

1、高效编程:语法简明高效,支持多种范式编程。如插值字符串、主构造函数、Flow 表达式、match、if-let、while-let 和重导出等,减少冗余书写,提升开发效率。

2、轻量运行时库:标准库中除 core 包外(libcangjie-std-core < 1MB),其他包都可以按需链接加载。

3、轻量用户线程:仓颉使用用户态线程模型,每个线程都是轻量级的执行实体,拥有独立的执行上下文并且共享内存。创建线程内存开销仅 8KB,切换线程不进入内核态,线程栈支持动态扩缩容,耗时纳秒级。

4、轻量对象:每对象仅额外消耗一个机器字长记录类型信息;不需要计数字段;GC 使用转发表而非转发指针。

5、类型安全:作为静态强类型语言,仓颉通过编译时类型检查识别错误,降低运行时风险,且具备强大的类型推断能力,减少类型标注工作。

二、快速环境配置

1、下载

在鸿蒙开发者网站找到下载中心,可以看到DevEco仓颉插件,下载到本地。 《京东金融APP的鸿蒙之旅系列专题》鸿蒙新特性篇:Hello, 仓颉 World

最新版下载链接如下:点击此处下载最新版

2、安装

打开DevEco Studio,进入设置,在plugin标签下选择从本地磁盘安装插件,安装后重启IDE。

《京东金融APP的鸿蒙之旅系列专题》鸿蒙新特性篇:Hello, 仓颉 World

3、创建

操作与创建ArkTs工程一致。

《京东金融APP的鸿蒙之旅系列专题》鸿蒙新特性篇:Hello, 仓颉 World

三、特性介绍

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。然而在实际开发过程中,我们发现了一些痛点:

  1. 某些ArkTs的官方API存在性能问题,使得我们在进行性能优化时某些关键点较依赖系统发版。

  2. ArkTs提供了TaskPool和Worker两种线程调用方式,但编写过程较为繁琐,线程间的数据传递存在限制且有性能损耗。

我们计划利用仓颉的优势来解决这些问题,以打造更为健壮的鸿蒙版京东金融应用。

点赞
收藏
评论区
推荐文章
【Rust学习】内存安全探秘:变量的所有权、引用与借用
Rust语言由Mozilla开发,最早发布于2014年9月,是一种高效、可靠的通用高级语言。其高效不仅限于开发效率,它的执行效率也是令人称赞的,是一种少有的兼顾开发效率和执行效率的语言。
京东云开发者 京东云开发者
4个月前
动态化-鸿蒙跨端方案介绍
一、背景👉华为在2023.9.25官方发布会上宣布,新的鸿蒙系统将不再兼容安卓应用,这意味着,包括京东金融APP在内的所有安卓应用,在新的鸿蒙系统上将无法运行,需要重新开发专门适用于新鸿蒙系统的专版APP。二、原生适配方案原生适配方案就是将京东金融APP
京东云开发者 京东云开发者
2个月前
《京东金融APP的鸿蒙之旅系列专题》鸿蒙工程化:Hvigor构建技术
作者:京东科技杨拓一、构建工具概述Hvigor构建工具是一款基于TypeScript实现的构建任务编排工具,专为提升构建和测试应用的效率而设计。它主要提供以下关键功能:1.任务管理机制:包括任务注册和编排,帮助开发者高效地管理和执行构建任务。2.工程模型管
京东云开发者 京东云开发者
2个月前
Taro 鸿蒙技术内幕系列(一):如何将 React 代码跑在 ArkUI 上
作者:京东零售朱鸣辉基于Taro打造的京东鸿蒙APP已跟随鸿蒙Next系统公测,本系列文章将深入解析Taro如何实现使用React开发高性能鸿蒙应用的技术内幕背景随着鸿蒙操作系统的快速发展,开发者们期待将现有跨平台应用迁移到鸿蒙平台。Taro作为一个流行的
京东云开发者 京东云开发者
1个月前
Taro 鸿蒙技术内幕系列(二):如何让 W3C 标准的 CSS跑在鸿蒙上
作者:京东零售马银涛基于Taro打造的京东鸿蒙APP已跟随鸿蒙Next系统公测,本系列文章将深入解析Taro如何实现使用React开发高性能鸿蒙应用的技术内幕背景HarmonyOS采用自研的ArkUI框架作为原生UI开发方案,这套方案有完善的布局系统和样式
京东云开发者 京东云开发者
3星期前
Taro 鸿蒙技术内幕系列(三) - 多语言场景下的通用事件系统设计
作者:京东零售朱天健基于Taro打造的京东鸿蒙APP已跟随鸿蒙Next系统公测,本系列文章将深入解析Taro如何实现使用React开发高性能鸿蒙应用的技术内幕背景在鸿蒙生态系统中,虽然原生应用通常基于ArkTS实现,但在实际研发过程中发现,使用C可以显
京东云开发者 京东云开发者
3星期前
Taro 鸿蒙技术内幕系列(四):JDImage 自研鸿蒙图片库
作者:京东零售何骁基于Taro打造的京东鸿蒙APP已跟随鸿蒙Next系统公测,本系列文章将深入解析Taro如何实现使用React开发高性能鸿蒙应用的技术内幕背景2024年初,京东正式启动了鸿蒙APP的开发工作。由于电商APP大量依赖图片来展示商品信息,对图