NOTE阅读文档版本:
语言规约 Cangjie-0.53.18-Spec
具体开发指南 Cangjie-LTS-1.0.3
在阅读 了解仓颉的语言规约时, 难免会涉及到一些仓颉的示例代码, 但 我们对仓颉并不熟悉, 所以可以用 仓颉在线体验 快速验证
有条件当然可以直接 配置Canjie-SDK
WARNING博主在此之前, 基本只接触过C/C++语言, 对大多现代语言都没有了解, 所以在阅读过程中遇到相似的概念, 难免会与C/C++中的相似概念作类比, 见谅
此样式内容, 表示文档原文内容
类型
仓颉编程语言是一种 静态类型(statically typed) 语言: 大部分保证程序安全的类型检查发生在编译期
同时, 仓颉编程语言是一种 强类型(strongly typed) 语言: 每个表达式都有类型, 并且表达式的类型决定了它的取值空间和它支持的操作
静态类型和强类型机制可以帮助程序员在编译阶段发现大量由类型引发的程序错误
可变类型 和 不可变类型
仓颉中的类型可分为两类: 不可变类型(immutable type)和可变类型(mutable type)
其中, 不可变类型包括数值类型(分为整数类型和浮点数类型)、
Rune类型、Bool类型、Unit类型、Nothing类型、String类型、元组(Tuple)类型、Range类型、函数(Function)类型、enum类型可变类型包括
Array类型、VArray类型、struct类型、class类型和interface类型不可变类型和可变类型的区别在于: 不可变类型的值, 其数据值一经初始化后就不会发生变化; 可变类型的值, 其数据值初始化后仍然有可以修改的方法
可变类型
Array类型
仓颉编程语言使用泛型类型
Array<T>表示Array类型,Array类型用于存储一系列相同类型(或者拥有公共父类)的元素关于
Array<T>, 说明如下:
Array<T>中的元素是有序的, 并且支持通过索引(从0开始)访问其中的元素
Array类型的长度是固定的, 即一旦建立一个Array实例, 其长度是不允许改变的
Array是引用类型, 定义为引用类型的变量, 变量名中存储的是指向数据值的引用, 因此在进行赋值或函数传参等操作时,Array拷贝的是引用类型变元
T被实例化成不同的类型, 会得到不同的Array类型, 例如,Array<Int32>和Array<Float64>分别表示Int32类型的Array和Float64类型的Array当
Array<T>中的Type类型支持使用==进行值判等(使用!=进行值判不等)时,Array<T>类型支持使用==进行值判等(使用!=进行值判不等);否则,
Array<T>类型不支持==和!=(如果使用==和!=, 编译报错)两个同类型的
Array<T>实例值相等, 当且仅当相同位置(index)的元素全部相等(意味着它们的长度相等)多维
Array定义为Array的Array, 使用Array<Array<...>>表示例如,
Array<Array<Int32>>表示Int32类型的二维Array当
ElementType拥有子类型时,Array<ElementType>中可以存放任何ElementType子类型的实例, 例如Array<Object>中可以存放任何Object子类的实例
仓颉中的Array, 有序(不是升序降序, 而是顺序), 可以通过索引随机访问, 创建之后长度固定且无法改变, 可创建多维
Array即为仓颉中的数组类型, 且Array类型是引用类型, 支持整个数组的判等和判不等操作, 前提是数组内元素类型要支持
创建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, 4let array4 = Array<String>(2) { i => "$i" } // 'array4' has 2 elememts: "0", "1"Array 字面量
Array字面量使用格式[element1, element2, ... , elementN]表示, 其中多个element之间使用逗号分隔, 每个element可以是一个expressionElement(普通表达式)
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的最小公共父类型是T,Array字面量的类型是Array<T>- 在上下文有明确的类型要求时, 此时要求所有
element的类型都是上下文所要求的element类型的子类型
仓颉Array类型的构造函数, 除了创建空数组之外, 比较重要的是指定size以及初始化元素的函数
仓颉的构造函数Array<T>(size: Int64, initElement: (Int64)->T)
第二个参数, 是用来初始化元素的, 参数是Int64类型, 返回值是T类型
实际的初始化操作, 就是调用函数传入索引, 返回目标索引获取的T类型数据作为Array中目标索引的初始值
仓颉中的数组字面量格式为:[elem1, elem2, ...], 也可以用来创建Array实例
访问Array中的元素
对于
Array类型的实例arr, 支持以下方式访问arr中的元素:访问某个位置处的元素: 通过
arr[index1]...[indexN]的方式访问具体位置处的元素(其中index1,...,indexN是索引值的表达式, 它们的类型均为Int64), 例如:let array5 = [0, 1]let element0 = array5[0] // element0 = 0array5[1] = 10 // change the value of the second element of 'array5' through indexlet array6 = [[0.1, 0.2], [0.3, 0.4]]let element01 = array6[0][1] // element01 = 0.2array6[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元素的访问也比较简单:[]下标索引 和for-in迭代
访问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 = 6let size2 = array10.size // size2 = 2Array的切片
数组的切片, 就是截取数组中的某一段连续的序列
当
Range<Int64>类型的值用作Array下标时, 用于截取Array的一个片段(称之为slicing)需要注意的是:
step必须是 1, 否则运行时会抛出异常
slicing返回的类型仍然是相同的Array类型, 并且是原Array的引用对切片中元素的修改会影响到原数组
当使用
start..end作为Array下标时, 如果省略start, 则将start的值设置为0, 如果省略end, 则将end的值设置为Array的长度值当使用
start..=end作为Array下标时, 如果start被省略, 则将start的值设置为0
start..=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] // step 不为 1, 运行时异常 array7[5..0:-1] // step 不为 1, 运行时异常 array7[5..0:-2] // step 不为 1, 运行时异常 array7[0..=5] // [0, 1, 2, 3, 4, 5] array7[0..=5:1] // [0, 1, 2, 3, 4, 5] array7[0..=5:2] // step 不为 1, 运行时异常 array7[5..=0:-1] // step 不为 1, 运行时异常 array7[5..=0:-2] // step 不为 1, 运行时异常 array7[..4] // [0, 1, 2, 3] array7[2..] // [2, 3, 4, 5] array7[..] // [0, 1, 2, 3, 4, 5] array7[..4:2] // 语法错误: start 或 end 不存在, 禁止使用:step array7[2..:-2] // 语法错误: start 或 end 不存在, 禁止使用:step array7[..:-1] // 语法错误: start 或 end 不存在, 禁止使用:step
array7[..=4] // [0, 1, 2, 3, 4] array7[..=4:2] // 语法错误: start 或 end 不存在, 禁止使用:step
array7[0..5:-1] // step 不为 1, 运行时异常 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] // size不匹配, 运行时抛异常arr[0..2] = [4] // size不匹配, 运行时抛异常
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 exceptionarr[..] = arr2如果使用slicing对原数组进行赋值, 总结就两条:
- 如果
=右边是 数组内元素类型的值, 就将slicing切片中 所有元素赋值为目标值 - 如果
=右边是 同数组类型的数组字面量, 那么这个数组字面量必须要与slicing的size匹配才能拷贝覆盖式赋值, 否则会抛异常
VArray类型
仓颉中已经有了Array数组类型, 但Array是引用类型的数组
仓颉编程语言使用泛型类型
VArray<T, $N>表示VArray类型,VArray类型用于存储一系列相同类型(或者拥有公共父类型)的元素关于
VArray<T, $N>, 说明如下:
VArray<T, $N>中的元素是有序的, 并且支持通过索引(从0开始)访问其中的元素如果提供的索引大于或等于其长度, 在编译期间能够推导出下标的则编译报错, 否则在运行时抛出异常
VArray<T, $N>类型的长度是类型的一部分其中
N表示VArray的长度, 它必须是一个整数字面量, 通过固定语法$加数字进行标注当提供的整数字面量为负数时, 编译期间进行报错
VArray是值类型, 定义为值类型的变量, 变量名中存储的是数据值本身, 因此在进行赋值或函数传参等操作时,VArray类型拷贝的是值当类型变元
T被实例化成不同的类型, 或$N标注长度不相等时, 会得到不同的VArray类型例
VArray<Int32, $5>和VArray<Float64, $5>是不同类型的VArray
VArray<Int32, $2>和VArray<Int32, $3>也是不同类型的VArray
VArray的泛型变元T是VArray类型时, 表示一个多维的VArray类型
仓颉中存在两种数组类型:
Array引用类型数组, 具体类型为Array<Type>VArray值类型数组, 具体类型为VArray<Type, $N>
VArray<Type, $N>不同的Type和不同的N都是不同的类型
创建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] // errorlet 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 = 0arr7[1] = 10 // change the value of the second element of 'arr7' 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 = 3arr8[1][1] = 5 // error: function call returns immutable valuearr8[1] = arr9 // arr8 = [[1, 2], [0, 1]]
因为VArray是值类型的数组
所以, 通过[index]操作符对VArray进行取值操作, 获取到的只会是数组中index位置同值的值类型拷贝新实例, 即一个独立的临时副本
如果通过[index]操作符直接对VArray数组的进行赋值, 则可以改变VArray[index]内的元素值
但对于多维的VArray, 尝试通过[][]对多维的索引位置进行赋值, 是不可行的
因为, [][]的第一个[]操作被看作取值操作, 是对外层维度的取值, 获取到的是外层这一维度的值类型的临时副本
不能直接拿着临时副本, 尝试修改临时副本的值
如果要直接通过[]修改VArray最外层维度的值, 可以考虑直接覆盖整个维度:
main() { var arr8: VArray<VArray<Int64, $2>, $2> = [[1, 2], [3, 4]]
arr8[0] = [5, 6] // 直接覆盖整个维度
println("${arr8[0][0]} ${arr8[0][1]}")
return 0}获取VArray的长度
支持通过
arr.size的方式返回arr中元素的个数let arr9: VArray<Int64, $6> = [0, 1, 2, 3, 4, 5]let size = arr9.size // size = 6
这一部分与Array是一致的
VArray在函数签名中时
当
VArray作为函数的参数或返回值时, 需要标注VArray的长度:func mergeArray<T>(a: VArray<T,$2>, b: VArray<T, $3>): VArray<T, $5>
因为VArray<Type, $N>中, Type和N都会影响实际类型
所以, 大概在实际需要声明VArray类型的所有地方都需要声明完整的Type和N
struct类型
struct类型是一种mutable类型, 在其内部可定义一系列的成员变量和成员函数定义
struct类型的语法为:structDefinition: structModifier? 'struct' identifier typeParameters? ('<:' superInterfaces)? genericConstraints? structBody;structBody: '{'structMemberDeclaration*structPrimaryInit?structMemberDeclaration*'}';structMemberDeclaration: structInit| staticInit| variableDeclaration| functionDefinition| operatorFunctionDefinition| macroExpression| propertyDefinition;其中
structModifier是struct的修饰符,struct是关键字,identifier是struct类型的名字,typeParameters和genericConstraints分别是类型变元列表和类型变元的约束
structBody中可以定义成员变量(variableDeclaration), 成员属性(propertyDefinition), 主构造函数(structPrimaryInit), 构造函数(structInit)和成员函数(包括普通成员函数和操作符函数)
从定义语法来看, 一个struct可以长这样:
(修饰符) struct 名字(<T>约束) { // 主构造函数(可选) // 成员变量 // 成员属性 // 构造函数 // 成员函数}关于
struct类型, 需要注意的是:
struct是值类型, 定义为值类型的变量, 变量名中存储的是数据值本身, 因此在进行赋值或函数传参等操作时,struct类型拷贝的是值
struct类型只能定义在top-level作为一种自定义类型,
struct类型默认不支持使用==(!=)进行判等(判不等)当然, 可以通过重载
==(!=)操作符使得自定义的struct类型支持==(!=)
struct不可以被继承
struct可以实现接口如果一个
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: Int32let length1: Int32let perimeter1: () -> Int32init (width1: Int32, length1: Int32) {this.width1 = width1this.length1 = length1this.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: Tlet length2: Tinit (side: T) {this.width2 = sidethis.length2 = side}init (width2!: T, length2!: T) {this.width2 = width2this.length2 = length2}}
从上面文档中给的定义示例, 好像与C++中的struct定义没有什么明显区别
不过仓颉中可以使用init()定义构造函数
但从描述来看, 还有很多区别: 值类型、递归和互递归等
递归和互递归
struct类型定义举例:struct R1 { // 错误: 'R1'不能有递归包含它的成员let other: R1}struct R2 { // oklet other: Array<R2>}struct R3 { // 错误: 'R3'不能有递归包含它的成员let other: R4}struct R4 { // 错误: 'R4'不能有递归包含它的成员let other: R3}struct R5 { // oklet other: E1}enum E1 { // okA(R5)}
NOTE示例中都忽略了
init()构造函数
从示例观察, 可以看到:
-
如果直接自递归, 是禁止的
struct R1 { // 错误: 'R1'不能有递归包含它的成员let other: R1}而
Array<R1>作为成员变量类型, 是允许的struct R1 { // oklet other: Array<R1>}且, 更无法使用
VArray<R1, $N>作为成员变量 -
互递归, 直接互递归, 也是被禁止的
struct R3 { // 错误: 'R3'不能有递归包含它的成员let other: R4}struct R4 { // 错误: 'R4'不能有递归包含它的成员let other: R3}但是如果另一个类型不是
struct好像就可以:struct R5 { // oklet other: E1}enum E1 { // okA(R5)}且, 经测试 这样是可以的:
struct R5 { // oklet other: R6}struct R6 { // oklet other: Array<R5>}
从示例给出的错误和正确案例, 猜测 与值类型和引用类型有关
如果将引用类型变量, 类比成C/C++中的指针, 就能很简单的解释了
如果struct内部 值类型自递归, 是错误的, 因为struct是值类型, 如果成员还是值类型自递归, 那么struct是无法确定大小的
互递归也是同样的原因, 如果直接值类型自递归或互递归, 无法确定struct的大小
而引用类型不一样, 如果将引用类型看作C/C++中的指针, 那么引用类型变量的大小就与具体类型无关了, 而与平台有关, 是固定的
当然这只是猜测, 毕竟才刚开始阅读语言规约, 暂时就这样理解吧
struct成员变量
定义成员变量的语法为:
variableDeclaration: variableModifier* NL* (LET | VAR) NL* patternsMaybeIrrefutable ((NL* COLON NL* type)? (NL* ASSIGN NL* expression) | (NL* COLON NL* type));在定义成员变量的过程中, 需要注意的是:
在主构造函数之外定义的成员变量可以有初始值, 也可以没有初始值
如果有初始值, 初始值表达式中仅可以引用定义在它之前的已经初始化的成员变量, 以及
struct中的静态成员函数
从定义语法来看:variableModifier变量修饰词, NL换行, (LET | VAR)修饰词, patternsMaybeIrrefutable可选模式
应该这样定义:
(public) let(var) 变量名: Type = Type字面量(public) let(var) 变量名: Type构造函数
在仓颉编程语言中, 有两种构造函数:主构造函数和
init构造函数 (简称构造函数)
构造函数看起来和C++有一定的区别
主构造函数
主构造函数的语法定义如下:
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)?;主构造函数的定义包括以下几个部分:
修饰符:可选
可以被所有访问修饰符修饰, 默认的可访问性为
internal主构造函数名:与类型名一致
主构造函数名前不允许使用
func关键字形参列表:主构造函数与
init构造函数不同的是, 前者有两种形参:普通形参和成员变量形参普通形参的语法和语义与函数定义中的形参一致
引入成员变量形参是为了减少代码冗余
成员变量形参的定义, 同时包含形参和成员变量的定义, 除此之外还表示了通过形参给成员变量赋值的语义
省略的定义和表达式会由编译器自动生成
成员变量形参的 语法和成员变量定义语法一致, 此外, 和普通形参一样支持使用
!来标注是否为命名形参成员变量形参的 修饰符有:
public,private,protected,internal成员变量形参 只允许非静态成员变量, 即不允许使用
static修饰成员变量形参 不能与主构造函数外的成员变量同名
成员变量形参 可以没有初始值
这是因为主构造函数会由编译器生成一个对应的构造函数, 将在构造函数体内完成将形参给成员变量的赋值;
成员变量形参 也可以有初始值, 初值表达式中可以引用该成员变量定义之前已经定义的其他形参或成员变量**(不包括定义在主构造函数外的实例成员变量)**, 但不能修改这些形参和成员变量的值
需要注意的是, 成员变量形参的初始值只在主构造函数中有效, 不会在成员变量定义中包含初始值
成员变量形参后不允许出现普通形参, 并且要遵循函数定义时的参数顺序, 命名形参后不允许出现非命名形参
主构造函数体:主构造函数不允许调用本
struct中其它构造函数主构造函数体内允许写声明和表达式, 其中声明和表达式需要满足
init构造函数的要求主构造函数定义的例子如下:
struct Test {static let counter: Int64 = 3let name: String = "afdoaidfad"private Test(name: String, // regular parameterannotation!: String = "nnn", // regular parametervar width!: Int64 = 1, // member variable parameter with initial valueprivate var length!: Int64, // member variable parameterprivate var height!: Int64 = 3 // member variable parameter) {}}
从文档描述和示例来看:
-
主构造函数与
struct同名 -
主构造函数除了普通的形参之外, 还可以像定义变量一样 声明(定义)成员变量形参
主构造函数中的成员变量形参, 会自动被定义为此
struct的成员变量与普通形参的区别是, 成员变量形参使用
let | var被修饰 -
主构造函数中的成员变量形参, 直接被看作
struct的成员变量, 且不被当作存在初始值这也就意味着, 主构造函数中的成员变量形参, 也必须要显式在其他构造函数中赋初始值
成员变量形参的初始值只在主构造函数中有效, 不会在成员变量定义中包含初始值
主构造函数是
init构造函数的语法糖, 编译器会自动生成与主构造函数对应的构造函数和成员变量的定义自动生成的构造函数形式如下:
- 其修饰符与主构造函数修饰符一致
- 其形参从左到右的顺序与主构造函数形参列表中声明的形参一致
- 构造函数体内形式如下:
- 首先是对成员变量的赋值, 语法形式为
this.x = x, 其中x为成员变量名- 然后是主构造函数体的其它代码
struct B<X,Y> {B( // 主构造函数, 与sturct同名x: Int64,y: X,v!: Int64 = 1, // 普通参数private var z!: Y // 成员变量形参) {}/* 编译器自动生成与主构造函数对应的初始化构造函数private var z: Y // 自动生成 成员变量定义init( x: Int64, y: X, v!: Int64 = 1, z!: Y) { // 自动生成的命名参数定义this.z = z // 自动生成的成员变量赋值表达式}*/}一个
struct最多可以定义一个主构造函数, 除了主构造函数之外, 可以照常定义其他构造函数, 但要求其他构造函数必须和主构造函数所对应的构造函数构成重载
这段的意思是说, 主构造函数其实是仓颉提供的语法糖
实际并不存在额外的构造函数, 主构造函数编译时还是会被转换为对应的init()构造函数
而且, 在显式定义其他init()构造函数时, 要与编译器根据主构造函数生成的init()构造函数 构成重载, 不能一致
即, 下面这样是不行的:
struct B<X,Y> { B( // 主构造函数, 与sturct同名 x: Int64, y: X, v!: Int64 = 1, // 普通参数 private var z!: Y // 成员变量形参 ) {}
// 显式定义init()构造函数 init(x: Int64, y: X, v!: Int64 = 1, z!: Y) { this.z = z }}因为显式定义的init()会与主构造函数生成的init()重名重参数列表, 构不成重载, 只会重定义
init构造函数
除了主构造函数, 还可以自定义构造函数, 构造函数使用关键字
init加上参数列表和函数体的方式定义一个
struct中可以定义多个构造函数, 但要求它们和主构造函数所对应的构造函数必须构成重载另外:
构造函数的参数可以有默认值
禁止使用实例成员变量
this.variableName及其语法糖variableName作为构造函数参数的默认值构造函数在所有实例成员变量完成初始化之前, 禁止使用 隐式传参或捕获了
this的函数或lambda, 但允许使用this.variableName或其语法糖variableName来访问已经完成初始化的成员变量variableName构造函数中的
lamdba和嵌套函数不能捕获this,this也不能作为表达式单独使用构造函数中允许通过
this调用其他构造函数如果调用, 必须在构造函数体内的第一个表达式处, 在此之前不能有任何表达式或声明
在构造函数体外, 不允许通过
this调用表达式来调用构造函数若构造函数没有显式调用其他构造函数, 则需要确保
return之前本struct声明的所有实例成员变量均完成初始化, 否则编译报错编译器会对所有构造函数之间的调用关系进行依赖分析, 循环的调用将编译报错
构造函数的返回类型为
Unit如果一个
struct既没有定义主构造函数, 也没有定义init构造函数, 则会尝试生成一个(public修饰的)无参构造函数如果存在本
struct的实例成员变量没有初始值, 则编译报错
总的来看, 仓颉struct的构造函数:
- 不能将成员变量, 作为参数的默认值
- 所有成员变量完成初始化之前, 不能以传参、捕获等方式将
this跳出构造函数使用 - 构造函数内, 可以调用其他构造函数, 但只能是构造函数内的第一个表达式
- 构造函数必须完成所有成员变量的初始化
- 构造函数返回类型为
Unit
struct的其他成员
struct 中也可以定义成员函数、操作符函数、成员属性和静态初始化器
- 定义普通成员函数参见[函数定义]
- 定义操作符函数的语法参见[操作符重载]
- 定义成员属性的语法参见[属性的定义]
- 定义静态初始化器的语法参见[静态初始化器]
没招了, 具体阅读到再说
struct中的修饰符
struct及其成员可以使用访问修饰符进行修饰, 详细内容请参考包和模块管理章节[访问修饰符]成员函数和变量可以使用
static修饰, 这些成员是静态成员, 静态成员属于struct类型的成员, 而不是struct实例的成员在
struct定义外部, 实例成员变量和实例成员函数只能通过struct实例访问, 静态成员变量和静态成员函数只能通过struct类型的名字访问另外函数还可以被
mut修饰,mut函数是一种特殊的实例成员函数,mut函数详细介绍参考函数章节[mut函数]
struct构造函数以及主构造函数内部定义的成员变量只能使用访问修饰符修饰, 不能使用static修饰
本片文档只主要说明了static修饰符, 其他的在其他文档中
而static与C++中的差不太多, 区别在于:
C++的静态成员可以通过对象的实例访问, 但仓颉中的静态成员只能通过类型访问
struct的实例化
定义完
struct类型之后, 就可以创建对应的struct实例
struct实例的定义方式按照是否包含类型变元可分为两种:
定义非泛型
struct的实例:StructName(arguments)其中
StructName为struct类型的名字,arguments为实参列表
StructName(arguments)会根据重载函数的调用规则调用对应的构造函数, 然后生成StructName的一个实例举例如下:
let newRectangle1_1 = Rectangle1(100, 200) // Invoke the first custom constructorlet newRectangle1_2 = Rectangle1(300) // Invoke the second custom constructor定义泛型
struct的实例:StructName<Type1, Type2, ... , TypeK>(labelValue1, labelValue2, ... , labelValueN)与定义非泛型
struct的实例的差别仅在于需要对泛型参数进行实例化举例如下:
let newRectangle2_1 = Rectangle2<Int32>(100) // Invoke the custom constructorlet 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的值
实例化struct, 就是通过类型名调用构造函数, 然后创建struct实例
只有非泛型和泛型的区别, 泛型需要指明类型
有一点需要主要:
只有struct变量和其成员变量, 均用var修饰是, 才能通过structInstance.varName = expr的方式修改成员变量的值
QUESTION
struct是值类型的那么, 给
struct成员赋值, 是与值类型变量本身赋值类似, 还是与引用类型变量赋值类似呢?这里猜测, 与引用类型变量赋值类似, 是直接替换了结构体成员内存中的数据, 不会重新创建实例, 更不会创建一个新的结构体
当然只是猜测
仓颉好像提供有
CPointer类型, 到时可以验证一下
class类型和interface类型
class和interface是引用类型, 定义为引用类型的变量, 变量名中存储的是指向数据值的引用, 因此在进行赋值或函数传参等操作时,class和interface拷贝的是引用请参见[类和接口]
class和interface应该是比较占篇幅的, 需要单独领出来的类型
具体到时再看