概述

仓颉是一帮子rust开发者,吸收了rust的语法,参考kotlin, golang, typescript 而做出的糅合怪!它使用方舟编译器,以llvm为后端,开发的一个国产编程语言。
因为吸取了很多编程语言的优点,所以体验不错,但是融合的稍显生硬。
又因为作为鸿蒙操作系统底层语言,所以标准库很完善,使用相对方便。和golang类似,简单的功能开发,几乎可以不依赖第三方库。

网络间流传了一张图来表示:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

基础语法

标识符

标识符分为普通标识符和原始标识符两类,它们分别遵从不同的命名规则。

// 合法的
abc
_abc
abc_
a1b2c3
a_b_c
a1_b2_c3
仓颉
__こんにちは

// 不合法的
ab&c  // 使用了非法字符 “&”
3abc  // 数字不能出现在头部
while // 不能使用仓颉关键字

// 原始标识符
`abc`
`_abc`
`a1b2c3`
`if`
`while`
`à֮̅̕b`

// 不合法的原始标识符
`ab&c`
`3abc`

变量

变量定义的具体形式为: 修饰符 变量名: 变量类型 = 初始值

其中修饰符用于设置变量的各类属性,可以有一个或多个,常用的修饰符包括:

  • 可变性修饰符:letvar,分别对应不可变和可变属性,可变性决定了变量被初始化后其值还能否改变,仓颉变量也由此分为不可变变量和可变变量两类。let 修饰的变量只能被赋值一次,即初始化,var 修饰的变量可以被多次赋值。
  • 可见性修饰符:privatepublic 等,影响全局变量和成员变量的可引用范围,详见后续章节的相关介绍。
  • 静态性修饰符:static,影响成员变量的存储和引用方式,详见后续章节的相关介绍。

在定义仓颉变量时,可变性修饰符是必要的,在此基础上,还可以根据需要添加其他修饰符。

  • 变量名应是一个合法的仓颉标识符。
  • 变量类型指定了变量所持有数据的类型。当初始值具有明确类型时,可以省略变量类型标注,此时编译器可以自动推断出变量类型。
  • 初始值是一个仓颉表达式,用于初始化变量,如果标注了变量类型,需要保证初始值类型和变量类型一致。在定义全局变量或静态成员变量时,必须指定初始值。在定义局部变量或实例成员变量时,可以省略初始值,但需要标注变量类型,同时要在此变量被引用前完成初始化,否则编译会报错。
main() {
    let a: Int64 = 20
    var b = 12
    b = 23
    println("${a}${b}")
}

在仓颉编程语言中,class 和 Array 等类型属于引用类型,其他基础数据类型和 struct 等类型属于值类型。
值类型变量通常在线程栈上分配,每个变量都有自己的数据副本;引用类型变量通常在进程堆中分配,多个变量可引用同一数据对象,对一个变量执行的操作可能会影响其他变量。

  1. 在给值类型变量赋值时,一般会产生拷贝操作,且原来绑定的数据/存储空间被覆写。在给引用类型变量赋值时,只是改变了引用关系,原来绑定的数据/存储空间不会被覆写。
  2. let 定义的变量,要求变量被初始化后都不能再赋值。对于引用类型,这只是限定了引用关系不可改变,但是所引用的数据是可以被修改的。

数据类型

整数类型分为有符号(signed)整数类型和无符号(unsigned)整数类型。

有符号整数类型包括 Int8Int16Int32Int64IntNative,分别用于表示编码长度为 8-bit16-bit32-bit64-bit 和平台相关大小的有符号整数值的类型。

无符号整数类型包括 UInt8UInt16UInt32UInt64UIntNative,分别用于表示编码长度为 8-bit16-bit32-bit64-bit 和平台相关大小的无符号整数值的类型。

整数类型字面量有 4 种进制表示形式:

  • 二进制(使用 0b 或 0B 前缀)
  • 八进制(使用 0o 或 0O 前缀)
  • 十进制(没有前缀)
  • 十六进制(使用 0x 或 0X 前缀)

在使用整数类型字面量时,可以通过加入后缀来明确整数字面量的类型,后缀与类型的对应为:

后缀 类型 后缀 类型
i8 Int8 u8 UInt8
i16 Int16 u16 UInt16
i32 Int32 u32 UInt32
i64 Int64 u64 UInt64
var x = 100i8  // x is 100 with type Int8
var y = 0x10u64 // y is 16 with type UInt64
var z = 0o432i32  // z is 282 with type Int32

运算符

  • 算术操作符包括:一元负号(-)、加法(+)、减法(-)、乘法(*)、除法(/)、取模(%)、幂运算(**)。
    • 除了一元负号(-)和幂运算(**),其他操作符要求左右操作数是相同的类型。
    • */+- 的操作数可以是整数类型或浮点类型。
    • % 的操作数只支持整数类型。
    • ** 的左操作数只能为 Int64 类型或 Float64 类型,并且:
      • 当左操作数类型为 Int64 时,右操作数只能为 UInt64 类型,表达式的类型为 Int64
      • 当左操作数类型为 Float64 时,右操作数只能为 Int64 类型或 Float64 类型,表达式的类型为 Float64
  • 位操作符包括:按位求反(!)、左移(<<)、右移(>>)、按位与(&)、按位异或(^)、按位或(|)。注意,按位与、按位异或和按位或操作符要求左右操作数是相同的整数类型。
  • 关系操作符包括:小于(<)、大于(>)、小于等于(<=)、大于等于(>=)、相等(==)、不等(!=)。要求关系操作符的左右操作数是相同的整数类型。
  • 自增和自减操作符包括:自增(++)和自减(--)。注意,仓颉中的自增和自减操作符只能作为一元后缀操作符使用。
  • 复合赋值操作符包括:+=-=*=/=%=**=<<=>>=&=^=|=

浮点类型

浮点类型包括 Float16Float32Float64,分别用于表示编码长度为 16-bit32-bit64-bit 的浮点数(带小数部分的数字,如 3.14159、8.24 和 0.1 等)的类型。

后缀分别对应f16, f32, f64

布尔类型

使用 Bool 表示,用来表示逻辑中的真和假。布尔类型只有两个字面量:truefalse

字符字节字面量

字符字节字面量,以方便使用 ASCII 码表示 UInt8 类型的值。字符字节字面量由字符 b、一对标识首尾的单引号、以及一个 ASCII 字符组成,例如:

