cangjie仓颉编程语言学习Note-1.基础语法
仓颉是一帮子rust开发者,吸收了rust的语法,参考kotlin, golang, typescript 而做出的糅合怪!它使用方舟编译器,以llvm为后端,开发的一个国产编程语言。因为吸取了很多编程语言的优点,所以体验不错,但是融合的稍显生硬。又因为作为鸿蒙操作系统底层语言,所以标准库很完善,使用相对方便。和golang类似,简单的功能开发,几乎可以不依赖第三方库。
概述
仓颉是一帮子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`
变量
变量定义的具体形式为: 修饰符 变量名: 变量类型 = 初始值
其中修饰符用于设置变量的各类属性,可以有一个或多个,常用的修饰符包括:
- 可变性修饰符:
let与var,分别对应不可变和可变属性,可变性决定了变量被初始化后其值还能否改变,仓颉变量也由此分为不可变变量和可变变量两类。let修饰的变量只能被赋值一次,即初始化,var修饰的变量可以被多次赋值。 - 可见性修饰符:
private与public等,影响全局变量和成员变量的可引用范围,详见后续章节的相关介绍。 - 静态性修饰符:
static,影响成员变量的存储和引用方式,详见后续章节的相关介绍。
在定义仓颉变量时,可变性修饰符是必要的,在此基础上,还可以根据需要添加其他修饰符。
- 变量名应是一个合法的仓颉标识符。
- 变量类型指定了变量所持有数据的类型。当初始值具有明确类型时,可以省略变量类型标注,此时编译器可以自动推断出变量类型。
- 初始值是一个仓颉表达式,用于初始化变量,如果标注了变量类型,需要保证初始值类型和变量类型一致。在定义全局变量或静态成员变量时,必须指定初始值。在定义局部变量或实例成员变量时,可以省略初始值,但需要标注变量类型,同时要在此变量被引用前完成初始化,否则编译会报错。
main() {
let a: Int64 = 20
var b = 12
b = 23
println("${a}${b}")
}
在仓颉编程语言中,class 和 Array 等类型属于引用类型,其他基础数据类型和 struct 等类型属于值类型。
值类型变量通常在线程栈上分配,每个变量都有自己的数据副本;引用类型变量通常在进程堆中分配,多个变量可引用同一数据对象,对一个变量执行的操作可能会影响其他变量。
- 在给值类型变量赋值时,一般会产生拷贝操作,且原来绑定的数据/存储空间被覆写。在给引用类型变量赋值时,只是改变了引用关系,原来绑定的数据/存储空间不会被覆写。
- 用
let定义的变量,要求变量被初始化后都不能再赋值。对于引用类型,这只是限定了引用关系不可改变,但是所引用的数据是可以被修改的。
数据类型
整数类型分为有符号(signed)整数类型和无符号(unsigned)整数类型。
有符号整数类型包括 Int8、Int16、Int32、Int64 和 IntNative,分别用于表示编码长度为 8-bit、16-bit、32-bit、64-bit 和平台相关大小的有符号整数值的类型。
无符号整数类型包括 UInt8、UInt16、UInt32、UInt64 和 UIntNative,分别用于表示编码长度为 8-bit、16-bit、32-bit、64-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。
- 当左操作数类型为
- 除了一元负号(
- 位操作符包括:按位求反(
!)、左移(<<)、右移(>>)、按位与(&)、按位异或(^)、按位或(|)。注意,按位与、按位异或和按位或操作符要求左右操作数是相同的整数类型。 - 关系操作符包括:小于(
<)、大于(>)、小于等于(<=)、大于等于(>=)、相等(==)、不等(!=)。要求关系操作符的左右操作数是相同的整数类型。 - 自增和自减操作符包括:自增(
++)和自减(--)。注意,仓颉中的自增和自减操作符只能作为一元后缀操作符使用。 - 复合赋值操作符包括:
+=、-=、*=、/=、%=、**=、<<=、>>=、&=、^=、|=。
浮点类型
浮点类型包括 Float16、 Float32 和 Float64,分别用于表示编码长度为 16-bit、 32-bit 和 64-bit 的浮点数(带小数部分的数字,如 3.14159、8.24 和 0.1 等)的类型。
后缀分别对应f16, f32, f64
布尔类型
使用 Bool 表示,用来表示逻辑中的真和假。布尔类型只有两个字面量:true 和 false。
字符字节字面量
字符字节字面量,以方便使用 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> 的元素类型 T 或 T 的成员不能包含引用类型、枚举类型、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 类型是所有类型的子类型。break、continue、return 和 throw 表达式的类型是 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)。
- 当需要对输入数据做一系列的处理时,可以使用
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
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))
}
操作符重载
定义操作符函数有两种方式:
- 对于可以直接包含函数定义的类型 (包括
struct、enum、class和interface),可以直接在其内部定义操作符函数的方式实现操作符的重载。 - 使用
extend的方式为其添加操作符函数,从而实现操作符在这些类型上的重载。对于无法直接包含函数定义的类型(是指除struct、class、enum和interface之外其他的类型)或无法改变其实现的类型,比如第三方定义的struct、class、enum和interface,只能采用这种方式;
操作符函数对参数类型的约定如下:
- 对于一元操作符,操作符函数没有参数,对返回值的类型没有要求。
- 对于二元操作符,操作符函数只有一个参数,对返回值的类型没有要求。
- 索引操作符(
[])分为取值let a = arr[i]和赋值arr[i] = a两种形式,它们通过是否存在特殊的命名参数 value 来区分不同的重载。索引操作符重载不要求同时重载两种形式,可以只重载赋值不重载取值,反之亦可。 - 函数调用操作符(
())重载函数,输入参数和返回值类型可以是任意类型。
下表列出了所有可以被重载的操作符(优先级从高到低):
| 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 表达式:
- 数值类型、
Bool、Unit、Rune、String类型的字面量(不包含插值字符串)。 - 所有元素都是
const表达式的Array字面量(不能是Array类型,可以使用VArray类型),tuple字面量。 const变量,const函数形参,const函数中的局部变量。const函数,包含使用const声明的函数名、符合const函数要求的lambda、以及这些函数返回的函数表达式。const函数调用(包含const构造函数),该函数的表达式必须是const表达式,所有实参必须都是const表达式。- 所有参数都是
const表达式的enum构造器调用,和无参数的enum构造器。 - 数值类型、
Bool、Unit、Rune、String类型的算术表达式、关系表达式、位运算表达式,所有操作数都必须是const表达式。 if、match、try、控制转移表达式(包含return、break、continue、throw)、is、as。这些表达式内的表达式必须都是const表达式。const表达式的成员访问(不包含属性的访问),tuple的索引访问。const init和const函数中的this和super表达式。const表达式的const实例成员函数调用,且所有实参必须都是const表达式。
const 函数是一类特殊的函数,这些函数具备了可以在编译时求值的能力。
在 const 上下文中调用这种函数时,这些函数会在编译时执行计算。而在其它非 const 上下文,const 函数会和普通函数一样在运行时执行。
const函数声明必须使用const修饰。- 全局
const函数和static const函数中只能访问const声明的外部变量,包含const全局变量、const静态成员变量,其它外部变量都不可访问。const init函数和const实例成员函数除了能访问const声明的外部变量,还可以访问当前类型的实例成员变量。 const函数中的表达式都必须是const表达式,const init函数除外。const函数中可以使用let、const声明新的局部变量。但不支持var。const函数中的参数类型和返回类型没有特殊规定。如果该函数调用的实参不符合const表达式要求,那这个函数调用不能作为const表达式使用,但仍然可以作为普通表达式使用。const函数不一定都会在编译时执行,例如可以在非const函数中运行时调用。const函数与非const函数重载规则一致。- 数值类型、
Bool、Unit、Rune、String类型 和enum支持定义const实例成员函数。 - 对于
struct和class,只有定义了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 种访问修饰符修饰:private、internal、protected 和 public,缺省的修饰符是 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 定义,它包含两个构造器:Some 和 None。
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
class 与 struct 的主要区别在于:class 是引用类型,struct 是值类型,它们在赋值或传参时行为是不同的;class 之间可以继承,但 struct 之间不能继承。
class 定义体中可以定义一系列的成员变量、成员属性、静态初始化器、构造函数、成员函数和操作符函数。
和 struct 一样,class 中也支持定义普通构造函数和主构造函数。
它可以定义若干普通的以 init 为名字的构造函数,可以定义(最多)一个主构造函数。
- 普通构造函数以关键字
init开头,后跟参数列表和函数体,函数体中必须完成所有未初始化实例成员变量的初始化,否则编译报错。 - 主构造函数的名字和
class类型名相同,它的参数列表中可以有两种形式的形参:普通形参和成员变量形参(需要在参数名前加上let或var),成员变量形参同时具有定义成员变量和构造函数参数的功能。- 这么麻烦,有啥好处吗?使用主构造函数通常可以简化 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
}
}
创建类的实例时调用的构造函数,将根据以下顺序执行类中的表达式:
- 先初始化主构造函数之外定义的有缺省值的变量;
- 如果构造函数体内未显式调用父类构造函数或本类其它构造函数,则调用父类的无参构造函数
super(),如果父类没有无参构造函数,则报错; - 执行构造函数体内的代码。
class 终结器
class 支持定义终结器,这个函数在类的实例被垃圾回收的时候被调用。终结器的函数名固定为 ~init。终结器一般被用于释放系统资源:
class C {
var p: CString
init(s: String) {
p = unsafe { LibC.mallocCString(s) }
println(s)
}
~init() {
unsafe { LibC.free(p) }
}
}
使用终结器有些限制条件,需要开发者注意:
- 终结器没有参数,没有返回类型,没有泛型类型参数,没有任何修饰符,也不可以被显式调用。
- 带有终结器的类不可被
open修饰,只有非open的类可以拥有终结器。 - 一个类最多只能定义一个终结器。
- 终结器不可以定义在扩展中。
- 终结器被触发的时机是不确定的。
- 终结器可能在任意一个线程上执行。
- 多个终结器的执行顺序是不确定的。
- 终结器向外抛出未捕获异常属于未定义行为。
- 终结器中创建线程或者使用线程同步功能属于未定义行为。
- 终结器执行结束之后,如果这个对象还可以被继续访问,则属于未定义行为。
- 如果对象在初始化过程中抛出异常,这样未完整初始化的对象的终结器不会执行。
继承
如果类 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
}
和成员函数一样,成员属性也支持 open、override、redef 修饰,所以我们也可以在子类型中覆盖/重定义父类型属性的实现。
类型转换
仓颉不支持不同类型之间的隐式转换(子类型天然是父类型,所以子类型到父类型的转换不是隐式类型转换),类型转换必须显式地进行。
对于数值类型(包括:Int8,Int16,Int32,Int64,IntNative,UInt8,UInt16,UInt32,UInt64,UIntNative,Float16,Float32,Float64),仓颉支持使用 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}")
}
Rune 到 UInt32 的转换使用 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 T(e 可以是任意表达式,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 可见的类型(除函数、元组、接口)添加新功能。当不能破坏被扩展类型的封装性,但希望添加额外的功能时,可以使用扩展。
扩展本身不能使用修饰符修饰。
可以添加的功能包括:
- 添加成员函数
- 添加操作符重载函数
- 添加成员属性
- 实现接口
扩展不支持以下功能:
- 扩展不能增加成员变量。
- 扩展的函数和属性必须拥有实现。
- 扩展的函数和属性不能使用
open、override、redef修饰。 - 扩展不能访问被扩展类型中
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 修饰的函数。泛型类型的任意两个扩展之间的可见性规则如下:
- 如果两个扩展的约束相同,则两个扩展相互可见,即两个扩展内可以直接使用对方内的函数或属性;
- 如果两个扩展的约束不同,且两个扩展的约束有包含关系,约束更宽松的扩展对约束更严格的扩展可见,反之,不可见;
- 当两个扩展的约束不同时,且两个约束不存在包含关系,则两个扩展均互相不可见。
更多推荐



所有评论(0)