加载中...
仓颉文档阅读-语言规约II: 类型(II)

仓颉文档阅读-语言规约II: 类型(II)

周一 9月 22 2025
8115 字 · 36 分钟

此样式内容, 表示文档原文内容

类型

仓颉编程语言是一种 静态类型(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>, 说明如下:

  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, 有序(不是升序降序, 而是顺序), 可以通过索引随机访问, 创建之后长度固定且无法改变, 可创建多维

Array即为仓颉中的数组类型, Array类型是引用类型, 支持整个数组的判等和判不等操作, 前提是数组内元素类型要支持

创建Array实例

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

构造函数

CANGJIE
// 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字面量每个元素都是一个表达式:

CANGJIE
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), 例如:

CANGJIE
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中的元素, 例如:

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

仓颉中Array元素的访问也比较简单:[]下标索引 和for-in迭代

访问Array的大小

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

CANGJIE
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的值设置为0

    start..=end形式的end不能省略

  • 如果下标是一个空range值, 那么返回的是一个空的Array

CANGJIE
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必须与切片范围相同, 否则运行时会抛出异常
CANGJIE
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 exception
arr[..] = arr2

如果使用slicing对原数组进行赋值, 总结就两条:

  1. 如果=右边是 数组内元素类型的值, 就将slicing切片中 所有元素赋值为目标值
  2. 如果=右边是 同数组类型的数组字面量, 那么这个数组字面量必须要与slicingsize匹配才能拷贝覆盖式赋值, 否则会抛异常

VArray类型

仓颉中已经有了Array数组类型, 但Array是引用类型的数组

仓颉编程语言使用泛型类型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>是不同类型的VArray

    VArray<Int32, $2>VArray<Int32, $3>也是不同类型的VArray

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

仓颉中存在两种数组类型:

  1. Array引用类型数组, 具体类型为Array<Type>
  2. VArray值类型数组, 具体类型为VArray<Type, $N>

VArray<Type, $N>不同的Type和不同的N都是不同的类型

创建VArray实例

VArray同样可以使用Array字面量来创建新的实例

这种方式只能在上下文中能明确该字面量是VArray类型时才可以使用, 否则仍然会优先推断为Array类型 VArray的长度必须为Int64的字面量类型, 且必须与提供的字面量Array长度一致, 否则会编译报错

CANGJIE
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也可以使用构造函数的形式创建实例

CANGJIE
// 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实例