var a = b'x' // a is 120 with type UInt8
var b = b'\n' // b is 10 with type UInt8
var c = b'\u{78}' // c is 120 with type UInt8

字符类型

使用 Rune 表示,可以表示 Unicode 字符集中的所有字符。
字符类型字面量有三种形式:单个字符、转义字符和通用字符。一个 Rune 字面量由字符 r 开头,后跟一个由一对单引号或双引号包含的字符。

// 单个字符
let a: Rune = r'a'
let b: Rune = r"b"

// 转义字符
let slash: Rune = r'\\'
let newLine: Rune = r'\n'
let tab: Rune = r'\t'

// 通用字符
let he: Rune = r'\u{4f60}'
let llo: Rune = r'\u{597d}'

字符串类型

使用 String 表示,用于表达文本数据,由一串 Unicode 字符组合而成。
字符串字面量分为三类:单行字符串字面量,多行字符串字面量,多行原始字符串字面量。

// 单行字符串字面量
let s1: String = ""
let s2 = 'Hello Cangjie Lang'
let s3 = "\"Hello Cangjie Lang\""
let s4 = 'Hello Cangjie Lang\n'

// 多行字符串字面量
let s1: String = """
    """
let s2 = '''
    Hello,
    Cangjie Lang'''

// 多行原始字符串字面量
let s1: String = #""#
let s2 = ##'\n'##
let s3 = ###"
    Hello,
    Cangjie
    Lang"###

插值字符串

插值表达式必须用花括号 {} 包起来,并在 {} 之前加上 $ 前缀。{} 中可以包含一个或者多个声明或表达式。

main() {
    let fruit = "apples"
    let count = 10
    let s = "There are ${count * count} ${fruit}"
    println(s)

    let r = 2.4
    let area = "The area of a circle with radius ${r} is ${let PI = 3.141592; PI * r * r}"
    println(area)
}

元组(Tuple)

可以将多个不同的类型组合在一起,成为一个新的类型。
元组至少是二元,例如,(Int64, Float64) 表示一个二元组类型,(Int64, Float64, String) 表示一个三元组类型。
元组类型是不可变类型,即一旦定义了一个元组类型的实例,它的内容不能再被更新。

let x: (Int64, Float64) = (3, 3.141592)
let y: (Int64, Float64, String) = (3, 3.141592, "PI")

元组支持通过 t[index] 的方式访问某个具体位置的元素,其中 t 是一个元组,index 是下标,并且 index 只能是从 0 开始且小于元组元素个数的整数类型字面量,否则,编译报错。

var a: Int64
var b: String
var c: Unit
func f() { ((1, "abc"), ()) }
((a, b), c) = f() // value of a is 1, value of b is "abc", value of c is '()'
((a, b), _) = ((2, "def"), 3.0) // value of a is 2, value of b is "def", 3.0 is ignored

元组作为参数:

// ok
func getFruitPrice (): (name: String, price: Int64) {
    return ("banana", 10)
}

let c: (name: String, Int64) = ("banana", 5)   // Error

Array数组

使用 Array<T> 来表示 Array 类型。T 表示 Array 的元素类型,T 可以是任意类型。

var a: Array<Int64> = ... // Array whose element type is Int64
var b: Array<String> = ... // Array whose element type is String

let a: Array<String> = [] // Created an empty Array whose element type is String
let b = [1, 2, 3, 3, 2, 1] // Created a Array whose element type is Int64, containing elements 1, 2, 3, 3, 2, 1

let a = Array<Int64>() // Created an empty Array whose element type is Int64
let c = Array<Int64>(3, repeat: 0) // Created an Array whose element type is Int64, length is 3 and all elements are initialized as 0
let d = Array<Int64>(3, {i => i + 1}) // Created an Array whose element type is Int64, length is 3 and all elements are initialized by the initialization function

需要注意的是,当通过 repeat 指定的初始值初始化 Array 时,该构造函数不会拷贝 repeat,如果 repeat 是一个引用类型,构造后数组的每一个元素都将指向相同的引用

注意: Array 是引用类型,因此 Array 在作为表达式使用时不会拷贝副本,同一个 Array 实例的所有引用都会共享同样的数据。

let arr1 = [0, 1, 2, 3, 4, 5, 6]
let arr2 = arr1[0..5] // arr2 contains the elements 0, 1, 2, 3, 4
let arr3 = arr1[..3]  // arr3 contains elements 0, 1, 2
let arr4 = arr1[2..]  // arr4 contains elements 2, 3, 4, 5, 6
println("${arr1.size}")

let arr1 = [0, 1, 2]
let arr2 = arr1
arr2[0] = 3
// arr1 contains elements 3, 1, 2
// arr2 contains elements 3, 1, 2

VArray 值数组

除了引用类型的数组 Array,仓颉还引入了值类型数组 VArray<T, $N> ,其中 T 表示该值类型数组的元素类型,$N 是一个固定的语法,通过 $ 加上一个 Int64 类型的数值字面量表示这个值类型数组的长度。

与频繁使用引用类型 Array 相比,使用值类型 VArray 可以减少堆上内存分配和垃圾回收的压力。但是需要注意的是,由于值类型本身在传递和赋值时的拷贝,会产生额外的性能开销,因此建议不要在性能敏感场景使用较大长度的 VArray

由于运行时后端限制,当前 VArray<T, $N> 的元素类型 TT 的成员不能包含引用类型、枚举类型、Lambda 表达式(CFunc 除外)以及未实例化的泛型类型。

var a: VArray<Int64, $3> = [1, 2, 3]
let b = VArray<Int64, $5>({ i => i}) // [0, 1, 2, 3, 4]
let c = VArray<Int64, $5>(repeat: 0) // [0, 0, 0, 0, 0]

区间类型

用于表示拥有固定步长的序列,区间类型是一个泛型,使用 Range<T> 表示。每个区间类型的实例都会包含 start、end 和 step 三个值。

// Range<T>(start: T, end: T, step: Int64, hasStart: Bool, hasEnd: Bool, isClosed: Bool)
let r1 = Range<Int64>(0, 10, 1, true, true, true) // r1 contains 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
let r2 = Range<Int64>(0, 10, 1, true, true, false) // r2 contains 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
let r3 = Range<Int64>(10, 0, -2, true, true, false) // r3 contains 10, 8, 6, 4, 2

