仓颉基本程序结构

内容纲要

@[toc]

程序结构

一般仓颉代码会被保存在一个以.cj为后缀的源文件中,在文件顶层,可定义一系列变量、函数和自定义类型(包括record、enum、class和interface)。定义在顶层的变量和函数分别为全局变量和全局函数

下面例子在文件顶层定义了两个全局变量xy,两个全局函数f1f2及一个class类型A和一个record类型B

let x: Int64 = 1
let y: Int64 = 2

func f1() {}
func f2() {}

class A {}
record B {}

在全局函数和自定义类型内部,同样可以定义变量和函数:定义在全局函数中的变量和函数分别被称为局部变量和局部函数;定义在自定义类型中的变量和函数分别被称为成员变量和成员函数enum和interface中仅支持定义成员函数)。

下面例子中,在函数f1内定义了一个局部变量z和一个局部函数foo,在class类型A中定义了一个成员变量a和一个成员函数f

func f1()
{
    let z: Int64 = 3

    func foo()
    {
        print("I am foo in f1")
    }
}

class A
{
    let a: Int64 = 100

    public func f()
    {
        println("I am f in A")
    }
}

在定义变量和函数时,通常会使用到表达式。表达式由操作数和操作符(可选)组成,每个表达式都有一个类型和一个值。如,上例中变量za的初值3100就是表达式,函数foof函数体中的println函数调用也是表达式。

变量

标识符

标识符分为普通标识符和原始标识符

普通标识符是除了关键字以外,由字母开头,后接任意长度的字母、数字或下划线组合而成的连续字符

