类型

仓颉编程语言是一种静态类型(statically typed)语言:大部分保证程序安全的类型检查发生在编译期。同时,仓颉编程语言是一种强类型(strongly typed)语言:每个表达式都有类型,并且表达式的类型决定了它的取值空间和它支持的操作。静态类型和强类型机制可以帮助程序员在编译阶段发现大量由类型引发的程序错误。

仓颉中的类型可分为两类:不可变类型(immutable type)和可变类型(mutable type)。其中,不可变类型包括数值类型(分为整数类型和浮点数类型,详见[数值类型])、Rune 类型、Bool 类型、Unit 类型、Nothing 类型、String 类型、元组(Tuple)类型、Range 类型、函数(Function)类型、enum 类型;可变类型包括 Array 类型、VArray 类型、struct 类型、class 类型和 interface 类型。

不可变类型和可变类型的区别在于:不可变类型的值,其数据值一经初始化后就不会发生变化;可变类型的值,其数据值初始化后仍然有可以修改的方法。

接下来将逐一介绍每种类型、类型的取值以及它们支持的操作。

不可变类型

下面依次介绍仓颉编程语言中的不可变类型。

数值类型

数值类型包括整数类型与浮点数类型,分别用于表示整数和浮点数。整数类型包含有符号(signed)整数类型和无符号(unsigned)整数类型,其中,有符号整数类型包括 Int8Int16Int32Int64IntNative,分别用于表示编码长度为 8-bit16-bit32-bit64-bit 和平台相关大小的有符号整数值的类型;无符号整数类型包括 UInt8UInt16UInt32UInt64UIntNative,分别用于表示编码长度为 8-bit16-bit32-bit64-bit 和平台相关大小的无符号整数值的类型。浮点数类型包括 Float16Float32Float64,分别用于表示编码长度为 16-bit32-bit64-bit 的浮点数的类型。

IntNative/UIntNative 的长度与当前系统的位宽一致。

下表列出了所有的数值类型以及它们的表示范围:

TypeRange
Int8$-2^7 \sim 2^7-1$ (-128 to 127)
Int16$-2^{15} \sim 2^{15}-1$ (-32768 to 32767)
Int32$-2^{31} \sim 2^{31}-1$ (-2147483648 to 2147483647)
Int64$-2^{63} \sim 2^{63}-1$ (-9223372036854775808 to 9223372036854775807)
IntNativeplatform dependent
UInt8$0 \sim 2^8-1$ (0 to 255)
UInt16$0 \sim 2^{16}-1$ (0 to 65535)
UInt32$0 \sim 2^{32}-1$ (0 to 4294967295)
UInt64$0 \sim 2^{64}-1$ (0 to 18446744073709551615)
UIntNativeplatform dependent
Float16See IEEE 754 binary16 format
Float32See IEEE 754 binary32 format
Float64See IEEE 754 binary64 format

其中,为了方便使用,仓颉编程语言中特别提供了一些类型别名(关于类型别名详见下文中的类型别名小节):

  1. Byte 类型作为 UInt8 的类型别名,ByteUInt8 完全等价。
  2. Int/UInt 类型分别作为 Int64/UInt64 的类型别名,IntInt64 完全等价,UIntUInt64 完全等价。
let a: UInt8 = 128
let b: Byte = a // ok

let c: Int64 = 9223372036854775807
let d: Int = c // ok

let e: UInt64 = 18446744073709551615
let f: UInt = e // ok

下面首先介绍数值类型字面量和数值类型支持的操作。

数值类型字面量

我们称 5242.93.14 等这样的数值为数值类型字面量,字面量只能用它们的值称呼它们,并且它们的值不能被修改。

下面分别介绍整数类型字面量和浮点类型字面量。

整数类型字面量可以使用 4 种进制表示形式:二进制、八进制、十进制、十六进制。其中,二进制以 0b(或 0B)开头,八进制以 0o(或 0O)开头,十六进制以 0x(或 0X)开头,十进制没有前缀。例如,可以将十进制值 24 定义成下列 4 种形式的任意一种,其中下划线 _ 充当分隔符的作用,方便识别数值的位数:

0b0001_1000     // Binary.
0o30            // Octal.
24              // Decimal.
0x18            // Hexadecimal.

整数类型字面量的语法定义参见[整数类型字面量]。

浮点数类型 符合 IEEE 754 格式:Float16 使用 IEEE 754 中的半精度格式(binary 16),Float32 使用 IEEE 754 中的单精度格式(binary32),Float64 使用 IEEE 754 中的双精度格式(binary64)。一个浮点数通常包含整数部分,小数部分(包含小数点)和指数部分,并且有两种进制表示形式:十进制、十六进制。在十进制表示中,一个浮点数至少要包含一个整数部分或一个小数部分,并且当没有小数部分时必须包含指数部分(以 eE 为前缀,底数为 10)。在十六进制表示中,一个浮点数至少要包含一个整数部分或小数部分(以 0x 开头),同时必须包含指数部分(以 pP 为前缀,底数为 2)。浮点类型量中有几个特殊的值需要注意:正无穷(POSITIVE_INFINITY),负无穷(NEGATIVE_INFINITY),Not a Number(NaN),正 0+0.0),负 0-0.0)。其中,无穷表示操作结果超出了表示范围,例如将两个很大的浮点数相乘,或将一个非零浮点数除以浮点零时,其结果都是无穷,无穷的符号由操作数的符号决定,符合“正负得负,负负得正”的规律。NaN 表示既不是实数也不是无穷的情况,例如计算正无穷乘以 0 的结果就是 NaN。浮点类型字面量举例如下:

3.14 		// decimal Float64 3.14.
2.4e-1 		// decimal Float64 0.24.
2e3 		// decimal Float64 2000.0.
.8 			// decimal Float64 0.8.
.123e2 		// decimal Float64 12.3.
0x1.1p0 	// hexadecimal Float64 1.0625 (decimal value).
0x1p2 		// hexadecimal Float64 4 (decimal value).
0x.2p4 		// hexadecimal Float64 2 (decimal value).

浮点类型字面量的语法定义参见[浮点数类型字面量]章节。

仓颉编程语言中的浮点小数类型的最小正值为非正规浮点小数 (subnormal floating point number)。

对于 Float32 类型最小可表示的正浮点数为 $2^{-149}$,大约为 1.4e-45;而最大值可表示的数为 $(2 - 2^{-23}) \times 2^{127}$,大约为 3.40282e38

对于 Float64 类型最小可表示的正浮点数为 $2^{-1074}$,大约为 4.9e-324;而最大可表示的数为 $(2 - 2^{-52}) \times 2^{1023}$,大约为 1.79769e308

当非 $0$ 的浮点小数字面值因过小或者过大而舍入得到 $0$ 或者无穷,那么编译器会告警。

数值类型支持的操作符

数值类型支持的操作符包括:算术操作符、位操作符、关系操作符、自增(减)操作符、一元负号操作符和(复合)赋值操作符。由于整数类型支持所有以上操作符,而浮点类型除了不支持位操作符外,其余操作符均支持,因此接下来简要介绍整数类型上的这些操作符(详细介绍参考[表达式]章节)。

算术操作符包括加(+)、减(-)、乘(*)、除(/)、取余(%)、取幂(**),默认算术操作符没有被重载(操作符重载详见参见[操作符重载]章节)的情况下,要求算术操作符的两个操作数的类型必须相同,如果要将两个不同类型的操作数进行操作,必须先进行强制类型转换。关于算术操作符的优先级和结合性,请参考[算术表达式]中的介绍。下面举例说明整数类型上的算术操作:

2 + 3                        // add result: 5
3 - 1                        // sub result: 2
3 * 9                        // mul result: 27
24 / 8                       // div result: 3
7 % 3                        // mod result: 1
2 ** 3                       // power result: 8
5 + 15 - 2 * 10 / 4          // result: 15
5 + 10 - 3 * 4 ** 2 / 3 % 5  // result: 14

位操作符 包括位求反(!)、左移(<<)、右移(>>)、位与(&)、位异或(^)、位或(|)。关于位操作符的详细说明,请参考第 4 章中关于[位运算表达式]的介绍。下面简单举例说明整数类型上的位操作:

!10         // bitwise logical NOT operator: -11
10 << 1     // left shift operator: 10*2=20
10 >> 1     // right shift operator: 10/2=5
10 & 15     // bitwise logical AND operator: 10
10 ^ 15     // bitwise XOR operator: 5
10 | 15     // bitwise logical OR operator: 15
0b1010 & 0b1110 ^ 0b1111 | 0b0101   // result: 0b0101

关系操作符包括小于(<)、大于(>)、小于等于(<=)、大于等于(>=)、相等(==)、不等(!=),关系表达式的结果是一个 Bool 类型的值(truefalse)。关于关系操作符的详细说明,请参考第 4 章中关于关系操作符的介绍。下面简单举例说明整数类型上的关系操作:

2 == 3  	// result: false
2 != 3  	// result: true
8 < 10  	// result: true
9 <= 9  	// result: true
24 > 28  	// result: false
24 >= 23  	// result: true

自增(减)操作符包括自增(++)和自减(--),可看做是一种特殊的赋值操作符(见下),用于实现将变量值加(减)1。自增(减)操作符只能作为后缀操作符使用,且只能作用于整数类型可变变量,因为不可变变量和整数字面量的值不允许修改。关于自增(减)操作符的详细说明,请参考第 4 章中关于自增和自减操作符的介绍。下面简单举例说明整数类型上的自增(减)操作:

main() {
    var i: Int32 = 5
    var j: Int32 = 6
    i++ // i=6
    j-- // j=5   
    return 0
}

一元负号操作符使用 - 表示,结果是对其操作数取负。关于 - 的详细说明,请参考第 4 章中关于一元负号操作符的介绍。下面简单举例说明整数类型上的一元负号操作:

var i: Int32 = 5
var j: Int64 = 100 
var k: Int32 = -i   // k = -5
var l: Int64 = -j   // l = -100 

(复合)赋值操作符包括赋值(=)和复合赋值操作(op=),其中 op 可以是算术操作符、逻辑操作符和位操作符中的任意二元操作符。(复合)赋值操作可以实现一个值为数值类型的表达式到数值类型变量的赋值。关于(复合)赋值操作符的详细说明,请参考第 4 章中关于[赋值表达式]的介绍。下面简单举例说明整数类型上的(复合)赋值操作:

main() {
    var x: Int64 = 5
    var y: Int64 = 10
    x = y 	// assignment: x = 10
    x += y 	// compound assignment: x = 20
    x -= y 	// x = 10
    x *= y 	// x = 100
    x /= y 	// x = 10
    x %= y 	// x = 0
    x = 5 	// x = 5
    x **= 2 // x = 25
    x <<= 1 // x = 50
    x >>= 2 // x = 12
    x &= y 	// x = 8
    x ^= y 	// x = 2
    x |= y 	// x = 10
    return 0
}

浮点数类型支持的操作符

浮点类型支持的操作包括(注意没有位操作):算术操作、关系操作、自增(减)操作、一元正(负)号操作、条件操作和(复合)赋值操作。浮点数的计算可能使用与其本身类型不同精度的操作。浮点操作中有几种特殊情况需要注意:在关系表达式中,如果有操作数的值为 NaN,则表达式的结果为 false,除了 NaN != xx != NaN 的结果为 truex 可以是包括 NaN 在内的任意浮点数)。

let x = NaN
var y = 3.14
var z = x < y  // z = false
var v = x != x // v = true
var w = (x < y) == !(x >= y) // w = false

在算术表达式中,包含 NaN 或无穷的表达式的值遵循 IEEE 754 标准:

Rune 类型

仓颉编程语言使用 Rune 类型表示 Unicode code point 。这些值可分为可打印字符、非打印字符(不可显示的字符,如控制符)、特殊字符(如单引号和反斜线)。

  • Rune 类型字面量(Rune Literals)

    Rune 类型字面量的语法定义参见[Rune 类型字面量]。

Rune 类型的字面量由一对单引号和字符组成,如:

    'S'
    '5'
    ' ' // blank

非打印和特殊的字符型字面量使用转义字符(\)定义:

escape charactercharacter
\0Empty character
\\backslash \
\bBackspace
\fWriter
\nnewline
\rEnter
\tHorizontal tab
\vVertical tab
\’single quotation mark
\”double quotation marks
\u{X}Any Unicode code point, where X is a 1-8 digit hexadecimal number
  • Rune 类型支持的操作

    Rune 类型仅支持关系操作符(根据 Unicode code point 编码进行比较)。

    main() {
        'A'=='A' 	// result: true
        'A'!='A' 	// result: false
        'A'<'a' 	// result: true
        'A'<='A' 	// result: true
        'A'>'a' 	// result: false
        'A'>='A' 	// result: true
        return 0
    }
    

Bool 类型

仓颉编程语言使用 Bool 表示布尔类型。

  • Bool 类型字面量(Bool Literal)

    Bool 类型的字面量只有两个:truefalse,分别表示“真”和“假”。

    let bool1: Bool = true
    let bool2: Bool = false
    
  • Bool 类型支持的操作

    仓颉编程语言不支持数值类型与 Bool 之间的类型转换,也禁止非 Bool 的值作为 Bool 值来使用,否则会报编译错误。Bool 类型支持的操作有:赋值操作、部分复合赋值操作(&&=, ||=)、部分关系操作(==, !=)和所有逻辑操作(!, &&, ||)。下面举例说明:

    main() {
        let bool1: Bool = true
        var bool2: Bool = false
        bool2 = true    // assignment
        bool2 &&= bool1 // bool2=true
        bool2 ||= bool1 // bool2=true
        true == false   // return false
        true != false   // return true
        !false          // logical NOT, return true
        true && false   // logical AND, return false
        false || false  // logical OR, return false
        return 0
    }
    

Unit 类型

Unit 类型只有一个值:()

Nothing 类型

Nothing 是一种特殊的类型,它不包含任何值,并且 Nothing 是所有类型的子类型。Nothing 暗示着死代码,例如如果变量为 Nothing 类型,则其永远不会被使用,因此也不需要为其创建内存;如果函数调用的某个参数为 Nothing 类型,则该参数后面的参数不会被求值,函数调用本身也不会被执行;如果块内某条表达式是 Nothing 类型,则其后的所有表达式和声明均不会被执行到。仓颉编译器会对检测到的死代码进行编译告警。

关于哪些表达式具有 Nothing 类型,参见[控制转移表达式]。

String 类型

仓颉编程语言使用 String 表示字符串类型,用于表达文本数据。由一串 Unicode 字符组合而成。

String 字面量

字符串字面量可以分为三类:单行字符串字面量,多行字符串字面量,多行原始字符串字面量。

字符串字面量的语法定义参见[字符串类型字面量]章节。

单行字符串字面量使用一对双引号定义(例如,"singleLineStringLiteral"),双引号中的内容可以是任意数量的任意字符(除了非转义的双引号以及单独出现的 \)。单行字符串字面量只能写在同一行中,不能跨越多行。

let s1 = "" 	// empty string
let s2: String = "Hello Cangjie Lang" 		// define string s2 
var s3 = "\"Hello Cangjie Lang\"" 			// define string s3 containing character "
var s4: String = "Hello Cangjie Lang\n"	// define string s4 containing character \n
// illegal string with character \
let s5 = "hello\"
// illegal string with character "
let s6 = "Hello "Cangjie Lang"