let n = 10
let r1 = 0..10 : 1   // r1 contains 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
let r2 = 0..=n : 1   // r2 contains 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
let r3 = n..0 : -2   // r3 contains 10, 8, 6, 4, 2
let r4 = 10..=0 : -2 // r4 contains 10, 8, 6, 4, 2, 0

let r5 = 0..10   // the step of r5 is 1, and it contains 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
let r6 = 0..10 : 0 // Error, step cannot be 0

Unit 类型

对于那些只关心副作用而不关心值的表达式,它们的类型是 Unit.
Unit 类型只有一个值,也是它的字面量:()。除了赋值、判等和判不等外,Unit 类型不支持其他操作。

Nothing 类型

Nothing 是一种特殊的类型,它不包含任何值,并且 Nothing 类型是所有类型的子类型。
breakcontinuereturnthrow 表达式的类型是 Nothing,程序执行到这些表达式时,它们之后的代码将不会被执行。

表达式

If 表达式

import std.random.*

main() {
    let speed = Random().nextFloat64() * 20.0
    println("${speed} km/s")
    if (speed > 16.7) {
        println("第三宇宙速度,鹊桥相会")
    } else if (speed > 11.2) {
        println("第二宇宙速度,嫦娥奔月")
    } else if (speed > 7.9) {
        println("第一宇宙速度,腾云驾雾")
    } else {
        println("脚踏实地,仰望星空")
    }
}

main() {
    let zero: Int8 = 0
    let one: Int8 = 1
    let voltage = 5.0
    let bit = if (voltage < 2.5) {
        zero
    } else {
        one
    }
}


if (let Some(a) <- fun() as Option<Int64>) {}
    // parser error, `as` has lower precedence than `..`
if (let Some(a) <- (fun() as Option<Int64>)) {}
    // correct

if (let Some(a) <- b && a + b > 3) {}
    // correct, parsed as (let Some(a) <- b) && (a + b > 3)
if (let m <- 0..generateSomeInt()) {}
    // correct

while

main() {
    var root = 0.0
    var min = 1.0
    var max = 2.0
    var error = 1.0
    let tolerance = 0.1 ** 10

    while (error ** 2 > tolerance) {
        root = (min + max) / 2.0
        error = root ** 2 - 2.0
        if (error > 0.0) {
            max = root
        } else {
            min = root
        }
    }
    println("2 的平方根约等于:${root}")
}



import std.random.*

main() {
    let random = Random()
    var totalPoints = 0
    var hitPoints = 0

    do {
        // 在 ((0, 0), (1, 1)) 这个正方形中随机取点
        let x = random.nextFloat64()
        let y = random.nextFloat64()
        // 判断是否落在正方形内接圆里
        if ((x - 0.5) ** 2 + (y - 0.5) ** 2 < 0.25) {
            hitPoints++
        }
        totalPoints++
    } while (totalPoints < 1000000)

    let pi = 4.0 * Float64(hitPoints) / Float64(totalPoints)
    println("圆周率近似值为:${pi}")
}

for-in

for (迭代变量 in 序列) {
  循环体
}

main() {
    var sum = 0
    for (i in 1..=100) {
        sum += i
    }
    println(sum)

	let array = [(1, 2), (3, 4), (5, 6)]
	for ((x, y) in array) {
		println("${x}, ${y}")
	}

    for (i in 0..8 where i % 2 == 1) { // i 为奇数才会执行循环体
        println(i)
    }
}

函数

函数是一等公民(first-class citizens),可以作为函数的参数或返回值,也可以赋值给变量。因此函数本身也有类型,称之为函数类型。

可以将参数列表中的参数分为两类:非命名参数和命名参数

  • 非命名参数的定义方式是 p: T,其中 p 表示参数名,T 表示参数 p 的类型,参数名和其类型间使用冒号连接。
  • 命名参数的定义方式是 p!: T,与非命名参数的不同是在参数名 p 之后多了一个 !。命名参数还可以设置默认值.
// 非命名参数
func add(a: Int64, b: Int64): Int64 {
    return a + b
}
// 命名参数
func add(a!: Int64, b!: Int64): Int64 {
    return a + b
}
func add(a!: Int64 = 1, b!: Int64 = 1): Int64 {
    return a + b
}

变量和函数之间不能同名
函数参数均为不可变变量
非命名参数只能定义在命名参数之前,也就意味着命名参数之后不能再出现非命名参数。
在函数定义时如果未显式定义返回值类型,编译器将根据函数体的类型以及函数体中所有的 return 表达式来共同推导出函数的返回值类型。

// 根据 return a + b 推导出 add 函数的返回值类型是 Int64
func add(a: Int64, b: Int64) {
    return a + b
}

对于 return,其等价于 return (),所以要求函数的返回值类型为 Unit

函数调用

对于非命名参数,它对应的实参是一个表达式,对于命名参数,它对应的实参需要使用 p: e 的形式,其中 p 是命名参数的名字,e 是表达式(即传递给参数 p 的值)。

func add(a: Int64, b: Int64) {
    return a + b
}
func add2(a: Int64, b!: Int64) {
    return a + b
}
func add3(a: Int64, b!: Int64 = 2) {
    return a + b
}

main() {
    let x = 1
    let y = 2
    let r1 = add(x, y)
    let r2 = add2(x, b: y)
    let r3 = add3(x) // 可以省略默认参数
}

函数类型

// 函数作为参数
func printAdd(add: (Int64, Int64) -> Int64, a: Int64, b: Int64): Unit {
    println(add(a, b))
}

// 返回一个函数
func add(a: Int64, b: Int64): Int64 {
    a + b
}
func returnAdd(): (Int64, Int64) -> Int64 {
    add
}

main() {
    var a = returnAdd()
    println(a(1,2))
}

函数重载

如果一个作用域中,一个函数名对应多个函数定义,这种现象称为函数重载。

  • 函数名相同,函数参数不同(是指参数个数不同,或者参数个数相同但参数类型不同)
  • 对于两个同名泛型函数,如果重命名一个函数的泛型形参后,其非泛型部分与另一个函数的非泛型部分函数参数不同,则两个函数构成重载,
  • 同一个类内的两个构造函数参数不同,构成重载。
  • 同一个类内的主构造函数和 init 构造函数参数不同,构成重载(认为主构造函数和 init 函数具有相同的名字)
  • 两个函数定义在不同的作用域,在两个函数可见的作用域中构成重载。
  • 两个函数分别定义在父类和子类中,在两个函数可见的作用域中构成重载。