原始标识符是在普通标识符外面加上一对反引号\,或者反引号内使用关键字。原始标识符是要用在需要将仓颉关键字作为标识符使用`的场景。

定义和使用变量

根据可变性的不同,仓颉中的变量被分为两类:不可变变量和可变变量。其中,不可变变量初始化值后再不可变,可变变量可再重复赋值,分别用let和var定义。

下面例子中,定义了一个Int64类型的不可变变量x(初值等于1)和一个Float64类型的可变变量y(初值等于1.1),并对y的值进行了修改(println函数中使用了字符串插值):

func main()
{
    let x: Int64 = 1
    println("x = ${x}")
    var y: Float64 = 1.1
    println("y = ${y}")
    y = 2.2
    println("y = ${y}")
}

编译执行,输出结果为:

x = 1
y = 1.100000
y = 2.200000

如果尝试修改不可变变量x的值,将编译报错:

func main()
{
    let x: Int64 = 1
    x = 2;  // Error: the value of x cannot be modified
}

当初始值的类型明确的时候,编译器可以根据初值推断变量的类型,这时可省略变量的类型标注。例如,将上面的例子修改为如下代码,依然可以正常编译运行(输出结果相同)。

func main()
{
    let x = 1
    println("x = $(x)")
    var y = 1.1
    println("y = ${y}")
    y = 2.2
    println("y = ${y}")
}

定义全局变量和静态成员变量时必须初始化。例如下面例子中,定义全局变量b和静态变量d时会因未初始化而报错。

let a: Int64 = 10
let b: Int64    // Error: global variable 'b' must be initialized

record R
{
    static let c: Int64 = 100
    static let d: Int64 // Error: static variable 'd must be initialized
}

对于局部变量,允许定义时不初始化(此时必须标注类型),但必须确保初始化一定要在该变量第一次被读取之前完成。例如以下代码,仍可正常编译运行:

func main()
{
    let x: Int64
    x = 1
    println("x = ${x}")
    var y: Float64
    y = 1.1
    println("y = ${y}")
    y = 2.2
    println("y = ${y}")
}

仓颉中要求每个变量在使用前必须初始化,否则编译报错。例如下例中,y的初始化表达式中访问x时,x并未初始化:

func main()
{
    let x: Int64
    var y = x + 1   // Error: variable 'x' has not been initialized
    println("y = ${y}")
}

基本数据类型

仓颉中的基本数据类型包括整数类型、浮点类型、布尔类型、字符类型、字符串类型、Unit类型、元组类型、区间类型、Nothing类型

整数类型

整数类型分为有符号和无符号整数类型。

有符号整数类型包括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和平台相关大小的无符号整数值的类型。

对于编码长度为N的有符号整数类型,其表示范围为:$-2^{N - 1} \sim 2^{N - 1} -1$;对于编码长度为N的无符号整数类型,其表示范围为:$0 \sim 2^{N} -1$。

类型表示范围
Int8$$-2^7 \sim 2^7-1 (-128 \sim 127)$$
Int16$$-2^{15} \sim 2^{15}-1 (-32,768 \sim 32,767)$$
Int32$$-2^{31} \sim 2^{31}-1 (-2,147,483,648 \sim 2,147,483,647)$
Int64$$-2^{63} \sim 2^{63}-1 (-9,223,372,036,854,775,808 \sim 9,223,372,036,854,775,807)$$
IntNativeplatform dependent
UInt8$$0 \sim 2^8-1 (0 \sim 255)$$
UInt16$$0 \sim 2^{16}-1 (0 \sim 65,535)$$
UInt32$$0 \sim 2^{32}-1 (0 \sim 4,294,967,295)$$
UInt64$$0 \sim 2^{64}-1 (0 \sim 18,446,744,073,709,551,615)$$
UIntNativeplatform dependent

程序具体使用哪种整数类型,取决于该程序中需要处理的整数的性质和范围。在Init64类型适合的情况下,首选Init64类型,因为表示范围足够大,且整数字面量在没有类型上下文的情况下默认推断为Init64类型,可避免不必要的类型转换

整数类型字面量

整数类型字面量有4种进制表示形式:二进制(使用0b0B前缀)、八进制(使用0o0O前缀)、十进制(没有前缀)、十六进制(使用0x0X前缀)。

在各进制表示中,可以使用下划线_充当分隔符的作用,方便识别数值的位数,如0b0001_1000

对于整数类型字面量,若它的值超出了上下文要求的整数类型的表示范围,编译器将会报错。

let x: Int8 = 128   // Error: 128 out of the range of Int8
let y: UInt8 = 256  // Error: 256 out of the range of UInt8
let z: Int32 = 0x8000_0000  // Error: 0x8000_0000 out of the range of Int32

支持的操作

整数类型默认支持的操作符包括:算术操作符、位操作符、关系操作符、自增自减操作符、赋值操作符、复合赋值操作符

算数操作符:+、-、\*、/、%、幂运算\*\*

位操作符:!、<<、>>、&、^、|

关系操作符:<、>、<=、>=、==、!=

自增自减操作符:++、--。注意,只能作为一元后缀操作符使用

赋值操作符:=

复合赋值操作符:+=、-=、*=、/=、%=、**=、<<=、>>=、&=、^=、|=

浮点类型

浮点类型包括Float32Float64,分别用于表示编码长度为32-bit64-bit的浮点数,分别对应IEEE 754中的单精度和双精度格式。

Float64的精度为小数点后15位,Float32的精度为小数点后6位。

浮点类型字面量

浮点类型字面量有两种进制表示形式:十进制、十六进制。在十进制表示中,一个浮点字面量至少要包含一个整数部分或一个小数部分,没有小数部分时必须包含指数部分(以eE为前缀,底数为10)。在十六进制表示中,一个浮点字面量除了至少要包含一个整数部分或小数部分(以0x0X为前缀),同时必须包含指数部分(以pP为前缀,底数为2)。

let a: Float32 = 3.14
let b: Float32 = 2e3
let c: Float32 = 2.4e-1
let d: Float32 = .123e2
let e: Float64 = 0x1.1p0
let f: Float64 = 0x1p2
let g: Float64 = 0x.2p4

支持的操作

同整数类型。

布尔类型

使用Bool表示逻辑中的真和假。

布尔类型字面量

只有两个字面量:truefalse

let a: Bool = true
let b: Bool = false

字符类型

使用Char表示,可以使用 Unicode 字符集中的所有字符。

字符类型字面量

有三种形式:单个字符、转义字符和通用字符,均使用一对单引号定义。

单个字符字面量举例:

let a: Char = 'a'
let b: Char = 'b'

转义字符字面量举例:

let slash: Char = '\‘'
let newLine: Char = '\n'
let doubleSlah: Char = '\t'

通用字符以\u开头,后加定义在一对花括号中的 1~8 个十六进制数,即可表示对应的 Unicode 值代表的字符:

func main()
{
    let he: Char = '\u{4f60}'
    let llo: Char = '\u{597d}'
    print(he)
    print(llo)
}

字符串类型

使用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"###

插值字符串

插值字符串是一种包含一个或多个插值表达式的字符串字面量(不适用于多行原始字符串字面量),通过将表达式插入到字符串中,可有效避免字符串拼接的问题,例如println("${x}")

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

当插值字符串求值时,每个插值表达式所在位置会被{}中的最后一项的值替换,整个插值字符串最终仍是一个字符串:

func 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 PIE = 3.141592; PIE * r * r}"
    println(area)
}

编译执行,输出结果为:

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

支持的操作

支持使用关系操作符进行比较,支持使用+进行拼接:

func main()
{
    let s1 = "abc"
    var s2 = "ABC"
    let r1 = s1 == s2
    println("The result of 'abc' == 'ABC' is: ${r1})
    let r2 = s1 + s2
    println("The result of 'abc' + 'ABC' is: ${r2}")
}

编译并执行上述代码,输出结果为:

The result of 'abc' == 'ABC' is: false
The result of 'abc' + 'ABC' is: abcABC

另外,还支持其他常见操作,如拆分、替换等。

Unit类型

对于只关心副作用但不关心值的表达式,它们的类型是Unit。如,print函数、赋值表达式、复合赋值表达式、自增自减表达式、循环表达式。

Unit类型只有一个值,也是它的字面量:()。除了赋值、判等和判不等外,不支持其他操作。

元组类型

元组(Tuple)可以将多个不同的类型组合在一起,成为一个新的类型。元组类型使用T1*T2*...*TN表示,其中T1TN可以是任意类型,不同类型间使用*连接,元组至少是二元以上,如:Int64*Float64表示一个二元组类型,Int64*Float64*String表示一个三元组类型。

元组的长度是固定的,即一旦定义了一个元组类型的实例,它的长度不能再被更改。

元组类型是不可变类型,即一旦定义了一个元组类型的实例,它的内容不能再被更新。

var tuple = (true, false)
tuple[0] = false    // error: 'tuple element' can not be assigned

元组类型字面量

使用(e1, e2, ..., eN)表示,其中e1eN是表达式,多个表达式之间使用逗号分隔。下面例子中,分别定义了一个Int64*Float64类型的变量x,以及一个Int64*Float64*String类型的变量y,并且使用元组类型的字面量为它们定义了初值:

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

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

func main()
{
    var pi = (3.14, "PIE")
    println(pi[0])
    println(pi[1])
}

区间类型

区间类型用于表示拥有固定步长的序列,区间类型是一个泛型,使用Range<T>表示。当T被实例化成不同的类型时(要求此类型必须支持关系操作符,并且可以和Int64类型的值做加法),会得到不同的区间类型,如最常用的Range<Int64>用于表示整数区间。

每个区间类型的实例都会包含start、end和step三个值。其中,start 和 end 分别表示序列的起始值和终止值,step 表示序列中前后两个元素间的差值 / 步长;start 和 end 的类型相同(即T被实例化的类型),step类型是Int64。

区间类型字面量

区间字面量有两种形式:“左闭右开”区间和“左闭右闭”区间。其中,”左必右开“区间的格式是start..end : step,它表示一个从start开始,以step为步长,到stop为止的区间;”左闭右闭“区间的格式是start..=end : step,它表示一个从start开始,以step为步长,到stop为止的区间。

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, ..., 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

区间字面量中,可以不写step,默认为1,但要注意,其值不能为0。此外,区间也有可能为空:

let r5 = 0..10
let r6 = 0..10 : 0  // Error

let r7 = 10..0 : 1  // r7 to r10 are empty ranges
let r8 = 0..10 : -1
let r9 = 10..=0 : 1
let r10 = 0..=10 : -1

Nothing类型

Nothing是一种特殊类型,不包含任何值,并且它是所有类型的子类型。

breakcontinuereturnthrow表达式的类型是Nothing,程序执行到这些表达式时,它们之后的代码不会被执行。

留下评论

您的电子邮箱地址不会被公开。