Contents

仓颉语言基础(二):变量

标识符

在仓颉编程语言中,开发者可以给一些程序元素命名,这些名字被称为“标识符”。也就是说,我们在程序中为变量等起的名字,就是标识符。
仓颉语言标识符采用Unicode15.0.0规范,分为普通标识符和原始标识符两类。

  • 普通标识符:即不能和仓颉语言保留关键字相同的标识符。其命名需要满足以下规则中至少一条。
    • 由 XID_Start 字符开头,后接任意长度的 XID_Continue 字符。
    • 由一个_开头,后接至少一个 XID_Continue 字符。

XID_Start和XID_Continue是Unicode字符集的概念。XID_Start包括大小写字母、中日韩等非拉丁语系的文字字符等。需要注意,XID_Start中不包括阿拉伯数字和下划线。XID_Continue中包含所有XID_Start的字符、阿拉伯数字、下划线、变音符号等。需要注意,其中不包括空格、标点符号、运算符(如!、@、+等字符)。
也就是说,仓颉语言的标识符支持英文、中日韩文等非拉丁语系字符、数字、下划线等,但是开头不能是数字,且标识符不能是单下划线

变量定义

变量定义的具体形式为:

修饰符 变量名: 变量类型 = 初始值
  • 修饰符用于设置变量的各类属性,可以有一个或多个。目前为止我们可以使用的有:
    • 可见性修饰符:varlet。这决定了变量在初始化后是否可变。var表示可变,let表示不可变。
    • const修饰符:常量。语义与let类似,但是其在编译期求值,可以省略类型标注,但不可以省略初始值。
  • 变量名应是标识符。
  • 变量类型在有明确初始值时可以省略,由编译器自行推断。
  • 初始值在局部变量中可以省略,但在使用之前必须赋初值。而在全局变量/静态变量中,不可省略。有关局部/全局/静态变量,会在后续介绍,目前使用的均为局部变量。

值类型和引用类型

任何变量都会关联到一个值,只不过有些变量,我们直接使用他本身的值,称作值类型变量,而有些变量内部存的是一个索引(或地址,可以理解为C语言的指针),使用时通过该索引去找到那个位置的数据,这被称作引用类型
仓颉语言中,下述介绍的类型除Array类型外,均属于值类型。而后续接触到的类型,将在后续进行介绍。

变量类型

整数类型

整数类型分为有符号整数无符号整数两类。
有符号整数包括Int8 Int16 Int32 Int64 IntNative,数字表示编码长度(单位为bit),Native表示与平台相关,与当前操作系统位宽一致。对于编码长度为n bits的有符号整数,其可表示的范围为$[-2^{n-1} , 2^{n-1}-1]$。
无符号整数类型为上述有符号整数类型前加U,即UInt8 UInt16 UInt32 UInt64 UIntNative,数字含义与上述相同。对于编码长度为n bits的无符号整数类型,其可表示的范围为$[0 , 2^{n}-1]$。
ByteUInt8的别名,IntInt64的别名,UIntUInt64的别名。
整数类型字面量即一个具体的整数数值,支持二进制(使用 0b 或 0B 前缀)、八进制(使用 0o 或 0O 前缀)、十进制(没有前缀)、十六进制(使用 0x 或 0X 前缀)。在各进制表示中,可以使用下划线 _ 充当分隔符的作用,方便识别数值的位数。
若未指定整数类型的字面量,编译器自动推断为Int64。 字符字节字面量是用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
c = b'\u{90}' - b'\u{66}' + c   // c is 162 with type UInt8

b'\u{78}' 是转义形式,表示类型为 UInt8,16 进制大小为 0x78 或 10 进制大小为 120 的字面值。

浮点类型

浮点类型(即小数)包含Float16 Float32 Float64,分别对应IEEE 754标准中的半精度、单精度、双精度格式。Float64 的精度(有效数字位)约为 15 位,Float32 的精度(有效数字位)约为 6 位,Float16 的精度(有效数字位)约为 3 位。
浮点类型字面量可为十进制/十六进制。在十进制表示中,一个浮点字面量至少要包含一个整数部分或一个小数部分,没有小数部分时必须包含指数部分(以 e 或 E 为前缀,底数为 10)。在十六进制表示中,一个浮点字面量除了至少要包含一个整数部分或小数部分(以 0x 或 0X 为前缀),同时必须包含指数部分(以 p 或 P 为前缀,底数为 2)。示例:

let a: Float32 = 3.14       // a is 3.140000 with type Float32
let b: Float32 = 2e3        // b is 2000.000000 with type Float32
let c: Float32 = 2.4e-1     // c is 0.240000 with type Float32
let d: Float64 = .123e2     // d is 12.300000 with type Float64
let e: Float64 = 0x1.1p0    // e is 1.062500 with type Float64
let f: Float64 = 0x1p2      // f is 4.000000 with type Float64
let g: Float64 = 0x.2p4     // g is 2.000000 with type Float64

在使用十进制浮点数字面量时,可以通过加入后缀(f16/f32/f64)来明确浮点数字面量的类型。例如:

let a = 3.14f32   // a is 3.140000 with type Float32
let b = 2e3f32    // b is 2000.000000 with type Float32
let c = 2.4e-1f64 // c is 0.240000 with type Float64
let d = .123e2f64 // d is 12.300000 with type Float64

布尔类型

布尔类型,即Bool,用于表示逻辑真假,其字面量只有true false两种。

字符类型

与多数语言不同,仓颉中使用Rune表示字符类型,可表示Unicode字符集的所有字符。
字符类型字面量有三种形式:单个字符、转义字符、通用字符。

  • 单个字符由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'      //表示制表符
  • 通用字符以 \u 开头,后面加上定义在一对花括号中的 1~8 个十六进制数,即可表示对应的 Unicode 值代表的字符。例如:
let he: Rune = r'\u{4f60}' //对应汉字“你”
let llo: Rune = r'\u{597d}' //对应汉字“好”

字符类型与UInt32等价,可使用UInt32(Rune)或Rune(num)进行转换,其中num只有落在UInt32区间内才合法。

字符串类型