若一个函数在当前作用域中被重载了,那么直接使用该函数名作为表达式可能产生歧义,如果产生歧义编译器会报错,此时,需要提供明确的类型。

func add(i: Int64, j: Int64) {
    i + j
}

func add(i: Float64, j: Float64) {
    i + j
}

main() {
    var f = add   // Error, ambiguous function 'add'
    var plus: (Int64, Int64) -> Int64 = add  // OK
}

嵌套函数

定义在源文件顶层的函数被称为全局函数。定义在函数体内的函数被称为嵌套函数。

func foo() {
    func nestAdd(a: Int64, b: Int64) {
        a + b + 3
    }

    println(nestAdd(1, 2))  // 6

    return nestAdd
}

main() {
    let f = foo()
    let x = f(1, 2)
    println("result: ${x}")
}

Lambda 表达式定义

Lambda 表达式的语法为如下形式: { p1: T1, ..., pn: Tn => expressions | declarations }

let f1 = { a: Int64, b: Int64 => a + b }

var display = { =>   // Parameterless lambda expression.
    println("Hello")
    println("World")
}

// 调用
let r1 = { a: Int64, b: Int64 => a + b }(1, 2) // r1 = 3
let r2 = { => 123 }()                          // r2 = 123
var g = { x: Int64 => println("x = ${x}") }
g(2)

Lambda 表达式不管有没有参数,都不可以省略 =>,除非其作为尾随 lambda。例如:

var display = { => println("Hello") }

func f2(lam: () -> Unit) {}
let f2Res = f2 { println("World") } // OK to omit the =>

尾随 lambda

尾随 lambda 可以使函数的调用看起来像是语言内置的语法一样,增加语言的可扩展性。

当函数最后一个形参是函数类型,并且函数调用对应的实参是 lambda 时,我们可以使用尾随 lambda 语法,将 lambda 放在函数调用的尾部,圆括号外面。

当函数调用有且只有一个 lambda 实参时,我们还可以省略 (),只写 lambda。

func myIf(a: Bool, fn: () -> Int64) {
    if(a) {
        fn()
    } else {
        0
    }
}

func test() {
    myIf(true, { => 100 }) // General function call

    myIf(true) {        // Trailing closure call
        100
    }
}

func f(fn: (Int64) -> Int64) { fn(1) }

func test() {
    f { i => i * i }
}

流操作 Flow

流操作符包括两种:表示数据流向的中缀操作符 |> (称为 pipeline)和表示函数组合的中缀操作符 ~> (称为 composition)。

  1. 当需要对输入数据做一系列的处理时,可以使用 pipeline 表达式来简化描述。
func inc(x: Array<Int64>): Array<Int64> { // Increasing the value of each element in the array by '1'
    let s = x.size
    var i = 0
    for (e in x where i < s) {
        x[i] = e + 1
        i++
    }
    x
}

func sum(y: Array<Int64>): Int64 { // Get the sum of elements in the array.
    var s = 0
    for (j in y) {
        s += j
    }
    s
}

let arr: Array<Int64> = [1, 3, 5]
let res = arr |> inc |> sum // res = 12
  1. composition 表达式表示两个单参函数的组合。composition 表达式语法如下: f ~> g。等价于如下形式: { x => g(f(x)) }
    另外,流操作符不能与无默认值的命名形参函数直接一同使用,这是因为无默认值的命名形参函数必须给出命名实参才可以调用。
func f(x: Int64): Float64 {
    Float64(x)
}
func g(x: Float64): Float64 {
    x
}

var fg = f ~> g // The same as { x: Int64 => g(f(x)) }

变长参数

变长参数是一种特殊的函数调用语法糖。当形参最后一个非命名参数是 Array 类型时,实参中对应位置可以直接传入参数序列代替 Array 字面量(参数个数可以是 0 个或多个)。
需要注意,只有最后一个非命名参数可以作为变长参数,命名参数不能使用这个语法糖。

func sum(arr: Array<Int64>) {
    var total = 0
    for (x in arr) {
        total += x
    }
    return total
}

main() {
    println(sum())
    println(sum(1, 2, 3))
}

操作符重载

定义操作符函数有两种方式:

  1. 对于可以直接包含函数定义的类型 (包括 structenumclassinterface ),可以直接在其内部定义操作符函数的方式实现操作符的重载。
  2. 使用 extend 的方式为其添加操作符函数,从而实现操作符在这些类型上的重载。对于无法直接包含函数定义的类型(是指除 structclassenuminterface 之外其他的类型)或无法改变其实现的类型,比如第三方定义的 structclassenuminterface,只能采用这种方式;

操作符函数对参数类型的约定如下:

  1. 对于一元操作符,操作符函数没有参数,对返回值的类型没有要求。
  2. 对于二元操作符,操作符函数只有一个参数,对返回值的类型没有要求。
  3. 索引操作符([])分为取值 let a = arr[i] 和赋值 arr[i] = a 两种形式,它们通过是否存在特殊的命名参数 value 来区分不同的重载。索引操作符重载不要求同时重载两种形式,可以只重载赋值不重载取值,反之亦可。
  4. 函数调用操作符(())重载函数,输入参数和返回值类型可以是任意类型。

下表列出了所有可以被重载的操作符(优先级从高到低):

Operator Description
() Function call
[] Indexing
! NOT
- Negative
** Power
* Multiply
/ Divide
% Remainder
+ Add
- Subtract
<< Bitwise left shift
>> Bitwise right shift
< Less than
<= Less than or equal
> Greater than
>= Greater than or equal
== Equal
!= Not equal
& Bitwise AND
^ Bitwise XOR
` `
// +, -
open class Point {
    var x: Int64 = 0
    var y: Int64 = 0
    public init (a: Int64, b: Int64) {
        x = a
        y = b
    }

    public operator func -(): Point {
        Point(-x, -y)
    }
    public operator func +(right: Point): Point {
        Point(this.x + right.x, this.y + right.y)
    }
}

// []
class A {
    operator func [](arg1: Int64, arg2: String): Int64 {
        return 0
    }
    operator func [](arg1: Int64, arg2: String, value!: Int64): Unit {
        return
    }
}