CANGJIE
var arr7: VArray<Int64, $2> = [0, 1]
let element0 = arr7[0]                     // element0 = 0
arr7[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 = 3
arr8[1][1] = 5                             // error: function call returns immutable value 
arr8[1] = arr9                             // arr8 = [[1, 2], [0, 1]]

因为VArray是值类型的数组

所以, 通过[index]操作符对VArray进行取值操作, 获取到的只会是数组中index位置同值的值类型拷贝新实例, 即一个独立的临时副本

如果通过[index]操作符直接对VArray数组的进行赋值, 则可以改变VArray[index]内的元素值

但对于多维的VArray, 尝试通过[][]对多维的索引位置进行赋值, 是不可行的

因为, [][]的第一个[]操作被看作取值操作, 是对外层维度的取值, 获取到的是外层这一维度的值类型的临时副本

不能直接拿着临时副本, 尝试修改临时副本的值

如果要直接通过[]修改VArray最外层维度的值, 可以考虑直接覆盖整个维度:

CANGJIE
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中元素的个数

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

这一部分与Array是一致的

VArray在函数签名中时

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

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

因为VArray<Type, $N>中, TypeN都会影响实际类型

所以, 大概在实际需要声明VArray类型的所有地方都需要声明完整的TypeN

struct类型

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

定义struct类型的语法为:

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

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

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

其中structModifierstruct的修饰符, struct是关键字, identifierstruct类型的名字, typeParametersgenericConstraints分别是类型变元列表和类型变元的约束

structBody中可以定义成员变量(variableDeclaration), 成员属性(propertyDefinition), 主构造函数(structPrimaryInit), 构造函数(structInit)和成员函数(包括普通成员函数和操作符函数)

从定义语法来看, 一个struct可以长这样:

CANGJIE
(修饰符) struct 名字(<T>约束) {
    // 主构造函数(可选)
    // 成员变量
    // 成员属性
    // 构造函数
    // 成员函数
}

关于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被封装在ClassInterfaceEnum或函数类型中, 也就是说, 可以使用ClassInterfaceEnum或函数类型使递归(或互递归)的struct定义合法化

struct类型定义举例:

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

从上面文档中给的定义示例, 好像与C++中的struct定义没有什么明显区别

不过仓颉中可以使用init()定义构造函数

但从描述来看, 还有很多区别: 值类型、递归和互递归等

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

CANGJIE
struct R1 { // 错误: 'R1'不能有递归包含它的成员
    let other: R1
}
struct R2 { // ok
    let other: Array<R2>
}

struct R3 { // 错误: 'R3'不能有递归包含它的成员
    let other: R4
}
struct R4 { // 错误: 'R4'不能有递归包含它的成员
    let other: R3
}

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

从示例观察, 可以看到:

  1. 如果直接自递归, 是禁止的

    CANGJIE
    struct R1 { // 错误: 'R1'不能有递归包含它的成员
        let other: R1
    }

    Array<R1>作为成员变量类型, 是允许的

    CANGJIE
    struct R1 { // ok
        let other: Array<R1>
    }

    且, 更无法使用VArray<R1, $N>作为成员变量

  2. 互递归, 直接互递归, 也是被禁止的

    CANGJIE
    struct R3 { // 错误: 'R3'不能有递归包含它的成员
        let other: R4
    }
    struct R4 { // 错误: 'R4'不能有递归包含它的成员
        let other: R3
    }

    但是如果另一个类型不是struct好像就可以:

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

    且, 经测试 这样是可以的:

    CANGJIE
    struct R5 { // ok
        let other: R6
    }
    struct R6 { // ok
        let other: Array<R5>
    }

从示例给出的错误和正确案例, 猜测 与值类型和引用类型有关

如果将引用类型变量, 类比成C/C++中的指针, 就能很简单的解释了

如果struct内部 值类型自递归, 是错误的, 因为struct是值类型, 如果成员还是值类型自递归, 那么struct是无法确定大小的

互递归也是同样的原因, 如果直接值类型自递归或互递归, 无法确定struct的大小

而引用类型不一样, 如果将引用类型看作C/C++中的指针, 那么引用类型变量的大小就与具体类型无关了, 而与平台有关, 是固定的

当然这只是猜测, 毕竟才刚开始阅读语言规约, 暂时就这样理解吧

struct成员变量

定义成员变量的语法为:

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

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

  1. 在主构造函数之外定义的成员变量可以有初始值, 也可以没有初始值

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

从定义语法来看:variableModifier变量修饰词, NL换行, (LET | VAR)修饰词, patternsMaybeIrrefutable可选模式

应该这样定义:

CANGJIE
(public) let(var) 变量名: Type = Type字面量
(public) let(var) 变量名: Type
构造函数

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

构造函数看起来和C++有一定的区别

主构造函数

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

PLAINTEXT

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构造函数的要求

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

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

从文档描述和示例来看:

  1. 主构造函数与struct同名

  2. 主构造函数除了普通的形参之外, 还可以像定义变量一样 声明(定义)成员变量形参

    主构造函数中的成员变量形参, 会自动被定义为此struct的成员变量

    与普通形参的区别是, 成员变量形参使用let | var被修饰

  3. 主构造函数中的成员变量形参, 直接被看作struct的成员变量, 且不被当作存在初始值

    这也就意味着, 主构造函数中的成员变量形参, 也必须要显式在其他构造函数中赋初始值

    成员变量形参的初始值只在主构造函数中有效, 不会在成员变量定义中包含初始值

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

自动生成的构造函数形式如下:

  • 其修饰符与主构造函数修饰符一致
  • 其形参从左到右的顺序与主构造函数形参列表中声明的形参一致
  • 构造函数体内形式如下:
    • 首先是对成员变量的赋值, 语法形式为this.x = x, 其中x为成员变量名
    • 然后是主构造函数体的其它代码
CANGJIE
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()构造函数 构成重载, 不能一致

即, 下面这样是不行的:

CANGJIE
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中可以定义多个构造函数, 但要求它们和主构造函数所对应的构造函数必须构成重载

另外:

  1. 构造函数的参数可以有默认值

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

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

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

  4. 构造函数中允许通过this调用其他构造函数

    如果调用, 必须在构造函数体内的第一个表达式处, 在此之前不能有任何表达式或声明

    在构造函数体外, 不允许通过this调用表达式来调用构造函数

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

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

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

如果一个struct既没有定义主构造函数, 也没有定义init构造函数, 则会尝试生成一个(public修饰的)无参构造函数

如果存在本struct的实例成员变量没有初始值, 则编译报错

总的来看, 仓颉struct的构造函数:

  1. 不能将成员变量, 作为参数的默认值
  2. 所有成员变量完成初始化之前, 不能以传参、捕获等方式将this跳出构造函数使用
  3. 构造函数内, 可以调用其他构造函数, 但只能是构造函数内的第一个表达式
  4. 构造函数必须完成所有成员变量的初始化
  5. 构造函数返回类型为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实例的定义方式按照是否包含类型变元可分为两种:

  1. 定义非泛型struct的实例:StructName(arguments)

    其中StructNamestruct类型的名字, arguments为实参列表

    StructName(arguments)会根据重载函数的调用规则调用对应的构造函数, 然后生成StructName的一个实例

    举例如下:

    CANGJIE
    let newRectangle1_1 = Rectangle1(100, 200)    // Invoke the first custom constructor
    let newRectangle1_2 = Rectangle1(300)         // Invoke the second custom constructor
  2. 定义泛型struct的实例:StructName<Type1, Type2, ... , TypeK>(labelValue1, labelValue2, ... , labelValueN)

    与定义非泛型struct的实例的差别仅在于需要对泛型参数进行实例化

    举例如下:

    CANGJIE
    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的值

实例化struct, 就是通过类型名调用构造函数, 然后创建struct实例

只有非泛型和泛型的区别, 泛型需要指明类型

有一点需要主要:

只有struct变量和其成员变量, 均用var修饰是, 才能通过structInstance.varName = expr的方式修改成员变量的值

class类型和interface类型

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

请参见[类和接口]

classinterface应该是比较占篇幅的, 需要单独领出来的类型

具体到时再看


Thanks for reading!

仓颉文档阅读-语言规约II: 类型(II)

周一 9月 22 2025
8115 字 · 36 分钟