字符串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'''
  • 多行原始字符串字面量以一个或多个#和一个单引号/双引号开头,后跟任意数量,可跨越多行的合法字符,直到遇到与开头匹配的单引号/双引号和相同数量的#为止。与多行字符串字面量不同,多行原始字符串字面量内的转义字符不会被转义,如\n不会被认为是一个换行,而被认为是\n这两个字符本身。
let s1: String = #""#
let s2 = ##'#'\n'## // 输出结果为:#'\n
let s3 = ###"
    Hello,
    Cangjie
    Lang"### // 该变量当中的换行、缩进等也会被保留

插值字符串是指在字符串中间插入表达式(此处称作插值表达式),将表达式计算结果拼接入字符串当中。其语法为:${expr},expr为一个或多个表达式。整个插值字符串求值时,每个插值表达式所在位置会被expr的最后一项替换。例如:

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)
}

输出结果为:

There are 100 apples
The area of a circle with radius 2.400000 is 18.095570

字符串类型支持使用关系操作符进行比较,支持使用+进行拼接,可用string.toRuneArray()转换为字符数组,可用s1.split(s2)将s1用s2进行分割,可用s1.contains(s2)判断s1中是否包含s2。更多操作会在后续介绍。

元组类型

元组Tuple可以将不同类型组合在一起,成为一个新类型。元组类型使用(T1, T2, ..., TN)表示,N至少为2。一旦定义一个元组类型,其长度、单个元素不可被更新,但整个元组可以被覆盖替换。
元组类型字面量使用(e1, e2, …, eN)表示。可通过tuple[index]访问到具体的元素。
可以为元组类型标记显示的类型参数名,比如一个元组类型为(name: String, price: Int64),其中name和price就是类型参数名。但是类型参数名必须统一写或统一不写,不允许混用。比如(name: String, Int64)就是不合法的。且参数名不能当作变量使用或者访问元组元素。

let a: (name: String, Int64) = ("banana", 5) // Error, 不可混用
let b: (name: String, price: Int64) = ("banana", 5) // OK
b.name // Error, 不能用参数名访问该元素

数组类型

Array类型可以用来构造单一类型有序序列数据。可以用Array<T>来表示类型为T的数组。例如:

var a: Array<Int64> = [0, 0, 0, 0] // Int64 类型数组
var b: Array<String> = ["a1", "a2", "a3"] // String类型数组

使用非空字面量来初始化Array,编译器可以自动推断其类型。例如:

let a = [1, 2, 3] // b 为 Array<Int64>()

可以使用构造函数来初始化Array,构造函数我们后续进行详细介绍。

let a = Array<Int64>() // 创建空Int64数组a
let b = Array<Int64>(3, repeat: 0) // 创建3个元素的Int64数组,内容为3个0。
/*
注意,如果该数组所存类型为引用类型,repeat的行为是将这个引用复制三份,也就是说他们实际指向相同的内容,对其中一个进行更改也会影响另外的元素。
*/

使用arr[index]访问数组元素,其中index取值范围为[0, size-1]。如果想获取数组某一区间的值,可以使用区间类型(见下文)。

let arr1 = [0, 1, 2, 3, 4, 5, 6]
let arr2 = arr1[0..5] // arr2 = [0, 1, 2, 3, 4]

区间类型用于下标时,若开始为0,可以省略;若结尾为下标最大值,可以省略。例如:

let arr1 = [0, 1, 2, 3, 4, 5, 6]
let arr2 = arr1[..5] // arr2 = [0, 1, 2, 3, 4]
let arr3 = arr1[2..] // arr3 = [2, 3, 4, 5, 6]

arr.size可以获取到数组的长度。
Array的长度不可变,因此在初始化后不能添加或删除元素,但可以对元素进行修改。对于类型相同的两个Array,可以使用=进行赋值,但是他们会共享元素,而不是真的复制了一份(正是因为Array是引用类型)。例如:

let a = Array<Int64>(3, repeat: 0)
let b = a
a[1] = 1
println(b) // 输出[0, 1, 0]

除Array外,仓颉提供了值类型数组VArray<T, $N>,T为元素类型(只能是值类型),N为数组长度(Int64)。其初始化、下标访问、size获取长度等方法与Array相同。

区间类型

区间类型Range<T>表示数据类型为T的区间(也就是一系列数据)。T类型必须可数、有序(例如Int64)。
区间类型可以使用构造函数进行初始化。其语法为:

修饰符 r1 = Range<T>(start, end, step, hasStart, hasEnd, isClosed)

其中start、end、step是T类型的值,分别表示起始数据、终止数据、步长。r1即为从start开始,每次加一个step,直到end为止的所有数据。hasStart、hasEnd、isClosed为Bool值,分别表示是否包含start、是否包含end、左闭右闭(isClosed=true)还是左闭右开(isClosed=false)。
区间类型字面量有左闭右开和左闭右闭两种形式。

  • 左闭右开的格式为 start..end : step
  • 左闭右闭的格式为start..=end : step

step省略时,默认等于1。但是step不能等于0。
需要注意,区间的值可以为空(即不包含任何元素的空序列)。若给出的start、end、step中不包含任何有效数据(如start>end而step>0)则区间为空。

Unit类型

Unit类型只有一个值,也就是()。对于那些只关心副作用而不关心值的表达式,他们的类型是Unit,比如赋值表达式、print函数等。除赋值、判等、判不等之外,不支持其他操作。

Nothing类型

Nothing类型是任何类型(包括Unit)的子类型,不包含任何值,是一种特殊的类型。
break continue return throw表达式的类型是Nothing(这些关键字会在后续介绍),表示程序执行到这里后,相同代码块内(即一堆{}内,详细内容后续介绍)后续的代码将不再执行。
目前编译器还不支持显示使用Nothing类型。

基本操作符

以下操作符按照优先级从高到低介绍。

  • 自增自减:++ --a++表示a=a+1,减法同理。
  • 按位取反、逻辑非、负号:!表示按位取反(即将对于变量的二进制每一位翻转)或逻辑非(将true/false翻转),-表示数学运算中的负号。均为单目运算符,放在表达式前。
  • 幂运算:**a**b表示a的b次幂。左侧只能为Int64或Float64。
  • 乘除:* /,两侧数据类型相同。除法的操作数为整数时,将非整数值向 0 的方向舍入为整数。
  • 取模:%,操作数只能是整数,两侧数据类型相同。
  • 加减:+ -,两侧数据类型相同。
  • 按位左移/右移:>> <<a << b即将变量对应的二进制向左移动b位。操作数必须为整数(位宽可以不同),右操作数不能是负数。无符号数补0,有符号数右移补高位数字(即正数补0,负数补1)。右操作数不能大于等于左操作数位宽,否则报错。
  • 区间操作符:.. ..=,含义见前文。
  • 大小比较:> >= < <=,返回Bool类型,要求操作数类型相同。
  • 判等判不等:== !=,返回Bool类型,要求操作数类型相同。元组类型当且仅当所有元素支持判等/不等才能进行,且当且仅当长度相等且相同位置元素全部相等时认为相等。
  • 按位与:&,即将两个变量的二进制每一位进行与运算。
  • 按位异或:^,同理
  • 按位或:|,同理
  • 逻辑与:&&,即两个Bool变量都为true时返回true,否则false。
  • 逻辑或:||,即两个Bool变量都为false时返回false,否则true。
  • 赋值:=,即将右侧变量的值赋给左侧。返回Unit类型。先计算右侧,后计算左侧。可以使用元组进行多赋值,比如(a, b) = (1, 2)将1、2分别赋给a、b。可以使用_忽略右侧对应位置的值,比如(a, _) = (1, 2)只将1赋值给a。
  • 复合运算符:op=,其中op为算数运算、按位运算、逻辑运算的一种,a op= b表示(a) = a op (b)