func f() {
    let a = A()
    let b: Int64 = a[1, "2"] // b == 0
    a[1, "2"] = 0 
}

const 函数

首先,是常用的const 表达式,具备了可以在编译时求值的能力。满足如下规则的表达式是 const 表达式:

  1. 数值类型、BoolUnitRuneString 类型的字面量(不包含插值字符串)。
  2. 所有元素都是 const 表达式的 Array 字面量(不能是 Array 类型,可以使用 VArray 类型),tuple 字面量。
  3. const 变量,const 函数形参,const 函数中的局部变量。
  4. const 函数,包含使用 const 声明的函数名、符合 const 函数要求的 lambda、以及这些函数返回的函数表达式。
  5. const 函数调用(包含 const 构造函数),该函数的表达式必须是 const 表达式,所有实参必须都是 const 表达式。
  6. 所有参数都是 const 表达式的 enum 构造器调用,和无参数的 enum 构造器。
  7. 数值类型、BoolUnitRuneString 类型的算术表达式、关系表达式、位运算表达式,所有操作数都必须是 const 表达式。
  8. ifmatchtry、控制转移表达式(包含 returnbreakcontinuethrow)、isas。这些表达式内的表达式必须都是 const 表达式。
  9. const 表达式的成员访问(不包含属性的访问),tuple 的索引访问。
  10. const initconst 函数中的 thissuper 表达式。
  11. const 表达式的 const 实例成员函数调用,且所有实参必须都是 const 表达式。

const 函数是一类特殊的函数,这些函数具备了可以在编译时求值的能力。
const 上下文中调用这种函数时,这些函数会在编译时执行计算。而在其它非 const 上下文,const 函数会和普通函数一样在运行时执行。

  1. const 函数声明必须使用 const 修饰。
  2. 全局 const 函数和 static const 函数中只能访问 const 声明的外部变量,包含 const 全局变量、const 静态成员变量,其它外部变量都不可访问。const init 函数和 const 实例成员函数除了能访问 const 声明的外部变量,还可以访问当前类型的实例成员变量。
  3. const 函数中的表达式都必须是 const 表达式,const init 函数除外。
  4. const 函数中可以使用 letconst 声明新的局部变量。但不支持 var
  5. const 函数中的参数类型和返回类型没有特殊规定。如果该函数调用的实参不符合 const 表达式要求,那这个函数调用不能作为 const 表达式使用,但仍然可以作为普通表达式使用。
  6. const 函数不一定都会在编译时执行,例如可以在非 const 函数中运行时调用。
  7. const 函数与非 const 函数重载规则一致。
  8. 数值类型、BoolUnitRuneString 类型 和 enum 支持定义 const 实例成员函数。
  9. 对于 structclass,只有定义了 const init 才能定义 const 实例成员函数。class 中的 const 实例成员函数不能是 open 的。struct 中的 const 实例成员函数不能是 mut 的。
struct Point {
    const Point(let x: Float64, let y: Float64) {}
}

const func distance(a: Point, b: Point) {
    let dx = a.x - b.x
    let dy = a.y - b.y
    (dx**2 + dy**2)**0.5
}

main() {
    const a = Point(3.0, 0.0)  // 编译期间计算
    const b = Point(0.0, 4.0)  // 编译期间计算
    const d = distance(a, b)   // 编译期间计算
    println(d)
}

struct

struct 类型的定义以关键字 struct 开头,后跟 struct 的名字,接着是定义在一对花括号中的 struct 定义体。可以定义一系列的成员变量、成员属性、静态初始化器、构造函数和成员函数。

