基本概念
- 标识符分为 普通标识符 和 原始标识符 两类,原始标识符是在普通标识符或仓颉关键字的外面加上一对反引号,主要用于将仓颉关键字作为标识符的场景。都需要符合标识符规则
- 仓颉语言后缀是.cj。在顶层作用域可以定义一个名叫main的函数作为程序入口函数
cj
func identifier() {
// 普通标识符
let abc = 10
var `abcd` = 10
// 原始标识符定义后,如果不是关键字。可以使用普通标识符表示,即``包起来的内容
abcd = 20
// 可以使用任意的Unicode标准的字符
let 仓颉 = true
// 原始标识符
var `type` = 20
`type` = 30
}
// 入口函数 定义 main 函数,不需要写 func 修饰符
// 可以不带参数或带Array<String>的参数,
// 可以返回整型或Unit类型
main() {
}
main() {
return 0
}
main(args:Array<String>): Unit {
prinlnt(args)
}
main(args:Array<String>): Int64 {
prinlnt(args)
return 0
}
- 变量和函数根据所处的作用域有不同的叫法,位于顶层作用域的叫全局函数和全局变量 ,位于自定义类型作用域的叫成员函数和成员变量 ,位于方法或函数作用域的叫局部函数和局部变量
cj
// 全局变量
let globalVariable: Int = 1
// 全局函数
func globalFunc() {
// 局部变量
let localVariable = 2023
// 局部函数
func localFunc() {
println(localVariable)
}
}
class A {
// 成员变量
let memberVariable = 2024
// 成员函数
public func memberFunc() {
println(memberVariable)
}
}
-
变量的定义形式
修饰符 变量名: 变量类型 = 初始值
- 修饰符分三类
- 可变性修饰符:let 与 var,分别对应不可变和可变属性,决定变量被初始化后其值还能否改变
- 可见性修饰符:private 与 public 等,决定变量是否可以被外界访问到
- 静态性修饰符:static,影响成员变量的存储和引用方式
- 修饰符分三类
cj
class A {
private let c: Int64 = 3
public static var d: Int = 4
}
局部变量定义时,可以不初始化,但使用前一定要赋初值
cj
main() {
// 这里定义时,标注类型但不初始化
let text: String
text = "仓颉造字"
// 使用前一定要保证初始化了
println(text)
}
仓颉中变量分为值类型和引用类型。这部分的概念和其他语言类似
值类型的变量一般存储在栈上,每个变量有单独的存储空间
引用类型的变量,变量存储在栈上,变量的值指向的对象存储在堆上。多个引用变量的值都指向堆上的同一个对象
表达式
条件表达式
if 表达式用于 判断条件。仓颉是强类型的 判断条件必须是bool类型
if-let 表达式 用于模式匹配
cj
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("脚踏实地,仰望星空")
}
let one: ?Int = 4
// 如果one里面有值,将one绑定到Option.Some中的v
if (let Option.Some(v) <- one) {
println(v)
}
}
循环表达式
while 表达式、do-while 表达式 用于循环处理
cj
main() {
var i = 0
while (i < 5) {
println(i)
i += 1
}
i = 0
do {
println(i)
i += 1
} while (i < 5)
}
do while语句还可以用于隔离变量
cj
do {
// 这样写有点拉
let a = 10
} while (false)
do {
// 这里的a和上面do while的a互不干扰
let a = 10
} while (false)
for-in 表达式
- 一般用于遍历迭代器或集合,其中迭代器绑定的变量为let修饰的,不可修改。
- 如果只是想遍历迭代器,可以使用通配符匹配模式
- 可以配合where子句,达到筛选的目的
- break 用于终止当前循环表达式的执行、转去执行循环表达式之后的代码
- continue 用于提前结束本轮循环、进入下一轮循环
cj
main() {
// 遍历区间
var sum = 0
// 其中i隐式使用let绑定,不可修改,同时使用where筛选奇数
for (i in 1..=100 where i % 2 == 1) {
sum += i
}
// 使用通配符模式遍历。不关心此次绑定的变量
for (_ in 0..5) {
prinlnt("not use i")
}
println(sum)
// 遍历元组构成的序列
let array = [(1, 2), (3, 4), (5, 6)]
// 其中x,y隐式使用let绑定,不可修改
for ((x, y) in array) {
println("${x}, ${y}")
}
}
while-let 表达式
一般用于在循环中绑定可选值
cj
import std.random.*
// 此函数模拟在通信中接收数据,获取数据可能失败
func recv(): Option<UInt8> {
let number = Random().nextUInt8()
if (number < 128) {
return Some(number)
}
return None
}
main() {
// 模拟循环接收通信数据,如果失败就结束循环
while (let Some(data) <- recv()) {
println(data)
}
println("receive failed")
}
基础数据类型
整数类型
- 整数类型分为有符号(signed)整数类型和无符号(unsigned)整数类型
- 其中IntNative和UIntNative表示和平台相关的整数类型,他们的bit长度和平台一样,在32位bit上是32位等价于Int32/UInt32,在64位bit上是64位等价于Int64/UInt64
- 整数字面量可以通过前缀0b/0o/ox指定整数的进制,通过后缀i8/i16/i32/i64/u8/u16/u32/u64指定整数类型,支持使用_分隔符拆分位数更易读
- 支持字符字节字面量,由字符 b、一对标识首尾的单引号、以及一个 ASCII 字符组成
cj
func intType() {
// 类型标注
let a: Int8 = 1
// 后缀指定类型
let a1 = 1i8
let b: Int16 = 2
let b1 = 2i16
let c: Int32 = 3
let c1 = 3i32
let d: Int64 = 4
let d1 = 4i64
let e: IntNative = 5
let f: UInt8 = 6
let f1 = 6u8
let g: UInt16 = 7
let g1 = 7u16
let h: UInt32 = 8
let h1 = 8u32
let i: UInt64 = 9
let i1 = 9u64
let j: UIntNative = 10
let k: Int = 11
let l: UInt = 12
// 前缀指定进制
// 二进制
let m: Int = 0b1
// 八进制
let n: Int = 0o72
// 十六进制
let o: Int = 0x1a
// 默认十进制
let p: Int = 1
// 下划线分割符,提高可读性
let q: Int = 1000_1000
// 字符字节字面量,必须是一个ASCII码
let a2 = b'x' // a is 120 with type UInt8
let b2 = b'\n' // b is 10 with type UInt8
let c2 = b'\u{78}' // c is 120 with type UInt8
}
有符号整数类型
包括 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 和平台相关大小的无符号整数值的类型
浮点类型
-
浮点类型包括 Float16、 Float32 和 Float64,分别用于表示编码长度为 16-bit、 32-bit 和 64-bit 的浮点数
-
Float64 的精度约为小数点后 15 位,Float32 的精度约为小数点后 6 位,Float16 的精度约为小数点后 3 位
-
浮点类型字面量有两种进制表示形式:十进制、十六进制
- 在 十进制 表示中,一个浮点字面量至少要包含一个整数部分或一个小数部分,没有小数部分时必须包含指数部分(以 e 或 E 为前缀,底数为 10)
- 在 十六进制 表示中,一个浮点字面量除了至少要包含一个整数部分或小数部分(以 0x 或 0X 为前缀),同时必须包含指数部分(以 p 或 P 为前缀,底数为 2)
cj
private func floatType() {
let a: Float32 = 2.0
let b: Float32 = 2e0
let c: Float32 = .6
let d: Float32 = 0x1p0
let e: Float32 = 0x.6p1
let f: Float32 = 0x1.2p0
}
布尔类型
和其他语言一样,布尔类型只有true和false
cj
func boolType() {
let a: Bool = false
let b: Bool = true
}
字符类型
- 字符类型使用 Rune 表示,可以表示 Unicode 字符集中的所有字符
- 字符类型字面量有三种形式:单个字符、转义字符和通用字符。
- 一个 Rune 字面量由字符 r 开头,后跟一个由一对单引号或双引号包含的字符
cj
func runeType() {
// 单个字符
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}'
}
字符串类型
- 字符串类型使用 String 表示,用于表达文本数据,由一串 Unicode 字符组合而成
- 字符串字面量分为三类:单行字符串字面量,多行字符串字面量,多行原始字符串字面量
- 支持插值字符串,{}会被它里面最后一个表达式的值替换
cj
func stringType() {
// 单行字符串字面量
// 一对单引号或一对双引号之内,引号中的内容可以是任意数量的(除了非转义的双引号和单独出现的 \ 之外的)任意字符。
// 单行字符串字面量只能写在同一行,不能跨越多行
let s1: String = "使用双引号"
let s2 = '使用单引号'
let s3 = "\"使用转义的双引号\""
let s4 = '使用转义符\n'
let s5 = '单引号里有双引号"'
let s6 = "双引号里有单引号'"
// 多行字符串字面量
// 开头结尾需各存在三个双引号(""")或三个单引号(''')
// 字面量的内容从开头的三个引号换行后的第一行开始,到结尾的三个引号之前为止,之间的内容可以是任意数量的(除单独出现的 \ 之外的)任意字符
let mulS1: String = """
使用双引号
第二行
最后一行前面其实还要有4个空格
"""
let mulS2 = '''
使用单引号
第二行
最后一行
'''
let mutS3 = """
双引号中有单引号'''''
"""
let mutS4 = '''
单引号中有双引号"""
"""
'''
// 多行原始字符串字面量
// 以一个或多个井号(#)和一个单引号(')或双引号(")开头,后跟任意数量的合法字符,直到出现与字符串开头相同的引号和与字符串开头相同数量的井号为止
// 转义规则不适用于多行原始字符串字面量,字面量中的内容会维持原样
let mulRawS1: String = """
使用双引号
第二行
最后一行前面其实还要有4个空格
\n 不会被转移,原样输出
"""
let mulRawS2 = '''
使用单引号
第二行
最后一行
'''
let mutRawS3 = """
双引号中有单引号'''''
"""
let mutRawS4 = '''
单引号中有双引号"""
"""
'''
// 隐式转换
// 左边是Byte类型,右边是表示ASCII字符的字面量,被强转成Byte进行赋值
let b: Byte = "0"
// 左边是Rune类型,右边是单个字符的字面量,被强转成Rune进行赋值
let r: Rune = '0'
// 插值字符串
// 插值表达式用花括号 {} 包起来,并在 {} 之前加上 $ 前缀。
// {} 中可以包含一个或者多个声明或表达式
// 当插值字符串求值时,每个插值表达式所在位置会被 {} 中的最后一项的值替换,整个插值字符串最终仍是一个字符串
let r1 = 2.4
// 这里第二个{}的值,实际上是PI * r1 * r1
let area = "The area of a circle with radius ${r} is ${let PI = 3.141592; PI * r1 * r1}"
}
元组类型
- 元组(Tuple)将多个不同的类型组合在一起。使用 (T1, T2, ..., TN) 表示,其中 T1 到 TN 可以是任意类型,不同类型间使用逗号(,)连接
- 元组至少是二元,它的长度固定,并且是不可变类型
- 元组要么所有元素都命名,要么都不命名,不能一部分命名,一部分不命名
- 访问元组,可以使用数组的方式,中括号里面放下标,下标从0开始,不大于元组长度
cj
func tupleType() {
// 参数不命名
let t1 = (10, 20)
// 通过下标访问元组
let t10 = t1[0]
// 对元组进行解构,t1第一个元素赋值给t1Num1,t1第二个元素赋值给t1Num2
let (t1Num1, t1Num2) = t1
// t1长度为2,下标不能>=2
// tuple index must be in bounds
// let t12 = t1[2]
// 所有参数命名
let t2: (name: String, age: Int) = ('unravel', 20)
// 对元组进行解构,name赋值给t2Name,age赋值给t2Age
let (t2Name, t2Age) = t2
// 一部分命名,一部分不命名,不被允许
// in a parameter type list, either all parameters must be named, or none of them; mixed is not allowed
// let t3: (name: String, Int) = ('unravel', 18)
}
数组类型
- 仓颉使用 Array<T> 来表示 Array 类型。T 表示 Array 的元素类型,T 可以是任意类型
- Array 是一种长度不变的 Collection 类型,没有提供添加和删除元素的成员函数
- Array 是引用类型,同一个 Array 实例的所有引用都会共享同样的数据
- 仓颉还提供值类型数组 VArray<T, <math xmlns="http://www.w3.org/1998/Math/MathML"> N ,其中 T 表示该值类型数组的元素类型, N\> ,其中 T 表示该值类型数组的元素类型, </math>N,其中T表示该值类型数组的元素类型,N 是一个固定的语法,通过 $ 加上一个 Int64 类型的数值字面量表示这个值类型数组的长度
cj
public func arrayType() {
// 显式标注类型
let a: Array<Int> = [1, 2, 3, 4, 5]
let a1: Array<String> = []
// 由编译器推断类型,Array<Int64>
let b = [1, 2, 3, 4, 5]
// 使用构造方法初始化
let c = Array<Int>()
// 指定容量,并使用item填充每个元素
let d = Array<Int>(5, item: 5)
// 指定容量,并使用闭包返回值填充每个元素
let e = Array<Int64>(5, { i => i + 1})
// 根据已有数组构造同类型数组
let f = Array<Int>(a)
// 通过下标访问元素
let a0 = a[0]
// 通过方法访问数组元素,返回一个Option<T>,下标超出时返回Option.None
let a00 = a.get(0)
// 通过range,获取一个数组切片ArraySlice
let a_to2 = a[..=1]
let a_from2 = a[1..]
let a_234 = a[1..=3]
// 通过for in 遍历数组
for (i in a) {
println(i)
}
// 通过下标修改数组
a[0] = 10
// 无法改变数组长度的操作
let arr1 = [0, 1, 2]
let arr2 = arr1
// 这里修改之后,arr1和arr2都变成了 [3, 1, 2],因为数组是引用,此时arr1和arr2都指向同一个对象
arr2[0] = 3
// 值类型数组
let va: VArray<Int64, $3> = [1, 2, 3]
// 指定容量,并使用item填充每个元素
let vb = Array<Int>(5, item: 5)
// 指定容量,并使用闭包返回值填充每个元素
let vc = Array<Int64>(5, { i => i + 1})
// 通过下标访问元素
let va0 = va[0]
// 之后vaCopy和va是两个数组,互补影响
var vaCopy = va
vaCopy[0] = 3
println([va[0], vaCopy[0]])
}
区间类型
- 使用 Range<T> 表示。(T 必须支持关系操作符,并且可以和 Int64 类型的值做加法)
- 每个区间类型的实例都会包含 start、end 和 step 三个值。
- start 和 end 分别表示序列的起始值和终止值,step 表示序列中前后两个元素之间的差值(即步长);
- start 和 end 的类型相同(即 T 被实例化的类型),step 类型是 Int64,并且它的值不能等于 0
- 区间类型可以是递增的,也可以是递减的,还可能为空
cj
func rangeType() {
// "左闭右开"区间的格式是 start..end : step
// 表示一个从 start 开始,以 step 为步长,到 end(不包含 end)为止的区间;
// 当 step > 0 且 start >= end,或者 step < 0 且 start <= end 时 是一个空区间
let n = 10
// r1 contains 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
let r1 = 0..10 : 1
// r3 contains 10, 8, 6, 4, 2
let r3 = n..0 : -2
// "左闭右闭"区间的格式是 start..=end : step
// 表示一个从 start 开始,以 step 为步长,到 end(包含 end)为止的区间。
// 当 step > 0 且 start > end,或者 step < 0 且 start < end 时 是一个空区间
// r2 contains 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
let r2 = 0..=n : 1
// r4 contains 10, 8, 6, 4, 2, 0
let r4 = 10..=0 : -2
// 可以不写 step,此时 step 默认等于 1,但是注意,step 的值不能等于 0
// the step of r5 is 1, and it contains 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
let r5 = 0..10
// step cannot be zero in range expression
// let r6 = 0..10 : 0
// 区间有可能为空
// r7 to r10 are empty ranges
let r7 = 10..0 : 1
}
Unit类型
- 每个表达式都有值,未标注类型时,以它最后一个语句的返回值作为类型
- 对于那些只关心副作用而不关心值的表达式,它们的类型是 Unit
- Unit 类型只有一个值,也是它的字面量:()
- 除了赋值、判等和判不等外,Unit 类型不支持其他操作
cj
func unitType() {
// 显式标注
let u0: Unit = ()
// 编译器推断
let u1 = ()
// 函数返回 Unit类型
func innerFunc() {
println('innerFunc')
}
if (u0 == u1 && u1 == innerFunc()) {
println('u0 == u1 && u1 == innerFunc()')
} else {
println('u0 != u1')
}
}
Nothing类型
- Nothing 是一种特殊的类型,它不包含任何值,并且 Nothing 类型是所有类型的子类型
- break、continue、return 和 throw 表达式的类型是 Nothing,程序执行到这些表达式时,它们之后的代码将不会被执行
cj
func nothingType() {
// 此时a是Unit类型,不是Nothing类型
let a = for (i in 0..6) {
if (i == 3) {
// break 表达式类型是Nothing
let c: Nothing = break
} else {
// continue 表达式类型是Nothing
let f: Nothing = continue
}
}
// 编译器不允许在使用类型的地方显式地使用 Nothing 类型
// throw 表达式类型是Nothing
let b: Nothing = throw Exception()
// return 表达式类型是Nothing
let f: Nothing = return
// 仅声明,未使用
let g: Nothing
}
函数
-
函数的参数有命名参数和非命名参数两种,其中命名参数就是在非命名参数后加个叹号!
-
命名参数和非命名参数都遵循 p:T 形式,p 表示参数名,T 表示参数 p 的类型
-
函数定义时,返回值类型是可选的,可以显示返回也可以编译器推导
-
函数参数作用域从定义处起至函数体结束
-
函数参数均为不可变变量,在函数定义内不能对其赋值
-
命名参数只能在非命名参数之后
-
只能为命名参数设置默认值,不能为非命名参数设置默认值
-
函数体的类型是函数体内最后一"项"的类型:若最后一项为表达式,则函数体的类型是此表达式的类型,若最后一项为变量定义或函数声明,或函数体为空,则函数体的类型为 Unit
-
函数是一等公民,可以作为其他函数的参数、返回值,可以赋值给一个变量
-
函数也有类型:函数类型由函数的参数类型和返回类型组成,参数类型和返回类型之间使用 -> 连接
-
对于一个函数类型,只允许统一写类型参数名,或者统一不写类型参数名,不能交替存在
-
允许在函数内再定义函数,新定义的函数叫嵌套函数
-
仓颉中支持闭包,
{ p1: T1, ..., pn: Tn => expressions | declarations }
- => 之前为参数列表,多个参数之间使用 , 分隔,每个参数名和参数类型之间使用 : 分隔
- => 之后为 lambda 表达式体,是一组表达式或声明序列
- Lambda 表达式的参数名的作用域与函数的相同,为 lambda 表达式的函数体部分,其作用域级别可视为与 lambda 表达式的函数体内定义的变量等同
- Lambda 表达式不管有没有参数,都不可以省略 =>,除非其作为尾随 lambda
- Lambda 表达式中 不支持声明返回类型,其返回类型总是从上下文中推断出来
cj
// func 之后依次是函数名、参数列表、可选的函数返回值类型、函数体
// 命名参数只能在非命名参数之后
// 非命名参数的定义方式是 p: T,其中 p 表示参数名,T 表示参数 p 的类型,参数名和其类型间使用冒号连接
// 命名参数的定义方式是 p!: T,与非命名参数的不同是在参数名 p 之后多了一个 !
public func myFunc(a: Int, b: String, c!: Array<String>): Int {
println("a: ${a} b: ${b} c: ${c}")
return 0
}
// 支持函数重载
// 最后一个非命名参数是Array类型时,可以作为可变参数。既可以通过,分割传递,也可以直接传递一个数组
// 可变参数只能是非命名参数
public func myFunc(a: Int, b: Array<String>): String {
println("a: ${a} b: ${b}")
return '第二个重载函数'
}
// 函数重载
// 全都是命名参数
// 命名参数还可以设置默认值,通过 p!: T = e 方式将参数 p 的默认值设置为表达式 e 的值
public func myFunc(a!: Int, b!: Int = 1): String {
println("a: ${a} b: ${b}")
return '第三个重载函数'
}
// 函数重载 无参无返回值
public func myFunc() {
println("无参无返回值 重载函数")
}
// 函数类型作为参数和作为返回值
public func receiveMyFunc(inputFunc: () -> Unit): (Int, Int) -> String {
// 调用传进来的函数类型的参数
inputFunc()
// 可以返回 外部定义的相同类型的函数
// 这里是上面的第三个重载函数
return myFunc
// 使用函数形式 返回一个函数类型的参数
// innerFunc是嵌套函数
// func innerFunc(a: Int, b: Int): String {
// // 函数内最后一条语句,作为返回值
// "a: ${a} b: ${b}"
// }
// return innerFunc
// 使用闭包形式 返回一个函数类型的参数
// return {a: Int, b: Int => "${a} ${b}"}
}
func test() {
myFunc(1, "hello", c: ['1', '2', '3'])
// 调用第二个myFun,最后一个参数通过,分割
myFunc(2, 'a', 'b', 'c')
// 调用第二个myFun,最后一个参数通过 数组传递
myFunc(3, ['a', 'b', 'c'])
// 调用第三个myFun,命名参数传递可以和定义时不同
myFunc(a: 10, b: 20)
myFunc(b: 10, a: 20)
// 调用第三个myFun,同时b采用默认值
myFunc(a: 30)
// 接收 无参的myFunc作为参数
// 返回有参的myFunc(第三个重载函数)作为返回值
// 使用变量retFunc接收receiveMyFunc返回值
// 编译器自动推断retFunc类型
let retFunc = receiveMyFunc(myFunc)
// retFun是 (Int64, Int64) -> String 类型的函数。可以进行调用
retFunc(1, 2)
// 显示标注retFunc2的类型,需要和receiveMyFunc的返回类型兼容
let retFunc2: (Int, Int) -> String = receiveMyFunc(myFunc)
retFunc2(1, 2)
// 都不写类型参数名
let f1: (String, Int) -> Unit
// 都写类型参数名
let f2: (name: String, age: Int) -> Unit
// 不能一部分写参数名,一部分不写参数名
// in a parameter type list, either all parameters must be named, or none of them; mixed is not allowed
// let f3: (name: String, Int) -> String
// 定义一个闭包
let closure0 = {a: Int64, b: Int64 => a + b}
// 无参闭包
var display = {=> println("Hello")}
// Lambda 表达式赋值给变量时,其参数类型根据变量类型推断
// 这里的闭包类型,是根据sum1的类型推断
let sum1: (Int64, Int64) -> Int64 = {a, b => a + b}
// Lambda 表达式作为 函数调用表达式 的 实参使用时,其参数类型根据函数的形参类型推断
func f(a1: (Int64) -> Int64): Int64 {
a1(1)
}
// 这里的闭包类型,根据f的类型推断。闭包作为f的实参,其类型根据f的形参确定
f({a2 => a2 + 10})
// Lambda 表达式赋值给变量时,其返回类型根据变量类型推断返回类型
let closure_retType0: () -> Unit = {=>}
// Lambda 表达式作为参数使用时,其返回类型根据使用处所在的函数调用的形参类型推断
f({a2 => a2 + 10})
// Lambda 表达式作为返回值使用时,其返回类型根据使用处所在函数的返回类型推断
func f(): (Int64) -> Int64 {
{a: Int64 => a}
}
// Lambda 表达式支持立即调用
let r1 = {a: Int64, b: Int64 => a + b}(1, 2) // r1 = 3
// Lambda 表达式也可以赋值给一个变量,使用变量名进行调用
func f3() {
var g = {x: Int64 => println("x = ${x}")}
g(2)
}
}
函数调用语法糖
尾随闭包
- 函数最后一个形参是函数类型(这个形参是命名类型还是非命名类型没区别),并且传递实参的时候传递的是lambda,这时可以使用尾随闭包
- 如果函数只有一个参数并且符合第一条,此时函数调用的小括号也可以省掉
cj
func trailingClosure() {
// 当函数最后一个形参是函数类型,并且函数调用对应的实参是 lambda 时,我们可以使用尾随 lambda 语法
// 将 lambda 放在函数调用的尾部,圆括号外面
func myIf(a: Bool, fn: () -> Int64) {
if (a) {
fn()
} else {
0
}
}
// 常规调用
myIf(true, {=> 100})
// 尾随闭包调用
myIf(true) {
100
}
// 当函数调用有且只有一个 lambda 实参时,我们还可以省略 (),只写 lambda
func f(fn!: (Int64) -> Int64) {
fn(1)
}
// 这里省略了小括号,直接写闭包的实现
f {i => i * i}
}
Pipeline
- 当需要对输入数据做一系列的处理时,可以使用 pipeline 表达式来简化描述
- 语法: e1 |> e2 |> e3。|>代表数据流向,e1是原始数据,e2、e3是函数或闭包。等价于:let e2Value = e2(e1); let e3Value = e3(e2Value)
- 流操作符操作的函数,第一个参数必须是非命名参数,其余参数是命名参数且必须有默认值。这是因为数据是从前面流下来的,函数必须有参数能接收数据并且是第一个参数接收数据,此参数必须是非命名参数,因为命名参数调用是须指定标签,流操作无法指定标签
- 如果第三条无法满足,可以使用闭包兼容,在闭包内调用带命名参数的函数
cj
func pipelineSugar() {
// 增加1
func inc(x: Int): Int {
x + 1
}
// 平方
func square(y: Int64): Int64 {
y ** 2
}
// 将5,先加1得到6,然后平方得到36
let res = 5 |> inc |> square
println("5 |> inc |> square = ${res}")
// 流操作符操作的函数,第一个参数必须是非命名参数,其余参数是命名参数且必须有默认值
func f1(a: Int64, b!: Int = 10): Unit {
println(a + b)
}
let a = 1 |> f1
func f2(a!: Int64, b!: Int = 10): Unit {
println(a + b)
}
// 在中间使用闭包兼容,在闭包内调用带命名参数的函数
let x = 1 |> {x: Int64 => f2(a: x)}
}
Composition
- composition 表达式表示 两个单参函数的组合
- 语法: f ~> g。等价于: { x => g(f(x)) }。其中 f,g 均为 只有一个参数的函数类型的表达式
- 表达式 f ~> g 中,会先对 f 求值,然后对 g 求值,最后才会进行函数的组合
cj
func compositionSugar() {
// 增加1
func inc(x: Int): Int {
x + 1
}
// 平方
func square(y: Int64): Int64 {
y ** 2
}
// 函数组合的中缀操作符 ~> (称为 composition)
// composition 表达式表示两个单参函数的组合
// composition 表达式语法如下: f ~> g。等价于如下形式: { x => g(f(x)) }
// 表达式 f ~> g 中,会先对 f 求值,然后对 g 求值,最后才会进行函数的组合
// 等价与 {x: Int64 => square(inc(x))}
let cop = inc ~> square
// 和上面的处理一样,只是不需要先把数据写在最前面
println("cop(5) = ${cop(5)}")
func f(x: Int64): Float64 {
Float64(x)
}
// The same as { x: Int64 => f({x: Int64 => x}(x)) }
// 只要前面一个表达式的返回类型是f的参数的子类型就可以
let lambdaComp = ({x: Int64 => x}) ~> f
func h1<T>(x: T): T {
x
}
func h2<T>(x: T): T {
x
}
// The same as { x: Int64 => h2<Int64>(h1<Int64>(x)) }
// 同样的,也支持泛型。
// 这里前一个泛型实例类型是Int64,返回Int64。
// 后一个泛型类型实例类型是Int64,参数也是Int64。
// 第一个泛型的返回值类型符合第二个泛型的参数类型
var hh = h1<Int64> ~> h2<Int64>
}
Composition、Pipeline比较
相同点
- 都把 函数或闭包 进行组合嵌套调用
- 都要求前一个 函数或闭包 的返回值类型是后一个 函数或闭包 参数的子类型
- 都要求 函数或闭包 的第一个参数必须是非命名参数
不同点
- Pipeline将数据作为第一项传入,直接调用函数。Composition仅做函数的组合,不直接调用函数
- Pipeline的 函数或闭包 可以有多个参数。Composition的 函数或闭包 只能有一个参数
可变参数
- 当形参最后一个非命名参数是 Array 类型时,实参中对应位置可以直接传入参数序列代替 Array 字面量(参数个数可以是 0 个或多个)
- 最后一个非命名参数之后,还可以有命名参数。不一定非命名参数是函数的最后一个参数
- 只有最后一个非命名参数可以作为变长参数,命名参数不能使用这个语法糖
- 函数重载决议总是会优先考虑不使用变长参数就能匹配的函数,只有在所有函数都不能匹配,才尝试使用变长参数解析
cj
func restArgsSurgar() {
// 当形参最后一个非命名参数是 Array 类型时,实参中对应位置可以直接传入参数序列代替 Array 字面量(参数个数可以是 0 个或多个)
// 最后一个非命名参数之后,还可以有命名参数。不一定非命名参数是函数的最后一个参数
func restFunc(arr: Array<Int>, name!: String = 'name') {
println("${arr} ${name}")
}
// 这里使用命名参数的默认值
restFunc(1, 2, 3)
// 这里传递实参给命名参数
restFunc(1, 2, 3, name: '命名参数')
// 既可以使用参数序列,也可以使用Array字面量
restFunc([1, 2, 3])
func f<T>(x: T) where T <: ToString {
println("item: ${x}")
}
func f(arr: Array<Int64>) {
println("array: ${arr}")
}
// 使用第2个f函数。两个f函数,没有不接收参数的。决议使用可变参数形式的第2个f
f()
// 使用第2个f函数。两个f函数,第1个是接受一个参数的。决议使用第1个f
f(1)
// 使用第2个f函数。两个f函数,没有接收2个参数的。决议使用可变参数形式的第2个f
f(1, 2)
func f2(arr: Array<Int64>) { arr.size }
func f2(first: Int64, arr: Array<Int64>) { first + arr.size }
// 这里报错 因为无法决议
// 两个f2函数,没有接收3个参数的。两个参数都使用了可变参数,无法决议,所以报错
// f2(1,2,3) // ambiguous match for function call 'f2'
}
函数重载
如果一个作用域中,一个函数名对应多个函数定义,这种现象称为函数重载。
仓颉支持丰富的函数重载和函数决议,详细了解可点击标题
操作符重载
- 仓颉支持已有的一些操作符重载,不支持新定义操作符
- 如果某个类型已经实现了某个操作符,再在扩展中实现同名操作符时会报错(重复实现)
- 可以在类型(其内部可以定义函数的类型)内重载操作符,也可以在扩展中重载操作符
- 对于枚举类型,当构造器形式和 () 操作符重载函数形式都满足时,优先匹配构造器形式
cj
open class Point <: ToString {
public func toString() {
"Point {x: ${this.x} y: ${this.y}}"
}
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)
}
// 下标取值
// 索引操作符取值形式 [] 内的参数序列对应操作符重载的非命名参数,可以是 1 个或多个,可以是任意类型。
// 不可以有其它命名参数。返回类型可以是任意类型
public operator func [](index: Int): Int {
if (index == 0) {
this.x
} else {
this.y
}
}
// 下标赋值
// 索引操作符赋值形式 [] 内的参数序列对应操作符重载的非命名参数,可以是 1 个或多个,可以是任意类型。
// = 右侧的表达式对应操作符重载的命名参数,有且只能有一个命名参数,该命名参数的名称必须是 value, 不能有默认值
// value 可以是任意类型。返回类型必须是 Unit 类型
// value 只是一种特殊的标记,在索引操作符赋值时并不需要使用命名参数的形式调用. eg. p[0] = 10
public operator func [](index: Int, value!: Int): Unit {
if (index == 0) {
this.x = value
} else {
this.y = value
}
}
// 函数调用操作符(())重载函数,输入参数和返回值类型可以是任意类型
public operator func ()(): Unit {
println('point实例作为函数 调用')
}
}
// 对于枚举类型,当构造器形式和 () 操作符重载函数形式都满足时,优先匹配构造器形式
enum E {
Y | X | X(Int64)
public operator func ()(p: Int64) {
println(p)
}
public operator func ()(p: Float64) {
println(p)
}
}
public func ttt() {
let p = Point(10, 20)
println(-p)
println(p + p)
let x = p[0]
println(x)
p[1] = 200
println(p)
p()
// 对于枚举类型,当构造器形式和 () 操作符重载函数形式都满足时,优先匹配构造器形式
// 优先调用构造器 X(Int64).
let e = X(1)
// 匹配到枚举E.X 然后调用 enum E上面的(p:Float64)重载函数
X(1.0)
// 匹配到枚举E.X
let e1 = X
// 调用 enum E上面的(p:Int64)重载函数
e1(1)
// 匹配到枚举E.Y 然后调用 enum E上面的(p:Int64)重载函数
Y(1)
}
可以被重载的操作符
参考资料
- 仓颉编程语言开发指南 developer.huawei.com/consumer/cn...
- 仓颉编程语言白皮书 developer.huawei.com/consumer/cn...
- 仓颉编程语言语言规约developer.huawei.com/consumer/cn...