多行字符串字面量以三个双引号(""")开头,并以三个双引号(""")结尾。开头的双引号后需要换行,否则报错。字面量从开头的双引号换行后的第一行开始,到遇到的第一个非转义的三个双引号为止,之间的内容可以是任意数量的任意合法字符(除了非转义的三个双引号 """ 和单独出现的 \)。在当前文件结束之前,如果还没遇到非转义的三个双引号,则编译报错。不同于单行字符串字面量,多行字符串字面量必须跨越多行。

// empty multi-line string
let s1 = """
"""

// Error: there must be a line terminator after the beginning double quotations. 
let errorStr = """abc
""" 

/*
The result of s2 is:
This
    is a multi-line string
*/
let s2 = """
This
    is a multi-line string"""
    
/*
The result of s3 is:
    This
  is a multi-line string
*/
let s3 = """
    This
  is a multi-line string"""
  
/*
The result of s4 is:
This
  is a 
multi-line string
*/
let s4 = """
This
    is a 
multi-line string
"""

/*
The result of s5 is:
This is a 

    multi-line string
*/
let s5 = """
This is a\n
    multi-line string
"""

多行原始字符串字面量 以一个或多个井号(#)和一个双引号开始,并以一个双引号以及和开始相同个数的井号(#)结束。需要注意的是,字符串字面值遇到第一个匹配的双引号以及和开始相同个数的井号即结束匹配。开始的双引号和结束的双引号之间的内容可以是任意数量的任意合法字符(除了一个双引号以及和开始相同个数的井号)。在当前文件结束之前,如果还没遇到匹配的双引号和相同个数的井号,则编译报错。不同于(普通)多行字符串字面量,多行原始字符串字面量中的内容会维持原样(转义字符不会被转义)。

// empty multi-line raw string
let empty1 = #""#
// empty multi-line raw string
let empty2 = ##""##
 
/*
The result of s2 is:

    This
  is a multi-line string
*/
let s2 = ##"
    This
  is a multi-line raw string"##

/*
The result of s3 is:
This is a\n 
    multi-line string
*/
let s3 = #"This is a\n
    multi-line string"#

/*
The result of s4 is:
 This is a "#
*/ 
let s4 = ##" This is a "#
"##
// Error: the beginning and ending numbers of `#` are not matched.
let errorStr1 = ##" error string "#
// Error: the beginning and ending numbers of `#` are not matched.
let errorStr2 = ##" error string "###
// Error: the string literal is terminated when meeting the first matched `"##`.
let errorStr3 = ##" error string "## error "##

String 类型支持使用 == 进行判等(使用 != 进行判不等),两个字符串相等,当且仅当它们对应的 code point sequences 完全相同。

插值字符串

插值字符串 是一种包含一个或多个插值表达式的字符串字面量(不适用于多行原始字符串字面量)。当插值字符串解析为结果字符串时,每个插值表达式所在位置会被对应表达式的结果替换。

每个插值表达式必须用 {} 括号包起来,并加上 $ 前缀。大括号中支持和[块]中一样的表达式声明序列,但不能为空。多行字符串中的插值表达式支持换行。

let obj = "apples"
let count = 10
let interps1 = "There are ${count * count} ${obj}."
// The result of "interps1" is: There are 100 apples.
let error = "Empty sequence is not allowed ${}" // Error

如果字符串中的 $ 符号后面没有 {,则当成普通 $ 符号处理。 如果在 $ 前添加了转义字符 \,那么无论 $ 符号后面是不是 {,都不会被当成插值表达式处理。

let d1 = "The $ sign."
// The result of "d1" is: The $ sign.
let d2 = "The \${v}."
// The result of "d2" is: The ${v}.

插值表达式也支持自定义类型,只要该类型满足 ToString interface 约束。上述已介绍的类型都满足 ToString interface。

Tuple 类型

仓颉编程语言中元组(Tuple)类型是一个由多种不同类型组合而成的不可变(immutable)类型,使用 (Type1, Type2, ..., TypeN) 表示,其中 N 代表元组的维度。关于元组类型,说明如下:

  1. Type1TypeN 可以是任意类型(要求 N 不小于 2,即Tuple 至少是二元的)。

  2. 对于元组的每一维度,例如第 K 维,可以存放任何 TypeK 子类型的实例。

  3. (Type1, Type2, ..., TypeN) 中的 Type1TypeN 均支持使用 == 进行值判等(使用 != 进行值判不等)时,此 Tuple 类型才支持使用 == 进行值判等(使用 != 进行值判不等);否则,此 Tuple 类型不支持 ==!=(如果使用 ==!=,编译报错)。两个同类型的 Tuple 实例相等,当且仅当相同位置(index)的元素全部相等(意味着它们的长度相等)。

Tuple 类型的语法定义为:

tupleType
    : '(' type (',' type)+ ')'
    ;

元组字面量

元组字面量使用格式 (expr1, expr2, ... , exprN) 表示,其中多个表达式之间使用逗号分隔,并且每个表达式可以拥有不同的类型。Tuple literal 的语法定义为:

tupleLiteral
    : '(' expression (',' expression)+ ')'
    ;

元组字面量举例:

(3.14, "PIE") 
(2.4, 3.5) 		
(2, 3, 4) 		
((2, 3), 4)

使用元组做解构

元组也可以用来对另一个元组值进行解构:将一个元组中不同位置处的元素分别绑定到不同的变量。下面的例子展示了如何对多返回值函数的返回值进行解构。

func multiValues(a: Int32, b: Int32): (Int32, Int32) { 
    return (a + b, a - b) // The type of the return value of the function multiValues is (Int32, Int32).
}

main() {
    var (x, y) = multiValues(8,24) // Define an anonymous tuple who has two elements, i.e., x and y.
    print("${x}") // output: 32
    print("${y}") // output: -16
    return 0
}

元组的下标访问

元组支持通过 tupleName[index] 的方式访问其中某个具体位置的元素(index 表示从 0 开始的下标,并且只能是一个 Int64 类型的字面量)。

main() {
    var z = multiValues(8, 24) // the type of z is inferred to be (Int32, Int32)
    print("${z[0]}") // output: 32
    print("${z[1]}") // output: -16
    return 0
}

定义元组类型的变量

在定义元组类型的变量时,可以省略类型标注,由编译器根据程序上下文推断出具体的类型。

let tuplePIE = (3.14, "PIE")     // The type of tuplePIE is inferred to be (Float64, String).
var pointOne = (2.4, 3.5) 		// The type of pointOne is inferred to be (Float64, Float64).
var pointTwo = (2, 3, 4) 		// The type of pointTwo is inferred to be (Int64, Int64, Int64).
var pointThree = ((2, 3), 4) 	// The type of pointThree is inferred to be ((Int64, Int64), Int64).

Range 类型

仓颉编程语言使用 Range<T> 表示 Range 类型,并要求 T 只能实例化为实现了 Comparable interface 和 Countable interface 的类型。Range 类型用于表示一个拥有固定步长的序列,并且是一个不可变(immutable)类型。

每个 Range<T>类型的实例都会包含 startendstep 值。其中,startend 分别表示序列的起始值和终止值,step 表示序列中前后两个元素之间的差值。

Range 类型支持使用 == 进行判等(使用 != 进行判不等),两个相同类型的 Range 实例相等,当且仅当它们同时为“左闭右开”或“左闭右闭”,并且它们的 start 值、end 值和 step 值均对应相等。

创建 Range 实例

存在两种 Range 操作符:....=,分别用于创建“左闭右开”和“左闭右闭”的 Range 实例。它们的使用方式分别为 start..end:stepstart..=end:step(其中 startendstep 均是表达式)。关于这两种表达式,说明如下。

  1. 要求 startend 的类型相同,step 的类型为 Int64

  2. 表达式 start..end:step 中,当 step > 0start >= end,或者 step < 0start <= end 时,start..end:step 返回一个空 Range 实例(不包含任何元素的空序列);如果 start..end:step 的结果是非空 Range 实例,则 Range 实例中元素的个数等于(end-start)/step 向上取整(即大于等于 (end-start)/step 的最小整数)。

  3. 表达式 start..=end:step 中,当 step > 0start > end,或者 step < 0start < end 时,start..=end:step 返回一个空 Range 实例;如果 start..=end:step 的结果是非空 Range 实例,则 Range 实例中元素的个数等于((end-start)/step)+1 向下取整(即小于等于 ((end-start)/step)+1 的最大整数)。

let range1 = 0..10:1 	// Define an half-open range [0,10) (with step = 1) which contains 0, 1, 2, 3, 4, 5, 6, 7, 8 and 9.
let range2 = 0..=10:2 	// Define a closed range [0,10] (with step = 2) which contains 0, 2, 4, 6, 8 and 10.
let range3 = 10..0:-2 	// Define an half-open range [10,0) (with step = -2) which contains 10, 8, 6, 4 and 2.
let range4 = 10..=0:-1 	// Define a closed range [10,0] (with step = -1) which contains 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 and 0.
let range5 = 10..0:1 	// Define an empty range.
let range6 = 0..0:1 	// Define an empty range.
let range7 = 0..=0:1    // Define a closed range [0,0] (with step = 1) which only contains 0.
let range8 = 0..=10:-1 	// Define an empty range.
  1. 表达式 start..end:stepstart..=end:step 中,step 的值不能等于 0
let range9 = 0..10: 0 				// error: the value of the step should not be zero.
let range10 = 0..=10: 0 			// error: the value of the step should not be zero.
  1. 除了下节提到的 Range<Int64> 类型的实例用在下标操作符 [] 中时可以省略 startend 外,其他场景下,startend 均是必选的,只有 step 是可选的(默认 step=1)。
let range11 = 1..10 		 // Define an half-open range [1, 10) with step = 1.
let range12 = 1..=10 		 // Define a closed range [1, 10] with step = 1.	
let range13 = ..10 		     // error: the start value is required.
let range14 = 1.. 		     // error: the end value is required.
let range15 = .. 		     // error: the start and end value are required.

Range 类型的使用

Range 类型的表达式主要用在 for-in 表达式中(参见 [循环表达式]),以及作为下标用于截取片段。需要注意的是:

  • Range<Int64> 类型的实例用在下标操作符 [] 中时,startend 均是可选的。它们的值由使用的上下文决定:在自定义类型上重载下标操作符 [] 且参数类型是 Range<Int64> 时,使用过程中省略 startstart 的值等于 0,省略 endend 的值等于 Int64 的最大值。

定义 Range 类型的变量

在定义 Range<T> 类型的变量时,可以显式添加类型标注,也可以省略类型标注(此时由编译器根据程序上下文推断)。

let range16: Range<Int64> = 0..10 		// Define an half-open range [0,10) with step = 1
let range17: Range<Int64> = -10..10:2 	// Define an half-open range [-10,10) with step = 2
let range18 = 0..=10 		            // Define a closed range [0,10] with step = 1
let range19 = -10..=10:2 	            // Define a closed range [-10,10] with step = 2

Function 类型

函数类型(function type)表示一个具体函数的类型,它同样是 immutable 的。函数类型的语法定义为:

arrowType 
    : parameterTypes '->' type
    ;
parameterTypes
    : '(' (type (',' type)*)? ')'
    ;

参数类型列表 parameterTypes 和返回类型 type 使用 -> 连接。其中,参数类型列表的一对 () 是必选的,() 内可以包含 0 个或多个类型(多个类型使用 , 分隔):

() -> Int32 	// A function type has an empty parameter type list, and its return value type is 'Int32'.
() -> Unit 		// A function type has an empty parameter type list, and its return value type is 'Unit'.
(Float32) -> Int32 	// A function type has one parameter whose type is 'Float32', and its return value type is 'Int32'
(Int32, Int32, Float32) -> Int32 // A function type has three parameters, and its return value type is 'Int32'
(Int32, Int32, Float32) -> (Int32, Int32) // A function type has three parameters, and its return value type is '(Int32, Int32)'	
(Int32, Int32, Float32) -> Unit // A function type has three parameters, and its return value type is 'Unit'.		

仓颉编程语言中,函数是一等公民(first-class citizens),意味着函数可以作为其他函数的参数,可以作为其他函数的返回值,也可以直接赋值给变量。

函数类型不支持使用 ==!=)进行判等(判不等)。更多关于函数的介绍,参看第 5 章函数部分。

enum 类型

enum 类型是一种 immutable 类型,用于将一组相关的值(称为 constructor)组织在一起。声明为 enum 类型的值,其在任何时刻只能取 enum 类型定义中的某个 constructor

定义 enum 类型

定义 enum 类型的语法为:

enumDefinition
    : enumModifier? 'enum' identifier typeParameters? ('<:' superInterfaces)? genericConstraints? '{' enumBody '}'
    ;
    
enumBody
    : '|'? caseBody ('|' caseBody)* (functionDefinition | operatorFunctionDefinition | propertyDefinition | macroExpression)*
    ;
    
caseBody
    : identifier ('(' type (',' type)* ')')?
    ;

其中 enumModifierenum 类型的修饰符(即 public),enum 是关键字,identifierenum 类型的名字,typeParametersgenericConstraints 分别是类型变元列表和类型变元的约束(参考第 9 章泛型相关内容)。enumBody 中可以定义若干 caseBody(即 constructor),每个 constructor 可以没有参数,也可以有一组不同类型的参数,多个 constructor 之间使用 | 分隔,第一个 constructor 之前可以存在一个可选的 |caseBody 后可以定义 enum 的其它成员,包含成员函数、操作符重载函数、成员属性。

enum 类型定义举例:

/*
Define an enum type 'TimeUnit1' who has four constructors: 'Year', 'Month', 'Day' and 'Hour'.
*/
enum TimeUnit1 {
    Year | Month | Day | Hour
}

/* 
'TimeUnit2' has four constructors: 'Year(Int32)', 'Month(Int32, Float32)', 'Day(Int32, Float32, Float32)' and 'Hour(Int32, Float32, Float32, Float32)'.
*/
enum TimeUnit2 { 
    | Year(Float32)
    | Month(Float32, Float32)
    | Day(Float32, Float32, Float32)
    | Hour(Float32, Float32, Float32, Float32) 
}

/* 
Define a generic enum type 'TimeUnit3<T1, T2>' who has four constructors: 'Year(T1)', 'Month(T1, T2)', 'Day(T1, T2, T2)' and 'Hour(T1, T2, T2, T2)'.
*/
enum TimeUnit3<T1, T2> { 
    | Year(T1)
    | Month(T1, T2)
    | Day(T1, T2, T2)
    | Hour(T1, T2, T2, T2) 
}

关于 enum 类型,需要注意的是:

  1. enum 类型只能定义在 top-level

  2. 不支持空括号无参的 constructor 定义,且无参 constructor 的类型为被定义的 enum 类型本身,不视作函数类型。有参的 constructor 具有函数类型,但不能作为一等公民使用。例如:

enum E {
    | mkE // OK. The type of mkE is E but not () -> E.
    | mkE() // Syntax error.
}

enum E1 {
    | A
}
let a = A // ok, a: E1

enum E2 {
    | B(Bool)
}
let b = B // error

enum E3 {
    | C
    | C(Bool)
}
let c = C // ok, c: E3
  1. 作为一种自定义类型,enum 类型默认不支持使用 ==!=)进行判等(判不等)。当然,可以通过重载 ==!=)操作符(参见[操作符重载])使得自定义的 enum 类型支持 ==!=)。

  2. 同一个 enum 中支持 constructor 的重载,但是只有参数个数参与重载,参数类型不参与重载,也就是说,允许同一个 enum 中定义多个同名 constructor,但是要求它们的参数个数不同(没有参数的 constructor 虽不为函数类型,也可以与其它 constructors 重载),例如:

enum TimeUnit4 { 
    | Year
    | Year(Int32) 		                     // ok
    | Year(Float32) 	                     // error: redeclaration of 'Year'
    | Month(Int32, Float32)                  // ok
    | Month(Int32, Int32) 	                 // error: redeclaration of 'Month'
    | Month(Int32) 			                 // ok
    | Day(Int32, Float32, Float32)           // ok
    | Day(Float32, Float32, Float32) 	     // error: redeclaration of 'Day'
    | Day(Float32, Float32) 			     // ok
    | Hour(Int32, Float32, Float32, Float32) // ok
    | Hour(Int32, Int32, Int32, Int32) 	     // error: redeclaration of 'Hour'
    | Hour(Int32, Int32, Int32) 		     // ok
}
  1. enum 类型支持递归和互递归定义,例如:
// recursive enum
enum TimeUnit5 { 
    | Year(Int32)
    | Month(Int32, Float32)
    | Day(Int32, Float32, Float32)
    | Hour(Int32, Float32, Float32, Float32)
    | Twounit(TimeUnit5, TimeUnit5) 
}

// mutually recursive enums
enum E1 {
    A | B(E2)
}
enum E2 {
    C(E1) | D(E1)
}
  1. enum 不可以被继承。

  2. enum 可以实现接口(接口参看第 6 章)。

Enum 值的访问

第一种是通过 enum 名加上 constructor 名的访问方式,例如:

let time1 = TimeUnit1.Year
let time2 = TimeUnit2.Month(1.0, 2.0)
let time3 = TimeUnit3<Int64, Float64>.Day(1, 2.0, 3.0)

第二种是省略 enum 名的方式,例如:

let time4 = Year                             // syntax sugar of 'TimeUnit1.Year'
let time5 = Month(1.0, 2.0)                  // syntax sugar of 'TimeUnit2.Month(1.0, 2.0)'
let time6 = Day<Int64, Float64>(1, 2.0, 3.0) // syntax sugar of 'TimeUnit3<Int64, Float64>.Day(1, 2.0, 3.0)'

注:由于仓颉语言支持对泛型参数的类型推导,所以在使用上述方式一与方式二访问时,泛型参数 <T> 均可省略。

关于第二种使用方式需要满足以下规则。

当不存在其它变量名、函数名、类型名、包名冲突的时候,可以省略类型前缀使用。否则优先选择变量名、函数名、类型名或包名。

package p
}

func g() {
    let a = A // ok, find p.A
    let b = E.A // ok
    let c = p.A // ok
    F(1) // ok, find p.F
    E.F(1) // ok
    p.F(1) // ok
    let x: C // ok, find p.C
    let y: p.C // ok
}

enum 构造器在省略类型前缀使用时,可以在构造器名称后面声明 enum 类型的泛型参数。

enum E<T> {
    | A(T)
    | B(T)
}

func f() {
    let a = A(1)        // ok, a: E<Int64>
    let b = A<Int32>(2) // ok, b: E<Int32>
}

enum 解构

使用 match 表达式和 enum pattern 可以实现对 enum 的解构(详见4.4.1 模式章节)。

对前文中定义的 TimeUnit1TimeUnit2 的解构,举例如下:

let time12 = TimeUnit1.Year
let howManyHours = match (time12) {
    case Year => 365 * 24 // matched
    case Month => 30 * 24
    case Day => 24
    case Hour => 1
}

let time13 = TimeUnit2.Month(1.0, 1.5)
let howManyHours = match (time13) {
    case Year(y) => y * 365.0 * 24.0
    case Month(y, m) => y * 365.0 * 24.0 + m * 30.0 * 24.0 // matched
    case Day(y, m, d) => y * 365.0 * 24.0 + m * 30.0 * 24.0 + d * 24.0
    case Hour(y, m, d, h) => y * 365.0 * 24.0 + m * 30.0 * 24.0 + d * 24.0 + h
}

enum 的其它成员

enum 中也可以定义成员函数、操作符函数和成员属性。

  • 定义普通成员函数参见[函数定义]。
  • 定义操作符函数的语法参见[操作符重载]。
  • 定义成员属性的语法参见[属性的定义]。

以下是一些 enum 定义的简单示例。

enum Foo {
    A | B | C
    func f1() {
        f2(this)
    }
    static func f2(v: Foo) {
        match (v) {
            case A => 0
            case B => 1
            case C => 2
        }
    }
    prop item: Int64 {
        get() {
            f1()
        }
    }
}

main() {
    let a = Foo.A
    a.f1()
    Foo.f2(a)
    a.item
}

enum 中的 constructor、静态成员函数、实例成员函数之间不能重载。因此,enum 的 constructor、静态成员函数、实例成员函数、成员属性之间均不能重名。

Option 类型

Option type 是一种泛型 enum 类型:

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

SomeNone 是两个 constructorSome(T) 表示一种有值的状态,None 表示一种无值的状态。类型变元 T 被实例化为不同的类型,会得到不同的 Option type,例如:Option<Int32>Option<String>等。

Option 类型的另一种写法是在类型名前加 ?,即对于任意类型 Type?Type 等价于 Option<Type>。例如,?Int32 等价于 Option<Int32>?String 等价于 Option<String> 等等。

关于 Option 类型,需要注意:

  1. 虽然 TOption<T> 是不同的类型,但是当明确知道某个位置需要的是 Option<T> 类型的值时,可以直接传一个 T 类型的值,编译器会将 T 类型的值封装成 Option<T> 类型的 Some constructor。例如,下面的定义是合法的。
let v1: ?Int64 = 100
let v2: ??Int64 = 100
  1. 对于两个不相等的类型 T1T2,即使它们之间拥有子类型关系,Option<T1>Option<T2>之间也不会构成子类型关系。

Option 类型的使用遵循泛型 enum 类型的使用规则。例如,可以定义一系列不同 Option 类型的变量:

let opInt32_1 = Some(100)           // The type of 'opInt32_1' is 'Option<Int32>'
let opInt32_2 = Option<Int32>.None  // The type of 'opInt32_2' is 'Option<Int32>'
let opChar = Some('m')              // The type of 'opChar' is 'Option<Rune>'
let opBool = Option<Bool>.None      // The type of 'opBool' is 'Option<Bool>'
let opEnum = Some(TimeUnit1.Year)   // The type of 'opEnum' is 'Option<TimeUnit1>'
Option 值的解构

提供若干方式以方便对 Option 值的解构:模式匹配、getOrThrow 函数、coalescing 操作符(??)和问号操作符(?)。

  1. Option 类型是一种 enum 类型,因此可以使用模式匹配表达式中的 enum pattern(详见 [enum 模式])实现 Option 值的解构。
let number1 = match (opInt32_1) {
    case Some(num) => num // matched
    case None => 0
}

let number2 = match (opInt32_2) {
    case Some(num) => num 
    case None => 0        // matched
}

let enumValue = match (opEnum) {
    case Some(tu) => match (tu) {
                          case Year => "Year"   // matched
                          case Month => "Month"
                          case Day => "Day"
                          case Hour => "Hour"
                      }
    case None => "None"
}
  1. 对于 Option<T> 类型的表达式 e,通过调用函数 getOrThrow()getOrThrow(exception: ()->Exception) 实现对 e 的解构:如果 e 的值等于 Option<T>.Some(v),则 e.getOrThrow() (或 e.getOrThrow(lambdaArg))的值等于 v 的值;如果 e 的值等于 Option<T>.None,则 e.getOrThrow() 在运行时抛出 NoneValueException 异常(e.getOrThrow(lambdaArg) 在运行时抛出 lambdaArg 中指定的异常)。
let number1 = opInt32_1.getOrThrow()                 // number1 = 100
let number2 = opInt32_2.getOrThrow()                 // throw NoneValueException
let number3 = opInt32_2.getOrThrow{ MyException("Get None value") } // throw MyException
  1. 对于 Option<T> 类型的表达式 e1T 类型的表达式 e2,表达式 e1 ?? e2 的类型为 T。当 e1 的值等于 Option<T>.Some(v) 时,e1 ?? e2 的值等于 v 的值;当 e1 的值等于 Option<T>.None 时,e1 ?? e2 的值等于 e2 的值。关于 ?? 操作符的详细介绍,参见[coalescing 表达式]。
let number1 = opInt32_1 ?? 0  // number1 = 100
let number2 = opInt32_2 ?? 0  // number2 = 0
  1. 问号操作符( ? )是一元后缀操作符。? 需和后缀操作符 .(), {}[] 一起使用,以实现 Option<T> 对这些后缀操作符的支持。对于一个 Option 类型的表达式 ee?.b 表示当 e 为 Some 时得到 Option<T>.Some(b),否则得到 Option<T>.None(T 为 b 的类型),其它操作符同理。关于 ? 操作符的详细介绍,参见[问号操作符]。
class C {
    var item = 100
}
let c = C()
let c1 = Some(c)
let c2 = Option<C>.None
let r1 = c1?.item   // r1 = Option<Int64>.Some(100)
let r2 = c2?.item   // r2 = Option<Int64>.None

可变类型

下面依次介绍仓颉编程语言中的可变类型。

Array 类型

仓颉编程语言使用泛型类型 Array<T> 表示 Array 类型,Array 类型用于存储一系列相同类型(或者拥有公共父类)的元素。关于 Array<T>,说明如下:

  1. Array<T> 中的元素是有序的,并且支持通过索引(从 0 开始)访问其中的元素。

  2. Array 类型的长度是固定的,即一旦建立一个 Array 实例,其长度是不允许改变的。

  3. Array 是引用类型,定义为引用类型的变量,变量名中存储的是指向数据值的引用,因此在进行赋值或函数传参等操作时, Array 拷贝的是引用。

  4. 类型变元 T 被实例化成不同的类型,会得到不同的 Array 类型,例如,Array<Int32>Array<Float64> 分别表示 Int32 类型的 ArrayFloat64 类型的 Array

  5. Array<T> 中的 Type 类型支持使用 == 进行值判等(使用 != 进行值判不等)时,Array<T> 类型支持使用 == 进行值判等(使用 != 进行值判不等);否则,Array<T> 类型不支持==!=(如果使用 ==!=,编译报错)。两个同类型的 Array<T> 实例值相等,当且仅当相同位置(index)的元素全部相等(意味着它们的长度相等)。

  6. 多维 Array 定义为 ArrayArray ,使用 Array<Array<...>> 表示。例如,Array<Array<Int32>> 表示 Int32 类型的二维 Array

  7. ElementType 拥有子类型时,Array<ElementType> 中可以存放任何 ElementType 子类型的实例,例如 Array<Object> 中可以存放任何 Object 子类 的实例。

创建 Array 实例

存在 2 种创建 Array 实例的方式:

构造函数
// Array<T>()
let emptyArr1 = Array<Int64>()  // create an empty array whose type is Array<Int64>
let emptyArr2 = Array<String>() // create an empty array whose type is Array<String>

// Array<T>(size: Int64, initElement: (Int64)->T)
let array3 = Array<Int64>(3) { i => i * 2 } // 'array3' has 3 elememts: 0, 2, 4
let array4 = Array<String>(2) { i => "$i" } // 'array4' has 2 elememts: "0", "1" 
Array 字面量

Array 字面量使用格式 [element1, element2, ... , elementN] 表示,其中多个 element 之间使用逗号分隔,每个 element 可以是一个 expressionElement (普通表达式)。Array 字面量的语法定义为:

arrayLiteral : '[' (elements)? ']' ;
elements    : element (',' element)* ;
element     : expression ;

Array 字面量每个元素都是一个表达式:

let emptyArray: Array<Int64> = []    // empty Array<Int64>
let array0 = [1, 2, 3, 3, 2, 1]      // array0 = [1, 2, 3, 3, 2, 1]
let array1 = [1 + 3, 2 + 3, 3 + 3]   // array1 = [4, 5, 6]
  • 在上下文没有明确的类型要求时,若所有 element 的最小公共父类型是 TArray 字面量的类型是 Array<T>
  • 在上下文有明确的类型要求时,此时要求所有 element 的类型都是上下文所要求的 element 类型的子类型。

访问 Array 中的元素

对于 Array 类型的实例 arr,支持以下方式访问 arr 中的元素:

访问某个位置处的元素:通过 arr[index1]...[indexN]的方式访问具体位置处的元素(其中 index1,...,indexN 是索引值的表达式,它们的类型均为 Int64),例如:

let array5 = [0, 1]
let element0 = array5[0]      // element0 = 0
array5[1] = 10                // change the value of the second element of 'array5' through index

let array6 = [[0.1, 0.2], [0.3, 0.4]]
let element01 = array6[0][1]  // element01 = 0.2
array6[1][1] = 4.0            // change the value of the last element of 'array6' through index

迭代访问:通过 for-in 表达式(见表达式章节)迭代访问 arr 中的元素。例如:

func access() {
    let array8 = [1, 8, 0, 1, 0]
    for (num in array8) {
        print("${num}")   // output: 18010
    }
}

访问 Array 的大小

支持通过 arr.size 的方式返回 arr 中元素的个数。

let array9 = [0, 1, 2, 3, 4, 5]
let array10 = [[0, 1, 2], [3, 4, 5]]
let size1 = array9.size  // size1 = 6
let size2 = array10.size // size2 = 2

Array 的切片

Range<Int64> 类型的值用作 Array 下标时,用于截取 Array 的一个片段(称之为 slicing)。需要注意的是:

  • step 必须是 1,否则运行时会抛出异常。
  • slicing 返回的类型仍然是相同的 Array 类型,并且是原 Array 的引用。对切片中元素的修改会影响到原数组。
  • 当使用 start..end 作为 Array 下标时,如果省略 start,则将 start 的值设置为 0,如果省略 end,则将 end 的值设置为 Array 的长度值。
  • 当使用 start..=end 作为 Array 下标时,如果 start 被省略,则将 start 的值设置为 0start..=end 形式的 end 不能省略。
  • 如果下标是一个空 range 值,那么返回的是一个空的 Array
let array7 = [0, 1, 2, 3, 4, 5]

func slicingTest() {
    array7[0..5]      // [0, 1, 2, 3, 4]
    array7[0..5:1]    // [0, 1, 2, 3, 4]
    array7[0..5:2]    // runtime exception
    array7[5..0:-1]   // runtime exception
    array7[5..0:-2]   // runtime exception
    array7[0..=5]     // [0, 1, 2, 3, 4, 5]
    array7[0..=5:1]   // [0, 1, 2, 3, 4, 5]
    array7[0..=5:2]   // runtime exception
    array7[5..=0:-1]  // runtime exception
    array7[5..=0:-2]  // runtime exception
    array7[..4]       // [0, 1, 2, 3]
    array7[2..]       // [2, 3, 4, 5]
    array7[..]        // [0, 1, 2, 3, 4, 5]
    array7[..4:2]     // error: the 'range step' is not allowed here.
    array7[2..:-2]    // error: the 'range step' is not allowed here.
    array7[..:-1]     // error: the 'range step' is not allowed here.

    array7[..=4]      // [0, 1, 2, 3, 4]
    array7[..=4:2]    // error: the 'range step' is not allowed here.

    array7[0..5:-1]   // runtime exception 
    array7[5..=0]     // [] 
    array7[..0]       // [] 
    array7[..=-1]     // []
    let temp: Array<Int64> = array7[..]
    temp[0] = 6 // temp == array7 == [6, 1, 2, 3, 4, 5]
}

当使用 slicing 进行赋值时,支持两种不同的用法:

  • 如果 = 右侧表达式的类型是数组的元素类型,会将这个表达式的值作为元素覆盖切片范围的元素。
  • 如果 = 右侧表达式的类型与数组的类型相同,会将这个数组拷贝覆盖当前切片范围,这时要求右侧表达式的 size 必须与切片范围相同,否则运行时会抛出异常。
let arr = [1, 2, 3, 4, 5]
arr[..] = 0
// arr = [0, 0, 0, 0, 0]
arr[0..2] = 1
// arr = [1, 1, 0, 0, 0]
arr[0..2] = [2, 2]
// arr = [2, 2, 0, 0, 0]
arr[0..2] = [3, 3, 3] // runtime exception
arr[0..2] = [4] // runtime exception

let arr2 = [1, 2, 3, 4, 5]
arr[0..2] = arr2[0..2] // ok
// arr = [1, 2, 0, 0, 0]
arr[0..2] = arr2 // runtime exception
arr[..] = arr2

VArray 类型

仓颉编程语言使用泛型类型 VArray<T, $N> 表示 VArray 类型,VArray 类型用于存储一系列相同类型(或者拥有公共父类型)的元素。关于 VArray<T, $N>,说明如下:

  1. VArray<T, $N> 中的元素是有序的,并且支持通过索引(从 0 开始)访问其中的元素。如果提供的索引大于或等于其长度,在编译期间能够推导出下标的则编译报错,否则在运行时抛出异常。

  2. VArray<T, $N> 类型的长度是类型的一部分。其中 N 表示 VArray 的长度,它必须是一个整数字面量,通过固定语法 $ 加数字进行标注。当提供的整数字面量为负数时,编译期间进行报错。

  3. VArray 是值类型,定义为值类型的变量,变量名中存储的是数据值本身,因此在进行赋值或函数传参等操作时,VArray 类型拷贝的是值。

  4. 当类型变元 T 被实例化成不同的类型,或 $N 标注长度不相等时,会得到不同的 VArray 类型。例如 VArray<Int32, $5>VArray<Float64, $5> 是不同类型的 VArrayVArray<Int32, $2>VArray<Int32, $3> 也是不同类型的 VArray

  5. VArray 的泛型变元 TVArray 类型时,表示一个多维的 VArray 类型。

创建 VArray 实例

VArray 同样可以使用 Array 字面量来创建新的实例。这种方式只能在上下文中能明确该字面量是 VArray 类型时才可以使用,否则仍然会优先推断为 Array 类型。

VArray 的长度必须为 Int64 的字面量类型,且必须与提供的字面量 Array 长度一致,否则会编译报错。

let arr1: VArray<Int64, $5> = [1,2,3,4,5] 
let arr2: VArray<Int16, $0> = []
let arr3: VArray<Int16, $0> = [1] // error
let arr4: VArray<Int16, $-1> = [] // error

除此以外 VArray 也可以使用构造函数的形式创建实例。

// VArray<T, $N>(initElement: (Int64)->T)
let arr5: VArray<Int64, $5> = VArray<Int64, $5> { i => i } // [0, 1, 2, 3, 4]
// VArray<T, $N>(item!: T)
let arr6: VArray<Int64, $5> = VArray<Int64, $5>(item: 0) // [0, 0, 0, 0, 0]

VArray 总是不允许缺省类型参数 <T, $N>

访问 VArray 中的元素

对于 VArray 类型的实例 arr,支持以下方式访问 arr 中的元素:

通过 arr[index1]...[indexN] 的方式访问具体位置处的元素(其中 index1,...,indexN 是索引值的表达式,它们的类型均为 Int64),例如:

需要注意的是,VArray 的下标取值操作会返回指定元素的拷贝。这意味着如果元素是值类型,那么下标取值会得到一个不可修改的新实例。对于多维 VArray,我们也不能通过 arr[n][m] = e 的方式来修改内层的 VArray,因为通过 arr[n] 所获取的内层 VArray 是一个经过拷贝的新的 VArray 实例。

var arr7: VArray<Int64, $2> = [0, 1]
let element0 = arr7[0]      // element0 = 0
arr7[1] = 10                // change the value of the second element of 'array5' through index

// Get and Set of multi-dimensional VArrays.
var arr8: VArray<VArray<Int64, $2>, $2> = [[1, 2], [3, 4]]
let arr9: VArray<Int64, $2> = [0, 1]
let element1 = arr8[1][0]   // element1 = 3
arr8[1][1] = 5              // error: function call returns immutable value
arr8[1] = arr9              // arr8 = [[1, 2], [0, 1]]

获取 VArray 的长度

支持通过 arr.size 的方式返回 arr 中元素的个数。

let arr9: VArray<Int64, $6> = [0, 1, 2, 3, 4, 5]
let size = arr9.size  // size = 6

VArray 在函数签名中时

VArray 作为函数的参数或返回值时,需要标注 VArray 的长度:

func mergeArray<T>(a: VArray<T,$2>, b: VArray<T, $3>): VArray<T, $5>

struct 类型

struct 类型是一种 mutable 类型,在其内部可定义一系列的成员变量和成员函数,定义 struct 类型的语法为:

structDefinition
    : structModifier? 'struct' identifier typeParameters? ('<:' superInterfaces)? genericConstraints? structBody
    ;

structBody
    : '{'
        structMemberDeclaration*
        structPrimaryInit? 
        structMemberDeclaration*
      '}'
    ; 

structMemberDeclaration
    : structInit
    | staticInit
    | variableDeclaration
    | functionDefinition
    | operatorFunctionDefinition
    | macroExpression
    | propertyDefinition
    ;    

其中 structModifierstruct 的修饰符,struct 是关键字,identifierstruct 类型的名字,typeParametersgenericConstraints 分别是类型变元列表和类型变元的约束(参考第 9 章泛型相关内容)。structBody 中可以定义成员变量(variableDeclaration),成员属性(propertyDefinition),主构造函数(structPrimaryInit),构造函数(structInit)和成员函数(包括普通成员函数和操作符函数)。

关于 struct 类型,需要注意的是:

  1. struct 是值类型,定义为值类型的变量,变量名中存储的是数据值本身,因此在进行赋值或函数传参等操作时,struct 类型拷贝的是值。
  2. struct 类型只能定义在 top-level
  3. 作为一种自定义类型,struct 类型默认不支持使用 ==!=)进行判等(判不等)。当然,可以通过重载 ==!=)操作符(参见[操作符重载])使得自定义的 struct 类型支持 ==!=)。
  4. struct 不可以被继承。
  5. struct 可以实现[接口]。
  6. 如果一个 struct 类型中的某个(或多个)非静态成员变量的类型中引用了 struct 自身,则称此 struct 为递归 struct 类型。对于多个 struct 类型,如果它们的非静态成员变量的类型之间构成了循环引用,则称这些 struct 类型间构成了互递归。递归(或互递归)定义的 struct 类型是非法的,除非每条递归链 T_1, T_2, ..., T_N 上都存在至少一个 T_i 被封装在 Class、Interface、Enum 或函数类型中,也就是说,可以使用 Class、Interface、Enum 或函数类型使递归(或互递归)的 struct 定义合法化。

struct 类型定义举例:

struct Rectangle1 { 
    let width1: Int32
    let length1: Int32
    let perimeter1: () -> Int32

    init (width1: Int32, length1: Int32) {
        this.width1 = width1
        this.length1 = length1
        this.perimeter1 = { => 2 * (width1 + length1) }
    }
    init (side: Int32) {
        this(side, side)
    }
    func area1(): Int32 { width1 * length1 }
}

// Define a generic struct type.
struct Rectangle2<T> {
    let width2: T
    let length2: T

    init (side: T) {
      this.width2 = side
      this.length2 = side
    }

    init (width2!: T, length2!: T) {
        this.width2 = width2
        this.length2 = length2
    }
}

递归和互递归 struct 类型定义举例:

struct R1 { // error: 'R1' cannot have a member that recursively contains it
    let other: R1
}
struct R2 { // ok
    let other: Array<R2>
}

struct R3 { // error:  'R3' cannot have a member that recursively contains it
    let other: R4
}
struct R4 { // error:  'R4' cannot have a member that recursively contains it
    let other: R3
}

struct R5 { // ok
    let other: E1
}
enum E1 { // ok
    A(R5)
}

struct 成员变量

定义成员变量的语法为:

variableDeclaration
    : variableModifier* NL* (LET | VAR) NL* patternsMaybeIrrefutable ((NL* COLON NL* type)? (NL* ASSIGN NL* expression) | (NL* COLON NL* type))
    ;

在定义成员变量的过程中,需要注意的是:

  1. 在主构造函数之外定义的成员变量可以有初始值,也可以没有初始值。如果有初始值,初始值表达式中仅可以引用定义在它之前的已经初始化的成员变量,以及 struct 中的静态成员函数。

构造函数

在仓颉编程语言中,有两种构造函数:主构造函数和 init 构造函数 (简称构造函数)。

主构造函数

主构造函数的语法定义如下:

structPrimaryInit
    : (structNonStaticMemberModifier)? structName '('
      structPrimaryInitParamLists? ')'
 	  '{'
 	       expressionOrDeclarations?
 	  '}'
	;

structName
    : identifier
    ;

structPrimaryInitParamLists
    : unnamedParameterList  (','  namedParameterList)? 
       (',' structNamedInitParamList)?
    | unnamedParameterList (',' structUnnamedInitParamList)? 
       (',' structNamedInitParamList)?
    | structUnnamedInitParamList (',' structNamedInitParamList)?
    | namedParameterList (',' structNamedInitParamList)?
    | structNamedInitParamList
    ;

structUnnamedInitParamList
    : structUnnamedInitParam (',' structUnnamedInitParam)*
    ;

structNamedInitParamList
    : structNamedInitParam (',' structNamedInitParam)*
    ;

structUnnamedInitParam
    : structNonStaticMemberModifier?  ('let' | 'var')  identifier ':' type
    ;

structNamedInitParam
    : structNonStaticMemberModifier?   ('let' | 'var')  identifier '!' ':' type
      ('=' expression)?
    ;

主构造函数的定义包括以下几个部分:

1、修饰符:可选。可以被所有访问修饰符修饰,默认的可访问性为 internal。详细内容请参考包和模块管理章节访问修饰符

2、主构造函数名:与类型名一致。主构造函数名前不允许使用 func 关键字。

3、形参列表:主构造函数与 init 构造函数不同的是,前者有两种形参:普通形参和成员变量形参。普通形参的语法和语义与函数定义中的形参一致。

引入成员变量形参是为了减少代码冗余。成员变量形参的定义,同时包含形参和成员变量的定义,除此之外还表示了通过形参给成员变量赋值的语义。省略的定义和表达式会由编译器自动生成。

  • 成员变量形参的语法和成员变量定义语法一致,此外,和普通形参一样支持使用!来标注是否为命名形参;

  • 成员变量形参的修饰符有:public, private, protected, internal;详细内容请参考包和模块管理章节访问修饰符

  • 成员变量形参只允许非静态成员变量,即不允许使用 static 修饰;

  • 成员变量形参不能与主构造函数外的成员变量同名;

  • 成员变量形参可以没有初始值。这是因为主构造函数会由编译器生成一个对应的构造函数,将在构造函数体内完成将形参给成员变量的赋值;

  • 成员变量形参也可以有初始值,初值表达式中可以引用该成员变量定义之前已经定义的其他形参或成员变量(不包括定义在主构造函数外的实例成员变量),但不能修改这些形参和成员变量的值。需要注意的是,成员变量形参的初始值只在主构造函数中有效,不会在成员变量定义中包含初始值;

  • 成员变量形参后不允许出现普通形参,并且要遵循函数定义时的参数顺序,命名形参后不允许出现非命名形参。

4、主构造函数体:主构造函数不允许调用本 struct 中其它构造函数。主构造函数体内允许写声明和表达式,其中声明和表达式需要满足 init 构造函数的要求。

主构造函数定义的例子如下:

struct Test{
    static let counter: Int64 = 3
    let name: String = "afdoaidfad"
    private Test( 
        name: String,             // regular parameter  
        annotation!: String = "nnn",   // regular parameter  
        var width!: Int64 = 1,     // member variable parameter with initial value
        private var length!: Int64,    // member variable parameter
        private var height!: Int64 = 3   // member variable parameter 
    ) {}
}

主构造函数是 init 构造函数的语法糖,编译器会自动生成与主构造函数对应的构造函数和成员变量的定义。自动生成的构造函数形式如下:

  • 其修饰符与主构造函数修饰符一致;
  • 其形参从左到右的顺序与主构造函数形参列表中声明的形参一致;
  • 构造函数体内形式如下:
    • 首先是对成员变量的赋值,语法形式为 this.x = x, 其中 x 为成员变量名;
    • 然后是主构造函数体的其它代码;
struct B<X,Y> { 
    B(
        x: Int64,          // primary constructor, it's name is the same as the struct
        y: X,
        v!: Int64 = 1,     // regular parameter
        private var z!: Y  // member variable parameter
    ) {}

    /* The corresponding init constructor with primary constructor auto-generated
    by compiler.
    
    private var z: Y    // auto generated member variable definition
    init( x: Int64, y: X, v!: Int64 = 1, z!: Y) { // auto generated named parameter definition
        this.z = z // auto generated assign expression of member variable
    }
    */
}

一个 struct 最多可以定义一个主构造函数,除了主构造函数之外,可以照常定义其他构造函数,但要求其他构造函数必须和主构造函数所对应的构造函数构成重载。

init 构造函数

除了主构造函数,还可以自定义构造函数,构造函数使用关键字 init 加上参数列表和函数体的方式定义。一个 struct 中可以定义多个构造函数,但要求它们和主构造函数所对应的构造函数必须构成重载(关于函数重载,请参见[函数重载定义])。另外:

  1. 构造函数的参数可以有默认值。禁止使用实例成员变量 this.variableName 及其语法糖 variableName 作为构造函数参数的默认值;

  2. 构造函数在所有实例成员变量完成初始化之前,禁止使用隐式传参或捕获了 this 的函数或 lambda,但允许使用 this.variableName 或其语法糖 variableName 来访问已经完成初始化的成员变量 variableName

  3. 构造函数中的 lamdba 和嵌套函数不能捕获 thisthis 也不能作为表达式单独使用。

  4. 构造函数中允许通过 this 调用其他构造函数。如果调用,必须在构造函数体内的第一个表达式处,在此之前不能有任何表达式或声明。在构造函数体外,不允许通过 this 调用表达式来调用构造函数。

  5. 若构造函数没有显式调用其他构造函数,则需要确保 return 之前本 struct 声明的所有实例成员变量均完成初始化,否则编译报错;

  6. 编译器会对所有构造函数之间的调用关系进行依赖分析,循环的调用将编译报错。

  7. 构造函数的返回类型为 Unit

如果一个 struct 既没有定义主构造函数,也没有定义 init 构造函数,则会尝试生成一个(public 修饰的)无参构造函数。如果存在本 struct 的实例成员变量没有初始值,则编译报错。

struct 的其它成员

struct 中也可以定义成员函数、操作符函数、成员属性和静态初始化器。

  • 定义普通成员函数参见[函数定义]。
  • 定义操作符函数的语法参见[操作符重载]。
  • 定义成员属性的语法参见[属性的定义]。
  • 定义静态初始化器的语法参见[静态初始化器]。

struct 中的修饰符

struct 及其成员可以使用访问修饰符进行修饰,详细内容请参考包和模块管理章节访问修饰符

成员函数和变量可以使用 static 修饰,这些成员是静态成员,静态成员属于 struct 类型的成员,而不是 struct 实例的成员。在 struct 定义外部,实例成员变量和实例成员函数只能通过 struct 实例访问,静态成员变量和静态成员函数只能通过 struct 类型的名字访问。

另外函数还可以被 mut 修饰, mut 函数是一种特殊的实例成员函数。mut 函数详细介绍参考函数章节mut 函数

struct 构造函数以及主构造函数内部定义的成员变量只能使用访问修饰符修饰,不能使用 static 修饰。

struct 的实例化

定义完 struct 类型之后,就可以创建对应的 struct 实例。struct 实例的定义方式按照是否包含类型变元可分为两种:

  1. 定义非泛型 struct 的实例:StructName(arguments)。其中 StructNamestruct 类型的名字,arguments 为实参列表。StructName(arguments)会根据重载函数的调用规则(参见[函数重载决议])调用对应的构造函数,然后生成 StructName 的一个实例。举例如下:
let newRectangle1_1 = Rectangle1(100, 200) // Invoke the first custom constructor.
let newRectangle1_2 = Rectangle1(300)      // Invoke the second custom constructor.
  1. 定义泛型 struct 的实例:StructName<Type1, Type2, ... , TypeK>(labelValue1, labelValue2, ... , labelValueN)。与定义非泛型 struct 的实例的差别仅在于需要对泛型参数进行实例化。举例如下:
let newRectangle2_1 = Rectangle2<Int32>(100) // Invoke the custom constructor.
let newRectangle2_1 = Rectangle2<Int32>(width2: 10, length2: 20) // Invoke another custom constructor.

最后,需要注意的是:对于 struct 类型的变量 structInstance,如果它使用 let 定义,不支持通过 structInstance.varName = expr 的方式修改成员变量 varName 的值(即使 varName 使用 var 定义);如果 structInstance 使用 var 定义,并且 varName 同样使用 var 定义,支持通过 structInstance.varName = expr 的方式修改成员变量 varName 的值。

class 类型和 interface 类型

classinterface 是引用类型,定义为引用类型的变量,变量名中存储的是指向数据值的引用,因此在进行赋值或函数传参等操作时, classinterface 拷贝的是引用。

请参见[类和接口]

类型转换

作为一种强类型(strongly typed)语言,仓颉编程语言仅支持显式类型转换(亦称强制类型转换)。对于值类型,支持使用 valueType(expr)实现将表达式 expr 的类型转换成 valueType;对于 classinterface,通过使用 as 操作符(见 [as 操作符])实现静态类型转换。对于值类型,我们说 valueTypeAvalueTypeB 是可转换的,是指定义了将 valueTypeA 转换成 valueTypeB 的转换规则,对于没有定义转换规则的两个值类型,我们称它们是不可转换的。对于 classinterface,如果两个类型在继承关系图上存在父子类型关系,那么这两个类型之间就是可转换的(当然,转换的结果也有可能是失败的),否则,这两个类型之间就是不可转换的。

下面分别介绍值类型之间的类型转换规则和 class/interface 之间的类型转换规则。对于其它未提及的类型,仓颉不支持通过上述两种方式实现它们之间的类型转换。

Value Types 之间的类型转换

对于数值类型,支持如下类型转换(未列出即表示不支持):

  1. Rune 类型到 UInt32 类型的转换;

  2. 整数类型(包括 Int8Int16Int32Int64IntNativeUInt8UInt16UInt32UInt64UIntNative)到 Rune 类型的转换。

  3. 所有数值类型(包括 Int8Int16Int32Int64IntNativeUInt8UInt16UInt32UInt64UIntNativeFloat16Float32Float64)之间的双向转换。

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 的值)或运行时抛异常。

main() {
    var c: Rune = 'a'
    var num: UInt32 = 0
    num = UInt32(c) // num = 97
    num -= 32       // num = 65
    c = Rune(num)   // c = `A`
    return 0
}

为了保证类型安全,仓颉编程语言不支持数值类型之间的隐式类型转换(数值字面量的类型由上下文推断得到,这种情形并不是隐式类型转换),要实现一种数值类型到另外一种数值类型的转换,必须使用显式的方式:NumericType(expr),表示将 expr 的类型强制转换为 NumericType 类型(NumericType 表示任意一种数值类型),如转换成功,会返回一个新的从 expr 构造而来的类型为 NumericType 的值。数值类型转换的语法定义为:

numericTypeConvExpr
    : numericTypes '(' expression ')'
    ;
numericTypes
    : 'Int8' | 'Int16' | 'Int32' | 'Int64' | 'IntNative' | 'UInt8' | 'UInt16' | 'UInt32' | 'UInt64' | 'UIntNative' | 'Float16' | 'Float32' | 'Float64'
    ;

如果根据数值类型所占 bit 数来定义数值类型间的“大小关系”(所占 bit 数越多,类型越“大”,所占 bit 数越少,类型越“小”),则仓颉编程语言支持以下类型转换:

a) 有符号整数类型之间的双向转换:小转大时数值结果不变,大转小时若超出小类型的表示范围,则根据上下文中的属性宏确定溢出处理策略(默认使用抛出异常的策略),不同溢出策略详见[算术表达式]。下面以 Int8Int16 之间的转换为例进行说明(溢出时,使用抛异常的处理策略):

main() {
    var i8Number: Int8 = 127
    var i16Number: Int16 = 0
    i16Number = Int16(i8Number) 	// ok: i16Number = 127
    i8Number = Int8(i16Number) 		// ok: i8Number = 127
    i16Number = 128
    i8Number = Int8(i16Number) 		// throw an ArithmeticException
    return 0
}

b) 无符号整数类型之间的双向转换:规则同上。以 UInt16UInt32 之间的转换为例进行说明(其他情况遵循一样的规则):

main() {
    var u16Number: UInt16 = 65535
    var u32Number: UInt32 = 0
    u32Number = UInt32(u16Number) 	// ok: u32Number = 65535
    u16Number = UInt16(u32Number) 	// ok: u16Number = 65535
    u32Number = 65536
    u16Number = UInt16(u32Number)   // throw an ArithmeticException
    return 0
}

c) 浮点类型之间的双向转换:使用 round-to-nearest 模式。举例说明 Float32Float64 之间的转换:

main() {
    var f32Number: Float32 = 1.1
    var f64Number: Float64 = 0.0
    f64Number = Float64(f32Number)   	// f64Number = 1.100000023841858
    f32Number = Float32(f64Number)    	// f32Number = 1.1
    f64Number = 1.123456789
    f32Number = Float32(f64Number) 	    // f32Number = 1.1234568
    f32Number = 4.4E38 					// f32Number = POSITIVE_INFINITY
    f64Number = Float64(f32Number) 	    // f64Number = POSITIVE_INFINITY
    f64Number = 4.4E38
    f32Number = Float32(f64Number) 	    // f32Number = POSITIVE_INFINITY
    f64Number = Float64(f32Number*0.0)
    f32Number = Float32(f64Number) 	    // f32Number = NaN
    return 0
}

d) 有符号整数类型和无符号整数类型之间的双向转换:因为任何有符号整数类型的表示范围均不能包含长度相同的无符号整数类型的表示范围(反之亦然),因此它们之间进行转换时,只要待转换表达式的值落在目标整数类型的表示范围之内则转换成功,否则根据上下文中的属性宏确定溢出处理策略(默认使用抛出异常的策略),不同溢出策略详见[算术表达式]。下面以 Int8UInt8 之间的转换为例进行说明(溢出时,使用抛异常的处理策略):

main() {
    var i8Number: Int8 = 127
    var u8Number: UInt8 = 0
    u8Number = UInt8(i8Number) 	// ok: u8Number = 127
    u8Number = 100
    i8Number = Int8(u8Number) 	// ok: i8Number= 100
    i8Number= -100
    u8Number = UInt8(i8Number) 	// throw an ArithmeticException
    u8Number = 255
    i8Number = Int8(u8Number) 	// throw an ArithmeticException
    return 0
}

e) 整数转换为浮点数:结果为尽可能接近原整数的浮点数。超出目标类型的表示范围时,返回 POSITIVE_INFINITYNEGTIVE_INFINITY

e) Converting from an integer to floating point types: it will produce the closest possible float value. POSITIVE_INFINITY or NEGTIVE_INFINITY is returned when the value exceeds the range of the target type.

f) 浮点数转换为整数:浮点类型到有符号整数类型的转换使用 round-toward-zero 模式,即保留整数部分舍弃小数部分。当整数部分超出目标整数类型的表示范围,则根据上下文中的整数溢出策略处理。如果是 throwing 策略,那么抛出异常;否则按如下规则转换:

  • NaN 返回 0
  • 小于整数取值范围下界时(包括负无穷),返回整数的取值范围下界
  • 大于整数取值范围上界时(包括正无穷),返回整数的取值范围上界

f) Converting from a floating point to integer types: it follows the round-toward-zero mode, i.e. the fractional part is discarded. When the integer part is beyond the range of the target type, the result depends on the integer overflow strategy. If the strategy is throwing, an exception is thrown; otherwise, the result is as follows:

  • NaN will return 0
  • Values larger than the maximum integer value, including POSITIVE_INFINITY, will saturate to the maximum value of the integer type.
  • Values smaller than the minimum integer value, including NEGTIVE_INFINITY, will saturate to the minimum value of the integer type.
main() {
    var i32Number: Int32 = 1024
    var f16Number: Float16 = 0.0
    var f32Number: Float32 = 0.0
    f16Number = Float16(i32Number) 	// ok: f16Number = 1024.0
    f32Number = Float32(i32Number) 	// ok: f32Number = 1024.0
    i32Number = 2147483647
    f16Number = Float16(i32Number) 	// f16Number = POSITIVE_INFINITY
    f32Number = Float32(i32Number) 	// precision lost: f32Number = 2.14748365E9
    
    f32Number = 1024.1024
    i32Number = Int32(f32Number) 	// ok: i32Number = 1024
    f32Number = 1024e10
    i32Number = Int32(f32Number) 	// throw an Exception
    f32Number = 3.4e40 				// f32Number = POSITIVE_INFINITY
    i32Number = Int32(f32Number) 	// throw an Exception
    f32Number = 3.4e40 * 0.0 		// f32Number = NaN
    i32Number = Int32(f32Number) 	// throw an Exception
    return 0
}

class/interface 之间的类型转换

对于一个 class/interface 类型的实例 obj,如果需要将它的(静态)类型转换到另一个 class/interface 类型 TargetType,可使用:obj as TargetType

关于 as 操作符的使用,以及 class/interface 之间的类型转换规则,参见[as 操作符]。

类型别名

当某个类型的名字比较复杂或者在特定场景中不够直观时,可以选择使用类型别名的方式为此类型取一个简单并且直观的别名。定义类型别名的语法为:

typeAlias
	: typeModifier? `type` identifier typeParameters? `=` type
	;

其中,typeModifier 是可选的可访问性修饰符(即 public),type 是关键字,identifier 是任意的合法标识符,type 是任意的在 top-level 可见的类型,identifiertype 之间使用 = 进行连接。另外,也可通过在 identifier 之后添加类型参数(typeParameters)的方式定义泛型(参考第 9 章泛型)别名。通过以上声明,即为类型 type 定义了一个名字为 identifier 的别名,并且 identifiertype 被视作同一种类型。例如:

type Point2D = (Float64, Float64)
type Point3D = (Float64, Float64, Float64)

let point1: Point2D = (0.5, 0.8)
let point2: Point3D = (0.5, 0.8, 1.1)

上述 type 定义并不会定义一个新的类型,它的作用仅仅是为某个已有类型定义另外一个名字而已,别名和原类型被视作同一个类型,并且别名不会对原类型的使用带来任何影响。

类型别名定义的规则:

  1. 类型别名的定义只能出现在 top-level
func test() {
    type Point2D = (Float64, Float64) 			// error: type alias can only be defined at top-level
    type Point3D = (Float64, Float64, Float64) 	// error: type alias can only be defined at top-level
}
  1. 使用 type 定义类型别名时,原类型必须在 type 定义的位置可见。
class LongNameClassA {

}
type ClassB = LongNameClassB	// error: use of undeclared type 'LongNameClassB'
  1. 定义泛型别名时,如果泛型别名中引入了原类型中没有使用的泛型参数,则编译器会告警。
type Class1<V> = GenericClassA<Int64, V> 		// ok. ClassA is a generic class
type Class2<Value, V> = GenericClassB<Int64, V> // warning: the type parameter 'Value' in 'Class2<Value, V>' is not used in 'GenericClassB<Int64, V>'
type Int<T> = Int32 				            // warning: the type parameter 'T' in 'Int<T>' is not used in `Int32`
  1. 定义泛型别名时,不允许为别名和原类型中的类型参数添加泛型约束,在用到泛型别名时,可以按需为其添加泛型约束。另外,原类型中已有的泛型约束会“传递”至别名。
type Class1<V> where V <: MyTrait = GenericClassA<Int64, V> // error: generic constraints are not allowed here
type Class2<V> = GenericClassB<Int64, V> where V <: MyTrait // error: generic constraints are not allowed here

type Class3<V> = GenericClassC<Int64, V>
func foo<V> (p: Class3<V>) where V <: MyTrait {             // add generic constraints when 'Class3<V>' is used
    functionBody
}

class ClassWithLongName<T> where T<:MyTrait {
    classBody
}
type Class<T> = ClassWithLongName<T>                       // Class<T> also has the constraint 'where T<:MyTrait'
  1. 一个(或多个)type 定义中禁止出现循环引用(无论是直接的或是间接的)。其中,判断循环引用的方式是通过名字判断是否存在循环引用,并不是使用类型展开的方式。
type Value = GenericClassAp<Int64, Value> 	// error: 'Value' references itself
type Type1 = (Int64)->Type1 		        // error: 'Type1' references itself
type Type2 = (Int64, Type2) 		            // error: 'Type2' references itself
type Type3 = Type4 				            // error: 'Type3' indirectlly references itself
type Type4 = Type3
  1. 类型别名被视为与原类型等价的类型。例如,在下面的例子中,可以将 Int 类型的参数和 Int32 类型的参数直接相加(Int 定义为 Int32 的别名)。注意,不能通过使用别名达到函数重载的目的:
type Int = Int32
let numOne: Int32 = 10
let numTwo: Int = 20
let numThree = numOne + numTwo

func add(left: Int, right: Int32): Int { left + right }

func add(left: Int32, right: Int32): Int32 { left + right } 	// error: invalid redeclaration of 'add : (Int32, Int32)->Int32'
  1. type 定义的默认可见性为 default。如果需要在其他 package 内使用本 package 中定义的类型别名,需要同时满足:(1) 原类型在本 package 中的可见性修饰符为 public;(2) type 定义使用 public 修饰符。另外,需要注意的是:别名可以与原类型拥有不同的可见范围,但是别名的的可见范围不能大于原类型的可见范围。
// a.cj
package A
public class ClassWithLongNameA {
    
}
class ClassWithLongNameB {
    
}
 
public type classA = ClassWithLongNameA 		// ok
type classAInter = ClassWithLongNameA 	// ok 

/* error: classB can not be declared with modifier 'public', as 'ClassWithLongNameB' is internal */
public type classB = ClassWithLongNameB 		

// b.cj
package B
import A.*
let myClassA: A.classA = ClassWithLongNameA()

类型别名的使用

类型别名可以用在任何等号右手边它指向的原类型能够使用的位置:

  1. 作为类型使用,例如:

    type A = B
    class B {}
    var a: A = B() // Use typealias A as type B
    
  2. 当类型别名实际指向的类型为 class、struct 时,可以作为构造器名称使用

    type A = B
    class B {}
    func foo() { A() }  // Use type alias A as constructor of B
    
  3. 当类型别名实际指向的类型为 class、interface、struct 时,可以作为访问内部静态成员变量或函数的类型名

    type A = B
    class B {
    	static var b : Int32 = 0;
    	static func foo() {}
    }
    func foo() { 
    	A.foo() // Use A to access static method in class B
    	A.b
    }
    
  4. 当类型别名实际指向的类型为 enum 时,可以作为 enum 声明的构造器的类型名

    enum TimeUnit {
    	Day | Month | Year
    }
    type Time = TimeUnit
    var a = Time.Day  
    var b = Time.Month   // Use type alias Time to access constructors in TimeUnit 
    

类型间的关系

类型间的关系有两种:相等和子类型。

类型相等

对于任意两个类型 T1T2,如果它们满足以下任一条件,则称 T1T2 相等(记为 $T1 \equiv T2$):

  • 存在类型别名定义 type T1 = T2

  • class 定义的内部和 classextend 内部,T1class 的名字,T2This

  • T1T2 的名字完全相同(自反性);

  • $T2 \equiv T1$(对称性);

  • 存在类型 $Tk$,满足 $T1 \equiv Tk$ 且 $Tk \equiv T2$(传递性);

子类型

对于任意两个类型 T1T2,如果它们满足以下任一条件,则称 T1T2 的子类型(记为 $T1 <: T2$):

  • $T1 \equiv T2$;
  • T1Nothing 类型;
  • T1T2 均是 Tuple 类型,并且 T1 每个位置处的类型都是 T2 对应位置处类型的子类型;
  • T1T2 均是 Function 类型,并且 T2 的参数类型是 T1 参数类型的子类型,T1 的返回类型是 T2 返回类型的子类型;
  • T1 是任意 class 类型, T2Object 类型;
  • T1T2 均是 interface 类型,并且 T1 继承了 T2
  • T1T2 均是 class 类型,并且 T1 继承了 T2
  • T2interface 类型,并且 T1 实现了 T2
  • 存在类型 $Tk$,满足 $T1 <: Tk$ 且 $Tk <: T2$(传递性);

最小公共父类型

在有子类型的类型系统里,有时会遇到需要求两个类型的最小公共父类型的情形,例如 if 表达式的类型便是其两个分支的类型的最小公共父类型,match 表达式类似。

两个类型的最小公共父类型,是其公共父类型中最小的一个。最小意味着它是其他所有公共父类型的子类型

最小公共父类型定义如下:对于任意两个类型 T1T2,如果类型 LUB 满足如下规则,则 LUBT1T2 的最小公共父类型:

  • 对于同时满足 T1 <: TT2 <: T 的任意类型 TLUB <: T 也成立。

注意,如果公共父类型中的某个类型不比其他类型大,它只是极小的,并不一定是最小的。

最大公共子类型

因为子类型关系中存在逆变(定义参考泛型章节下的类型型变)的情形,如函数类型的参数类型是逆变的,此时会需要求两个类型的最大公共子类型。

两个类型的最大公共子类型,是其公共子类型中最大的一个。最大意味着它是其他所有公共子类型的父类型

最大公共子类型定义如下:对于任意两个类型 T1T2,如果类型 GLB 满足如下规则,则 GLBT1T2 的最大公共子类型:

  • 对于同时满足 T <: T1T <: T2 的任意类型 TT <: GLB 也成立。

注意,如果公共子类型中的某个类型不比其他类型小,它只是极大的,并不一定是最大的。

类型安全

在没有数据竞争的情况下,编译器保证内存安全和类型安全。

下面是一个类型安全和内存安全都得不到保证的例子:

class C {
    var x = 1
    var y = 2
    var z = 3
}

enum E {
    A(Int64) | B(C)
}

var e = A(1)

main() {
    spawn {
        while (true) {
            e = B(C())      // writing to `e`
            e = A(0)
        }
    }
    while (true) {
        match (e) {         // reading from `e`
            case A(n) =>
                println(n+1)
            case B(c) =>
                c.x = 2
                c.x = 3
                c.x = 4
        }
    }
}

不保证是因为对变量 e 赋值的线程和读取该变量的线程之间存在数据竞争。有关数据竞争的更多信息,请参见第 15 章 "并发"。