struct 的成员(包括成员变量、成员属性、构造函数、成员函数、操作符函数用 4 种访问修饰符修饰:privateinternalprotectedpublic,缺省的修饰符是 internal

  • private 表示在 struct 定义内可见。
  • internal 表示仅当前包及子包内可见。
  • protected 表示当前模块可见。
  • public 表示模块内外均可见。

struct 只能定义在源文件顶层。
struct 类型是值类型,其实例成员函数无法修改实例本身
在赋值或传参时,会对 struct 实例进行复制,生成新的实例

// 常规内容
struct Rectangle {
    let width: Int64
    let height: Int64

	public init(width: Int64) {  // 构造函数
        this.width = width
        this.height = width
    }
    public init(width: Int64, height: Int64) {  // 构造函数
        this.width = width
        this.height = height
    }

    public func area() {
        width * height
    }
}

// 静态初始化器
struct Rectangle2 {
    static let degree: Int64
    static init() {
        degree = 180
    }
}

let r = Rectangle(10, 20)
// 如果希望通过 struct 实例去修改成员变量的值,需要将 struct 类型的变量定义为可变变量,并且被修改的成员变量也必须是可变成员变量
let w = r.width

内部修改struct内容:
因为 struct 是值类型,所以如果一个变量是 struct 类型且使用 let 声明,那么不能通过这个变量访问该类型的 mut 函数。

struct Foo {
    var i = 0

    public mut func g() {  // 必须添加 mut 
        i += 1  // Ok
    }
}

枚举类型

enum 类型的定义以关键字 enum 开头,接着是 enum 的名字,之后是定义在一对花括号中的 enum 体,enum 体中定义了若干构造器,多个构造器之间使用 | 进行分隔(第一个构造器之前的 | 是可选的)。

// 构造器 constructor
enum RGBColor {
    | Red | Green | Blue
}

// 有参构造器
enum RGBColor {
    | Red(UInt8) | Green(UInt8) | Blue(UInt8)
}

// 同名构造器
// 支持同一个 enum 中定义多个同名构造器,但是要求这些构造器的参数个数不同  
enum RGBColor {
    | Red | Green | Blue
    | Red(UInt8) | Green(UInt8) | Blue(UInt8)
}

//  支持递归定义
enum Expr {
    | Num(Int64)
    | Add(Expr, Expr)
    | Sub(Expr, Expr)
}

// 支持成员函数,操作符重载等
enum RGBColor {
    | Red | Green | Blue

    public static func printType() {
        print("RGBColor")
    }
}

Option

Option 类型使用 enum 定义,它包含两个构造器:SomeNone

enum Option<T> {
    | Some(T)
    | None
}

Option 类型还有一种简单的写法:在类型名前加 ?。也就是说,对于任意类型 Ty?Ty 等价于 Option<Ty>。例如,?Int64 等价于 Option<Int64>?String 等价于 Option<String> 等等。

// 定义
let a: Option<Int64> = Some(100)
let b: ?Int64 = Some(100)
let c: Option<String> = Some("Hello")
let d: ?String = None

// 自动包装
let a: Option<Int64> = 100
let b: ?Int64 = 100
let c: Option<String> = "100"

// 特殊用法
let a = None<Int64> // a: Option<Int64>
let b = None<Bool> // b: Option<Bool>

模式匹配

支持的模式,包括:常量模式、通配符模式、绑定模式、tuple 模式、类型模式和 enum 模式。

// 常量模式
main() {
    let score = 90
    let level = match (score) {
        case 0 | 10 | 20 | 30 | 40 | 50 => "D"
        case 60 => "C"
        case 70 | 80 => "B"
        case 90 | 100 => "A"
        case _ => "Not a valid score"   // 通配符模式
    }
    println(level)
}

// 绑定模式
// n 是不可修改的
main() {
    let x = -10
    let y = match (x) {
        case 0 => "zero"
        case n => "x is not zero and x = ${n}" // Matched.
    }
    println(y)
}

// enum 模式
enum RGBColor {
    | Red | Green | Blue
}

main() {
    let x = Red
    let y = match (x) {
        case Red => "red" // The 'Red' is enum mode here.
        case _ => "not red"
    }
    println(y)
}

// Tuple 模式
main() {
    let tv = ("Alice", 24)
    let s = match (tv) {
        case ("Bob", age) => "Bob is ${age} years old"
        case ("Alice", age) => "Alice is ${age} years old" // Matched, "Alice" is a constant pattern, and 'age' is a variable pattern.
        case (name, 100) => "${name} is 100 years old"
        case (_, _) => "someone"
    }
    println(s)
}

// 类型模式
open class Base {
    var a: Int64
    public init() {
        a = 10
    }
}

class Derived <: Base {
    public init() {
        a = 20
    }
}

main() {
    var b = Base()
    var r = match (b) {
        case d: Derived => d.a // Type pattern match failed.
        case _ => 0 // Matched.
    }
    println("r = ${r}")
}


Tuple 模式和 enum 模式可以嵌套任意模式。

enum TimeUnit {
    | Year(UInt64)
    | Month(UInt64)
}

enum Command {
    | SetTimeUnit(TimeUnit)
    | GetTimeUnit
    | Quit
}

main() {
    let command = SetTimeUnit(Year(2022))
    match (command) {
        case SetTimeUnit(Year(year)) => println("Set year ${year}")
        case SetTimeUnit(Month(month)) => println("Set month ${month}")
        case _ => ()
    }
}

class

classstruct 的主要区别在于:class 是引用类型,struct 是值类型,它们在赋值或传参时行为是不同的;class 之间可以继承,但 struct 之间不能继承。
class 定义体中可以定义一系列的成员变量、成员属性、静态初始化器、构造函数、成员函数和操作符函数。

struct 一样,class 中也支持定义普通构造函数和主构造函数。
它可以定义若干普通的以 init 为名字的构造函数,可以定义(最多)一个主构造函数。

  • 普通构造函数以关键字 init 开头,后跟参数列表和函数体,函数体中必须完成所有未初始化实例成员变量的初始化,否则编译报错。
  • 主构造函数的名字和 class 类型名相同,它的参数列表中可以有两种形式的形参:普通形参和成员变量形参(需要在参数名前加上 letvar),成员变量形参同时具有定义成员变量和构造函数参数的功能。
    • 这么麻烦,有啥好处吗?使用主构造函数通常可以简化 class 的定义,不需要定义参数作为成员变量,自动完成。
class Rectangle {
    let width: Int64
    let height: Int64
    static let height2 = 20

    public init(width: Int64, height: Int64) {
        this.width = width
        this.height = height
    }

    public func area() {
        width * height
    }
}

创建类的实例时调用的构造函数,将根据以下顺序执行类中的表达式:

  1. 先初始化主构造函数之外定义的有缺省值的变量;
  2. 如果构造函数体内未显式调用父类构造函数或本类其它构造函数,则调用父类的无参构造函数 super(),如果父类没有无参构造函数,则报错;
  3. 执行构造函数体内的代码。

class 终结器

class 支持定义终结器,这个函数在类的实例被垃圾回收的时候被调用。终结器的函数名固定为 ~init。终结器一般被用于释放系统资源:

class C {
    var p: CString

    init(s: String) {
        p = unsafe { LibC.mallocCString(s) }
        println(s)
    }
    ~init() {
        unsafe { LibC.free(p) }
    }
}

使用终结器有些限制条件,需要开发者注意:

  1. 终结器没有参数,没有返回类型,没有泛型类型参数,没有任何修饰符,也不可以被显式调用。
  2. 带有终结器的类不可被 open 修饰,只有非 open 的类可以拥有终结器。
  3. 一个类最多只能定义一个终结器。
  4. 终结器不可以定义在扩展中。
  5. 终结器被触发的时机是不确定的。
  6. 终结器可能在任意一个线程上执行。
  7. 多个终结器的执行顺序是不确定的。
  8. 终结器向外抛出未捕获异常属于未定义行为。
  9. 终结器中创建线程或者使用线程同步功能属于未定义行为。
  10. 终结器执行结束之后,如果这个对象还可以被继续访问,则属于未定义行为。
  11. 如果对象在初始化过程中抛出异常,这样未完整初始化的对象的终结器不会执行。

继承

如果类 B 继承类 A,则我们称 A 为父类,B 为子类。子类将继承父类中除 private 成员和构造函数以外的所有成员。
类可被继承是有条件的:定义时必须使用修饰符 open 修饰。
可以在子类定义处通过 <: 指定其继承的父类,但要求父类必须是可继承的。
class 仅支持单继承!

  • private 表示在 class 定义内可见。
  • internal 表示仅当前包及子包内可见。
  • protected 表示当前模块及当前类的子类可见。
  • public 表示模块内外均可见。
open class A {
    let a: Int64 = 10
}

class B <: A { // Ok: 'B' Inheritance 'A'
    let b: Int64 = 20
}

class C <: B { // Error, 'B' is not inheritable
    let c: Int64 = 30
}

sealed 修饰符只能修饰抽象类,表示被修饰的类定义只能在本定义所在的包内被其他类继承。
sealed 的子类可以不是 sealed 类,仍可被 open/sealed 修饰,或不使用任何继承性修饰符。

package A
public sealed abstract class C1 {}   // Warning, redundant modifier, 'sealed' implies 'public'
sealed open abstract class C2 {}     // Warning, redundant modifier, 'sealed' implies 'open'
sealed abstract class C3 {}          // OK, 'public' is optional when 'sealed' is used

class S1 <: C1 {}  // OK
public open class S2 <: C1 {}   // OK
public sealed abstract class S3 <: C1 {}  // OK
open class S4 <: C1 {}   // OK

// B
package B
import A.*

class SS1 <: S2 {}  // OK
class SS2 <: S3 {}  // Error, S3 is sealed class, cannot be inherited here.
sealed class SS3 {} // Error, 'sealed' cannot be used on non-abstract class.

类型判断:

  • is 判断是否是具体的类型
  • as 可以做类型转换,父子转换
class A {}
class B <: A {}

main(){
    let b = B()
    print("b is A? ${b is A}")  // false
    print("b is B? ${b is B}")  // true
    print("b as A? ${b as A}")  // ok
}

仓颉语言中,有些预设的子类型关系是永远成立的:

  • 一个类型 T 永远是自身的子类型,即 T <: T
  • Nothing 类型永远是其他任意类型 T 的子类型,即 Nothing <: T
  • 任意类型 T 都是 Any 类型的子类型,即 T <: Any
  • 任意 class 定义的类型都是 Object 的子类型,即如果有 class C {},则 C <: Object

所以: Nothing <: T_Class <: T_Class <: Object <: Any

接口

接口用来定义一个抽象类型,它不包含数据,但可以定义类型的行为。一个类型如果声明实现某接口,并且实现了该接口中所有的成员,就被称为实现了该接口。

接口的成员可以包含:

  • 成员函数,成员函数也可以有默认的实现。
  • 操作符重载函数
  • 成员属性
interface I { // 'open' modifier is optional.
    func f(): Unit
}
class Foo <: I {
    public func f(): Unit {
        println("Foo")
    }
}

main() {
    let a = Foo()
    let b: I = a
    b.f() // "Foo"
}

interface还支持静态成员函数

interface NamedType {
    static func typename(): String
}

interface I <: NamedType {
    static func typename(): String {
        f()
    }
    static func f(): String
}

class A <: NamedType {
    public static func typename(): String {
        "A"
    }
}

class B <: NamedType {
    public static func typename(): String {
        "B"
    }
}

func printTypeName<T>() where T <: NamedType {
    println("the type is ${ T.typename() }")
}

main() {
    printTypeName<A>() // Ok
    printTypeName<B>() // Ok
    printTypeName<I>() // Error, 'I' must implement all static function. Otherwise, an unimplemented 'f' is called, causing problems.
}

约束条件类似抽象类 abstract class 可以使用sealed 修饰。

接口继承与抽象类不同,它支持多个继承:

interface Addable {
    func add(other: Int64): Int64
}

interface Subtractable {
    func sub(other: Int64): Int64
}

// 接口的继承
// 它同时也可以
interface Calculable <: Addable & Subtractable {
    func mul(other: Int64): Int64
    func div(other: Int64): Int64
}

// 接口的实现
class MyInt <: Addable & Subtractable {
    var value = 0
    public func add(other: Int64): Int64 {
        value + other
    }
    public func sub(other: Int64): Int64 {
        value - other
    }
    public func mul(other: Int64): Int64 {
        value * other
    }
    public func div(other: Int64): Int64 {
        value / other
    }
}

接口的实现(注意类型匹配,或者是父子关系)

open class Base {}
class Sub <: Base {}

interface I {
    func f(): Base
}

class C <: I {
    public func f(): Sub {
        Sub()
    }
}

如果多个接口实现了相同的方法,那么实现class必须实现自己的:

interface SayHi {
    func say() {
        "hi"
    }
}

interface SayHello {
    func say() {
        "hello"
    }
}

class Foo <: SayHi & SayHello {
    public func say() {
        "Foo"
    }
}

属性

属性(Properties)提供了一个 getter 和一个可选的 setter 来间接获取和设置值。属性在使用时可以作为表达式或被赋值。使用属性的时候与普通变量无异,我们只需要对数据操作,对内部的实现无感知,可以更便利地实现访问控制、数据监控、跟踪调试、数据绑定等机制。

属性可以在 interface、class、struct、enum、extend 中定义。

class Foo {
    // 常见用法
    private var a = 0
    public mut prop b: Int64 {
        get() {
            println("get")
            a
        }
        set(value) {
            println("set")
            a = value
        }
    }
    
    // 只读
    // 只需要声明 prop
    public prop c1: Int64 {
        get() { 0 }
    }
    // 可读写
    // 除了 prop, 必须 mut
    public mut prop c2: Int64 {
        get() { 0 }
        set(v) {}
    }
}

main() {
    var x = Foo()
    let y = x.b + 1 // get
    x.b = y // set
}

和成员函数一样,成员属性也支持 openoverrideredef 修饰,所以我们也可以在子类型中覆盖/重定义父类型属性的实现。

类型转换

仓颉不支持不同类型之间的隐式转换(子类型天然是父类型,所以子类型到父类型的转换不是隐式类型转换),类型转换必须显式地进行。

对于数值类型(包括:Int8Int16Int32Int64IntNativeUInt8UInt16UInt32UInt64UIntNativeFloat16Float32Float64),仓颉支持使用 T(e) 的方式得到一个值等于 e,类型为 T 的值。

main() {
    let a: Int8 = 10
    let b: Int16 = 20
    let r1 = Int16(a)
    println("The type of r1 is 'Int16', and r1 = ${r1}")
    let r2 = Int8(b)
    println("The type of r2 is 'Int8', and r2 = ${r2}")

    let c: Float32 = 1.0
    let d: Float64 = 1.123456789
    let r3 = Float64(c)
    println("The type of r3 is 'Float64', and r3 = ${r3}")
    let r4 = Float32(d)
    println("The type of r4 is 'Float32', and r4 = ${r4}")

    let e: Int64 = 1024
    let f: Float64 = 1024.1024
    let r5 = Float64(e)
    println("The type of r5 is 'Float64', and r5 = ${r5}")
    let r6 = Int64(f)
    println("The type of r6 is 'Int64', and r6 = ${r6}")
}

RuneUInt32 的转换使用 UInt32(e) 的方式,其中 e 是一个 Rune 类型的表达式,UInt32(e) 的结果是 e 的 Unicode scalar value 对应的 UInt32 类型的整数值。

整数类型到 Rune 的转换使用 Rune(num) 的方式,其中 num 的类型可以是任意的整数类型,且仅当 num 的值落在 [0x0000, 0xD7FF][0xE000, 0x10FFFF] (即 Unicode scalar value)中时,返回对应的 Unicode scalar value 表示的字符,否则,编译报错(编译时可确定 num 的值)或运行时抛异常。

class的父子类转换属于允许的转换,不属于隐式转换。一般直接转,或者使用as.

as 操作符可以用于将某个表达式的类型转换为指定的类型。因为类型转换有可能会失败,所以 as 操作返回的是一个 Option 类型。具体而言,对于表达式 e as Te 可以是任意表达式,T 可以是任何类型),当 e 的运行时类型是 T 的子类型时,e as T 的值为 Option<T>.Some(e),否则 e as T 的值为 Option<T>.None

// is 
open class Base {
    var name: String = "Alice"
}
class Derived <: Base {
    var age: UInt8 = 18
}

main() {
    let a = 1 is Int64
    println("Is the type of 1 'Int64'? ${a}")  // true
    let b = 1 is String
    println("Is the type of 1 'String'? ${b}")  // false

    let b1: Base = Base()
    let b2: Base = Derived()
    var x = b1 is Base
    println("Is the type of b1 'Base'? ${x}")  // true
    x = b1 is Derived
    println("Is the type of b1 'Derived'? ${x}") // false
    x = b2 is Base
    println("Is the type of b2 'Base'? ${x}")  // true
    x = b2 is Derived
    println("Is the type of b2 'Derived'? ${x}") // true
}


// as
open class Base {
    var name: String = "Alice"
}
class Derived <: Base {
    var age: UInt8 = 18
}

let a = 1 as Int64     // a = Option<Int64>.Some(1)
let b = 1 as String    // b = Option<String>.None

let b1: Base = Base()
let b2: Base = Derived()
let d: Derived = Derived()
let r1 = b1 as Base    // r1 = Option<Base>.Some(b1)
let r2 = b1 as Derived // r2 = Option<Derived>.None
let r3 = b2 as Base    // r3 = Option<Base>.Some(b2)
let r4 = b2 as Derived // r4 = Option<Derived>.Some(b2)
let r5 = d as Base     // r5 = Option<Base>.Some(d)
let r6 = d as Derived  // r6 = Option<Derived>.Some(d)

泛型

类型别名:type I64 = Int64 . 类型别名并不会定义一个新的类型,它仅仅是为原类型定义了另外一个名字.

一般泛型:

func id<T>(a: T) {
    return a
}

// 约束
func genericPrint<T>(a: T) where T <: ToString {
    println(a)
}

main() {
    genericPrint<Int64>(10)
    return 0
}

扩展

扩展可以为在当前 package 可见的类型(除函数、元组、接口)添加新功能。当不能破坏被扩展类型的封装性,但希望添加额外的功能时,可以使用扩展。
扩展本身不能使用修饰符修饰。

可以添加的功能包括:

  • 添加成员函数
  • 添加操作符重载函数
  • 添加成员属性
  • 实现接口

扩展不支持以下功能:

  1. 扩展不能增加成员变量。
  2. 扩展的函数和属性必须拥有实现。
  3. 扩展的函数和属性不能使用 openoverrideredef修饰。
  4. 扩展不能访问被扩展类型中 private 修饰的成员。

根据扩展有没有实现新的接口,扩展可以分为 直接扩展接口扩展 两种用法,直接扩展即不包含额外接口的扩展;接口扩展即包含接口的扩展,接口扩展可以用来为现有的类型添加新功能并实现接口,增强抽象灵活性。

// 直接扩展
extend String {
    public func printSize() {
        println("the size is ${this.size}")
    }
}
main() {
    let a = "123"
    a.printSize() // the size is 3
}

// 泛型的扩展
class MyList<T> {
    public let data: Array<T> = Array<T>()
}

extend<T> MyList<T> {} // OK
extend<R> MyList<R> {} // OK
extend<T, R> MyList<(T, R)> {} // OK
extend MyList {} // Error
extend<T, R> MyList<T> {} // Error
extend<T, R> MyList<T, R> {} // Error

// 接口扩展
// 使用扩展语法,约束了 T1 和 T2 在支持 equals 的情况下,Pair 也可以实现 equals 函数
class Pair<T1, T2> {
    var first: T1
    var second: T2
    public init(a: T1, b: T2) {
        first = a
        second = b
    }
}

interface Eq<T> {
    func equals(other: T): Bool
}

extend<T1, T2> Pair<T1, T2> where T1 <: Eq<T1>, T2 <: Eq<T2> {
    public func equals(other: Pair<T1, T2>) {
        first.equals(other.first) && second.equals(other.second)
    }
}

class Foo <: Eq<Foo> {
    public func equals(other: Foo): Bool {
        true
    }
}

main() {
    let a = Pair(Foo(), Foo())
    let b = Pair(Foo(), Foo())
    println(a.equals(b)) // true
}

扩展的限制

为了防止一个类型被意外实现不合适的接口,仓颉不允许定义孤儿扩展,指的是既不与接口(包含接口继承链上的所有接口)定义在同一个包中,也不与被扩展类型定义在同一个包中的接口扩展。

// package a
public class Foo {}

// package b
public interface Bar {}

// package c
import a.Foo
import b.Bar

extend Foo <: Bar {} // Error

我们不能在 package c 中,为 package a 里的 Foo 实现 package b 里的 Bar。但是可以在 package a 或者在 package b 中为 Foo 实现 Bar

对同一类型可以扩展多次,并且在扩展中可以直接调用被扩展类型的其他扩展中非 private 修饰的函数。泛型类型的任意两个扩展之间的可见性规则如下:

  • 如果两个扩展的约束相同,则两个扩展相互可见,即两个扩展内可以直接使用对方内的函数或属性;
  • 如果两个扩展的约束不同,且两个扩展的约束有包含关系,约束更宽松的扩展对约束更严格的扩展可见,反之,不可见;
  • 当两个扩展的约束不同时,且两个约束不存在包含关系,则两个扩展均互相不可见。
Logo

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

更多推荐