9332 字
47 分钟
仓颉文档阅读-语言规约VI: 类和接口(II)
NOTE

阅读文档版本:

语言规约 Cangjie-0.53.18-Spec

具体开发指南 Cangjie-LTS-1.0.3

在阅读 了解仓颉的语言规约时, 难免会涉及到一些仓颉的示例代码, 但 我们对仓颉并不熟悉, 所以可以用 仓颉在线体验 快速验证

有条件当然可以直接 配置 Canjie-SDK

WARNING

博主在此之前, 基本只接触过 C/C++语言, 对大多现代语言都没有了解, 所以在阅读过程中遇到相似的概念, 难免会与 C/C++中的相似概念作类比, 见谅

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

类和接口#

任何面向对象的编程语言, 基本都存在类这个概念, C++不例外, 仓颉当然也不例外

但, C++ 中不存在接口这个东西, 接口这个词, 在业务方面听起来不陌生, 但是语言中的接口具体情况并不是非常了解

#

类的成员#

类的成员包括:

  • 从父类(若存在)继承而来的成员

  • 如果类实现了接口, 其成员还包括从接口中继承而来的成员

  • 在类体中声明或定义的成员, 包括: 静态初始化器、主构造函数、init构造函数、静态成员变量、实例成员变量、静态成员函数、实例成员函数、静态成员属性、实例成员属性

类的成员可以从不同的维度进行分类:

从是否被static修饰可以分为静态成员和实例成员

静态成员指不需要实例化类对象就能访问的成员, 实例成员指必须先实例化类对象才能通过对象访问到的成员

从成员的种类区分有静态初始化器、构造函数、成员函数、成员变量、成员属性

需要注意的是:

所有的静态成员都不能通过对象名访问

仓颉的类与 C++的类, 有一定的不同

仓颉的类拥有属性, 并且静态成员给不能通过类实例访问

构造函数#

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

主构造函数#

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

classPrimaryInit
: classNonStaticMemberModifier? className '(' classPrimaryInitParamLists? ')'
'{'
superCallExression?
( expression
| variableDeclaration
| functionDefinition)*
'}'
;
className
: identifier
;
classPrimaryInitParamLists
: unnamedParameterList (',' namedParameterList)? (',' classNamedInitParamList)?
| unnamedParameterList (',' classUnnamedInitParamList)?
(',' classNamedInitParamList)?
| classUnnamedInitParamList (',' classNamedInitParamList)?
| namedParameterList (',' classNamedInitParamList)?
| classNamedInitParamList
;
classUnnamedInitParamList
: classUnnamedInitParam (',' classUnnamedInitParam)*
;
classNamedInitParamList
: classNamedInitParam (',' classNamedInitParam)*
;
classUnnamedInitParam
: classNonStaticMemberModifier? ('let'|'var') identifier ':' type
;
classNamedInitParam
: classNonStaticMemberModifier? ('let'|'var') identifier'!' ':' type ('=' expression)?
;
classNonStaticMemberModifier
: 'public'
| 'protected'
| 'internal'
| 'private'
;

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

  1. 修饰符: 可选

    主构造函数可以使用publicprotectedprivate其中之一修饰, 都不使用是包内可见; 详见 访问修饰符

  2. 主构造函数名: 与类型名一致

    主构造函数名前不允许使用func关键字

  3. 形参列表: 主构造函数与init构造函数不同的是, 前者有两种形参: 普通形参和成员变量形参

    普通形参的语法和语义与函数定义中的形参一致

    引入成员变量形参是为了减少代码冗余

    成员变量形参的定义, 同时包含形参和成员变量的定义, 除此之外还表示了通过形参给成员变量赋值的语义

    省略的定义和表达式会由编译器自动生成

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

    • 成员变量形参的修饰符有:public,protected,private

    • 成员变量形参只允许实例成员变量, 即不允许使用static修饰

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

    • 成员变量形参可以没有初始值

      这是因为主构造函数会由编译器生成一个对应的构造函数, 将在构造函数体内完成将形参给成员变量的赋值

    • 成员变量形参也可以有初始值, 初始值仅用于构造函数的参数默认值

      成员变量形参的初值表达式中可以引用该成员变量定义之前已经定义的其他形参或成员变量(不包括定义在主构造函数外的实例成员变量), 但不能修改这些形参和成员变量的值

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

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

仓颉类中的主构造函数, 函数名与类名相同, 与 C++中的构造函数有些类似

主构造函数, 可以声明成员变量形参, 声明的形参 实际会被编译器自动生成为 成员变量

成员变量形参的定义方式与成员变量的定义方式一致, 即 需要letvar进行修饰

成员变量形参, 可以声明为命名形参, 可以用 同在主构造函数参数列表中且已经声明完毕的参数 进行赋值, 但不能使用在主构造函数外的成员变量进行赋值

struct类型的主构造函数应该是大致相同的

  1. 主构造函数体: 如果显式调用父类构造函数, 函数体内第一个表达式必须是调用父类构造函数的表达式

    同时, 主构造函数中不允许使用this调用本类中其它构造函数

    父类构造函数调用之后, 主构造函数体内允许写表达式、局部变量声明、局部函数定义, 其中声明、定义和表达式需要满足init构造函数中对thissuper使用的规则

    具体规则详见[init 构造函数]

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

class Test {
static let counter: Int64 = 3
let name: String = "afdoaidfad"
private Test(
name: String, // 常规参数
annotation!: String = "nnn", // 常规参数
var width!: Int64 = 1, // 携带初始值的成员变量参数
private var length!: Int64, // 成员变量参数
private var height!: Int64 = 3 // 携带初始值的成员变量参数
) {
}
}

主构造函数定义时, 成员变量形参后不允许出现普通形参的例子如下:

class Test {
static let counter: Int64 = 3
let name: String = "afdoaidfad"
private Test(
name: String, // 常规参数
annotation!: String = "nnn", // 常规参数
var width!: Int64 = 1, // 携带初始值的成员变量参数
length!: Int64 // Error: 常规参数不能在成员变量参数之后
) {
}
}

类的静态成员变量, 可以在定义时直接赋值进行初始化

主构造函数中不能通过this调用本类的其他构造函数

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

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

  • 其修饰符与主构造函数修饰符一致

  • 其形参从左到右的顺序与主构造函数形参列表中声明的形参一致

  • 构造函数体内形式依次如下:

    • 依次是对成员变量的赋值, 语法形式为this.x = x, 其中x为成员变量名

    • 主构造函数体中的代码

open class A<X> {
A(protected var x: Int64, protected var y: X) {
this.x = x
this.y = y
}
}
class B<X> <: A<X> {
B( // 主构造函数, 函数名与类名相同
x: Int64, // 常规参数
y: X, // 常规参数
v!: Int64 = 1, // 常规参数
private var z!: Int64 = v // 成员变量参数
) {
super(x, y)
}
/* 编译器自动生成的 与主构造函数 对应的init构造函数.
private var z: Int64 // 自动在构造函数前 生成成员变量定义
init( x: Int64,
y: X,
v!: Int64 = 1,
z!: Int64 = v) { // 自动生成命名参数定义
super(x, y)
this.z = z // 自动生成 成员变量的赋值表达式
}
*/
}

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

事实上, 主构造函数只是init构造函数的语法糖, 在编译时 会由编译器自动生成对应的init构造函数

且, 在主构造函数中声明的成员变量形参, 会在生成init构造函数之前, 由编译器自动生成好 成员变量的定义

实际上, 仓颉中类的构造函数, 语法上支持主构造函数, 但本质上并不存在与类名同名的主构造函数, 在编译时 都会被转换为init构造函数, 所以要注意init构造函数之间禁止重定义

init构造函数#

构造函数使用init关键字指定, 不能带有func关键字, 不能为构造函数显式定义返回类型, 且必须有函数体

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

构造函数的语法如下:

Init
: nonSMemberModifier? 'init' '(' InitParamLists? ')'
'{'
(superCallExression | initCallExpression)?
( expression
| variableDeclaration
| functionDefinition)*
'}'
;
InitParamLists
: unnamedParameterList (',' namedParameterList)?
| namedParameterList
;

可以在init前添加访问修饰符来限制该构造函数的可访问范围: 详见 访问修饰符

当构造一个类的对象时, 实际上会调用此类的构造函数, 如果没有参数类型匹配且可访问的构造函数, 则会编译报错

在一个类中, 用户可以为这个类提供多个init构造函数, 这些构造函数必须符合函数重载的要求

关于函数重载的详细描述, 请参见 函数重载

class C {
init() {}
init(name: String, age: Int32) {}
}

创建类的实例时调用的构造函数, 将根据以下顺序执行类中的表达式:

  • 先初始化主构造函数之外定义的有缺省值的变量

  • 如果构造函数体内未显式调用父类构造函数或本类其它构造函数, 则调用父类的无参构造函数super(), 如果父类没有无参构造函数, 则报错

  • 执行构造函数体内的代码

仓颉中, 类的init构造函数除了命名不同外, 才与 C++中类的构造函数比较相似

但 无论是主构造函数还是init构造函数, 返回值类型均为Unit类型

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

如果父类没有无参构造函数或者存在本类的实例成员变量没有初始值, 则编译报错

与 C++一样, 仓颉中的类, 如果没有显示定义任何构造函数, 编译器会尝试生成一个无参构造函数, 但如果父类没有无参构造函数 会编译错误

构造函数、thissuper的使用规则:

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

  • init构造函数可以调用父类构造函数或本类其它构造函数, 但两者之间只能调用一个

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

  • 若构造函数没有显式调用其他构造函数, 也没有显式调用父类构造函数, 编译器会 在该构造函数体的开始处 插入直接父类的无参构造函数的调用

    如果此时父类没有无参构造函数, 则会编译报错

  • 构造函数体内调用完父类构造函数或本类其它构造函数之后, 允许使用super.x访问父类的实例成员变量x

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

  • 可以被继承的类的构造函数中, 禁止调用实例成员函数 或 实例成员属性

  • 可以被继承的类的构造函数中, 禁止this逃逸

  • 构造函数在所有实例成员变量完成初始化之前, 禁止使用隐式传参或捕获了this的函数或lambda, 禁止使用super.f访问父类的实例成员方法f, 禁止使用单独的this表达式, 但允许使用this.x或其语法糖x来访问已经完成初始化的成员变量x

  • 在构造函数体外, 不允许通过this调用该类的构造函数

  • 禁止构造函数之间循环依赖, 否则将编译报错

var b: Int64 = 1
class A {
var a: Int64 = 1
var b: ()->Int64 = { 3 } // OK
/* 在所有实例成员变量都初始化之前, 不能使用捕获了 this 的 lambda 表达式 */
var c: ()->Int64 = { a } // Error
/* 在所有实例成员变量初始化完成前, 不能使用捕获了 this 的函数 */
var d: ()->Int64 = f // Error
var e: Int64 = a + 1 // OK
func f(): Int64 {
return a
}
}
class B {
var a: Int64 = 1
var b: ()->Int64
init() {
b = { 3 }
}
init(p: Int64) {
this()
b = { this.a } // OK
b = f // OK
}
func f(): Int64 {
return a
}
}
var globalVar: C = C()
func f(c: C) {
globalVar = c
}
open class C {
init() {
globalVar = this // Error, this 无法从 open 类的构造函数中逃逸
f(this) // Error, this 无法从 open 类的构造函数中逃逸
m() // Error: 在 open 类的构造函数中禁止调用实例函数
}
func m() {}
}

仓颉的类, 构造函数最核心的一点就是, 需要在构造函数内完成所有成员变量的初始化, 且只能在构造函数内完成

如果 成员变量还未被初始化, 则无法将thissuper作为参数传递或闭包捕获

且, 仓颉的构造函数, 编译器最终只认init构造函数, 主构造函数会被自动转换为对应的init构造函数

this逃逸, 是指 将this作为参数传递或闭包捕获

而在类的所有成员变量未完成初始化前, 总是禁止this逃逸

但, 如果存在open class, 即使所有成员变量已经完成了初始化, 也禁止this逃逸

应该是防止出现 子类成员未完全初始化, 但却通过父类this进行多态调用的情况

静态初始化器#

类或结构体中的静态变量也可以在静态初始化器中通过赋值表达式来初始化

不支持在枚举和接口中使用静态初始化器

静态初始化器的语法如下:

staticInit
: 'static' 'init' '(' ')'
'{'
expressionOrDeclarations?
'}'
;

仓颉的类, 静态成员变量除了在定义时直接赋值进行初始化之外, 还可以通过静态初始化器进行初始化

静态初始化器长这样: static init() {}

静态成员变量是不允许在构造函数内进行初始化

原因很明显, 构造函数是构造实例时才执行的, 但静态成员变量不属于任何一个实例, 如果静态成员变量在构造函数内进行初始化, 直接通过类访问静态成员变量, 就可能会出现问题

静态初始化器的规则如下:

  • 静态初始化器会被自动调用, 开发者不能显式调用

  • 静态初始化器会在它所属的包被加载时被调用, 就像静态变量的初始化表达式

  • 一个类或结构中最多只能有一个静态初始化器

  • 对于一个非泛型的类或结构体, 静态初始化器保证仅被调用一次

  • 对于一个泛型的类或结构体, 静态初始化器在每个不同的类型实例化中, 保证仅被调用一次

    • 注意, 如果没有该泛型类或结构体的类型实例化, 则静态初始化器根本不会被调用
  • 静态初始化器, 在这个类或结构中所有静态成员变量的直接初始化后被调用, 就像构造函数是在所有实例字段的直接初始化后被调用一样

    • 这意味着可以在静态初始化器中引用进一步声明的静态成员变量

    • 这也意味着静态初始化器可以位于类或结构中的任何位置, 顺序并不重要

  • 在同一个文件中, 跨越多个类, 即使这些类之间存在继承关系, 静态初始化器仍然以自上而下的顺序被调用

    • 这意味着不能保证父类的所有静态成员变量必须在当前类的初始化之前被初始化

      class Foo <: Bar {
      static let y: Int64
      static init() { // 首先调用
      y = x // Error: 尚未初始化的变量
      }
      }
      open class Bar {
      static let x: Int64
      static init() { // 然后调用
      x = 2
      }
      }
  • 静态成员变量必须只以一种方式初始化, 要么直接通过右侧表达式, 要么在静态初始化器中

    • 尽管一个可变的静态变量可以同时 被直接赋值 和 在静态初始化器中被赋值 来进行初始化, 但在这种情况下的变量初始化只是直接赋值, 而静态初始化器中的赋值被认为是简单的重新赋值

    这意味着在静态初始化器被调用之前, 该变量会有一个直接赋值

    • 如果一个不可变的静态变量同时 被直接赋值 和 在静态初始化器中被赋值, 编译器会报一个关于重新赋值的错误

      • 如果一个不可变的静态变量在静态初始化器中被多次赋值, 也会报告这个错误
    • 如果一个静态变量 既没有直接初始化 也没有在静态初始化器中初始化, 编译器会报一个关于未初始化变量的错误

    • 上述情况可由一个特殊的初始化分析检测出来的, 它取决于实现方式

  • 静态初始化器不能有任何参数

  • 静态初始化器中不允许使用return表达式

  • 在静态初始化器中抛出异常会导致程序的终止, 就像在静态变量的右侧表达式中抛出异常一样

  • 实例成员变量, 或者未初始化完成的静态成员变量不能在静态初始化器中使用

  • 静态初始化器的代码是同步的, 以防止部分初始化的类或结构的泄漏

  • 静态属性仍应以完整的方式声明, 包括gettersetter;

  • 与静态函数不同, 静态初始化器不能在扩展中(在extend内)使用

  • 由于静态初始化器是自动调用的, 并且无法显式调用它, 因此可见性修饰符(即publicprivate)不能用于修饰静态初始化器

下面用一个例子来演示初始化分析的规则:

class Foo {
static let a: Int64
static var c: Int64
static var d: Int64 // Error: 未初始化的变量
static var e: Int64 = 2
static let f: Int64 // Error: 未初始化的变量
static let g: Int64 = 1
let x = c
static init() {
a = 1
b = 2
Foo.c // Error: 尚未初始化的变量
let anotherFoo = Foo()
anotherFoo.x // Error: 尚未初始化的变量
c = 3
e = 4
g = 2 // Error: 尝试给let变量 重新赋值
}
static let b: Int64
}

仓颉中, 类的静态成员变量, 只能在定义时直接赋值 或 在静态初始化器中进行初始化

类的静态初始化器会在包导入时, 自动调用(无法显式手动调用), 且调用顺序是代码自上而下的书写顺序, 即 如果存在子类书写在父类之上, 也是先执行子类的静态初始化器

类的静态初始化器, 是在所有静态成员变量完成初始化之后, 再自动调用的, 说明在定义时直接初始化的静态成员变量 也是再导入包之后就被定义好的

QUESTION

从文档内容来看, 仓颉类的成员变量, 如果在定义时直接赋值进行初始化, 那么类对象进行实例化时, 也会先将 所有定义时已经初始化的成员变量 初始化之后, 再执行构造函数进行初始化

那么, 有一个疑问:

既然, 仓颉类在实例化时, 定义时赋值的成员变量就已经完成了初始化, 那么为什么构造函数中, 给命名形参赋值时不能使用已经在定义时就完成初始化的成员变量呢?

成员变量#
成员变量的声明#

声明在类、接口中的变量称为成员变量

成员变量可以用关键字let声明为不可变的, 也可以用关键字var声明为可变的

主构造函数之外的实例成员变量声明时可以有初始值, 也可以没有初始值:

如果有初始值, 初始值表达式可以使用此变量声明之前的成员变量

由于这些成员变量的初始化的执行顺序是在调用父类构造函数之前, 初始值表达式中禁止使用带super的限定名访问父类的成员变量

以下是变量的代码示例:

open class A {
var m1: Int32 = 1
}
class C <: A {
var a: Int32 = 10
let b: Int32 = super.m1 // Error
}

仓颉中, 类的成员变量 如果在声明时就直接赋予初始值, 初始值表达式中禁止使用super访问父类的成员变量, 即使 所访问的父类成员变量也在声明时就赋予了初始值

变量的修饰符#

类中的变量可以被访问修饰符修饰, 详细内容请参考包和模块管理章节[访问修饰符]

另外, 如果类中的一个变量用static修饰, 则它属于类的静态变量

static可以与其他访问修饰符同时使用

静态变量会被子类继承, 子类和父类的静态变量是同一个

class C {
static var a = 1
}
var r1 = C.a // ok

类的静态变量, 在整个继承关系中, 都是同一个静态变量

类成员函数#
类成员函数的声明和定义#

在类中允许定义函数, 同时允许在抽象类中声明函数

定义和声明的区别在于该函数 是否有函数体

类成员的函数分为实例成员函数、静态成员函数

类成员函数定义或声明的语法如下:

functionDefinition
: modifiers 'func' identifier typeParameters? functionParameters (':' returnType)? genericConstraints? (('=' expression) | block)?
;

成员函数定义的语法与普通函数定义的语法保持一致

唯一不同的是, 成员函数可以省略函数体的实现, 以达到只声明成员函数的目的

实例成员函数#

实例成员函数的第一个隐式参数是this, 每当调用实例成员函数时, 都意味着需要先传入一个完整的对象, 因此 在对象未创建完成时 就调用实例成员函数的行为 是被禁止的, 但是该函数的类型将不包括该隐式参数

类对象创建完成的判断依据是已经调用了类的构造函数

实例成员函数可以分为抽象成员函数和非抽象成员函数

  • 抽象成员函数

    抽象成员函数只能在抽象类或接口中声明, 没有函数体

    abstract class A {
    public func foo(): Unit // 抽象成员函数
    }
  • 非抽象成员函数

    非抽象成员函数允许在任何类中定义, 必须有函数体

    class Test {
    func foo(): Unit { // 非抽象成员函数
    return
    }
    }

抽象实例成员函数默认具有open的语义

在抽象类中定义抽象实例成员函数时,open修饰符是可选的, 但必须显式指定它的可见性修饰符为publicprotected

仓颉中, 只要类的构造函数执行完毕, 就标志着类对象创建完成

且, 仓颉中的类成员函数 与 C++类的成员函数类似, 也存在一个 位于第一个参数的隐式的this参数, 调用时会被自动传入

仓颉中, 没有函数体实现的成员函数是抽象成员函数, 抽象成员函数只能存在于 抽象类中, 即 类需要用abstract修饰

抽象成员函数必须显式使用publicprotected修饰

静态成员函数#

静态成员函数用static关键字修饰, 它不属于某个实例, 而是属于它所在的类型, 同时静态函数必须有函数体

  • 静态成员函数中不能使用实例成员变量, 不能调用实例成员函数, 不能调用superthis关键字

  • 静态成员函数中可以引用其他静态成员函数或静态成员变量

  • 静态成员函数可以用privateprotectedpublicinternal修饰, 详见[访问修饰符]

  • 静态成员函数在被其它子类继承时, 这个静态成员函数不会被拷贝到子类中

  • 抽象类和非抽象类中的静态成员函数都必须拥有实现

例如:

class C<T> {
static let a: Int32 = 0
static func foo(b: T): Int32 {
return a
}
}
main(): Int64 {
print("${C<Int32>.foo(3)}")
print("${C<Bool>.foo(true)}")
return 0
}

这段程序对于C<Int32>C<Bool>分别有他们各自的静态成员变量a与静态函数foo

类中的静态函数可以声明新的类型变元, 这些类型变元也可以存在约束

在调用时只需对类给出合法的类型, 然后对静态函数给出合法的类型就可以了:

class C<T> {
static func foo<U>(a: U, b: T): U { a }
}
var a: Bool = C<Int32>.foo<Bool>(true, 1)
var b: String = C<Bool>.foo<String>("hello", false)
func f<V>(a: V): V { C<Int32>.foo<V>(a, 0) }

仓颉中, 类的静态成员函数, 不能只声明

静态成员函数内, 无法调用普通成员函数, 因为静态成员函数不属于任何实例, 而是属于类, 更没有对应的thissuper

静态成员函数被继承时, 整个继承链中的静态成员函数都是同一个, 不会在子类中拷贝一份, 如果是泛型父类, 则 指定变元父类的继承链中共享

类成员函数的修饰符#

类成员函数可以被所有访问修饰符修饰, 详见访问修饰符

其他可修饰的非访问修饰符如下:

  • open: 一个成员函数想要被覆盖, 需要用open修饰符修饰, 它与static修饰符有冲突

    当带open修饰的实例成员被class继承时, 该open的修饰符也会被继承

    如果class中存在被open修饰的成员, 而当前class没有被open修饰或不包含open语义, 那么这些open修饰的成员仍然没有open效果, 编译器对这种情况会报warning提示(对于继承下来的open成员或者override的成员不需要报warning)

    一个被 open 修饰的函数, 必须被publicprotected修饰

    // case 1
    open class C1 { // 在这种情况下, 需要在 class C1 之前添加 open 修饰符
    public open func f() {}
    }
    class C2 <: C1 {
    public override func f() {}
    }
    // case 2
    open class A {
    public open func f() {}
    }
    open class B <: A {}
    class C <: B {
    public override func f() {} // ok
    }
    // case 3
    interface I {
    func f() {}
    }
    open class Base <: I {} // 函数 f 继承了 open 修饰符
    class Sub <: Base {
    public override func f() {} // ok
    }
  • override: 当一个函数覆盖另一个可以被覆盖的函数时, 允许可选地使用override进行修饰(override不具备open的语义, 如果用override修饰的函数还需要允许能被覆盖, 需要重新用open修饰), 示例如上

    函数覆盖的规则请参见[覆盖]章节

  • static: 用static修饰的函数为静态成员函数, 必须有函数体

    静态成员函数不能用open修饰

    静态成员函数内不可以访问所在类的实例成员; 实例成员函数内能访问所在类的静态成员

    class C {
    static func f() {} // 不能被重写, 且必须存在函数体
    }
  • redef: 当一个静态函数重定义继承自父类型的静态函数时, 允许可选地使用redef进行修饰

    open class C1 {
    static func f1() {}
    static func f2() {}
    }
    class C2 <: C1 {
    redef static func f1() {}
    redef static func f2() {}
    }

仓颉的成员函数修饰符有这几个:open``override``static``redef

overridestatic不用过多理解,override只是用来表示此函数是被重写的(不具有open语义),static表示此函数是一个静态函数

open修饰成员函数, 表示此函数可以被子类重写, 但 要求此类(拥有open成员函数的父类)是open的, 毕竟只有具有open语义的类才能被继承

open成员函数, 必须要有publicprotected的修饰

仓颉中, 类的成员函数 存在redef修饰符, 此修饰符的作用是 让子类重写 继承于父类的static成员函数

子类通过redef修饰符重写继承于父类的static成员函数之后, 子类就拥有了自己的同名static成员函数

类终结器#

类终结器是类的一个实例成员函数, 这个方法在类的实例被垃圾回收的时候被调用

class C {
// 下面是一个终结器
~init() {}
}

终结器的语法如下:

classFinalizer
: '~' 'init' '(' ')' block
;
  • 终结器没有参数, 没有返回类型, 没有泛型类型参数, 没有任何修饰符, 也不可以被用户调用

  • 带有终结器的类不可被open修饰, 只有非open的类可以拥有终结器

  • 一个类最多只能定义一个终结器

  • 终结器不可以定义在扩展中

  • 终结器被触发的时机是不确定的

  • 终结器可能在任意一个线程上执行

  • 多个终结器的执行顺序是不确定的

  • 终结器向外抛出未捕获异常的行为由实现决定

  • 终结器中创建线程或者使用线程同步功能的行为由实现决定

  • 终结器执行结束之后, 如果这个对象还可以被继续访问, 后果由实现决定

  • 不允许this逃逸出终结器

  • 终结器中不允许调用实例成员方法

举例如下:

class SomeType0 {
~init() {} // OK
}
class SomeType1 {
~init(x: Int64) {} // Error, 终结器不能拥有参数
}
class SomeType2 {
private ~init() {} // Error, 终结器不能有可访问性修饰符
}
class SomeType3 {
open ~init() {} // Error, 终结器不能有open修饰符
}
open class SomeType4 {
~init() {} // Error, open class 不能存在终结器
}
var GlobalVar: SomeType5 = SomeType5()
class SomeType5 {
~init() {
GlobalVar = this // 禁止将 this 从终结器中逃逸, 否则可能会发生意外行为
}
}

仓颉类的终结器, 类似于 C++类的析构函数, 命名为~init()

但仓颉类的终结器是由垃圾回收机制调用的, 不能显式调用

仓颉中, 可被继承的类不能实现终结器, 这就表示 要在子类实例的终结器中去显式释放父类实例的资源

且终结器中不允许调用任何实例成员方法(成员函数), 也就是说只能在子类终结器中显式操作父类实例的成员变量去释放资源, 而不能调用父类的成员方法释放资源

类成员属性#

类中也可以定义成员属性, 定义成员属性的语法参见[属性]章节

类的实例化#

定义完非抽象的class类型之后, 就可以创建对应的class实例

创建class实例的方式按照是否包含类型变元可分为两种:

  1. 创建非泛型class的实例:ClassName(arguments)

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

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

    举例如下:

    class C {
    var a: Int32 = 1
    init(a: Int32) {
    this.a = a
    }
    init(a: Int32, b: Int32) {
    this.a = a + b
    }
    }
    main() : Int64 {
    var myC = C(2) // 调用第一个构造函数
    var myC2 = C(3, 4) // 调用第二个构造函数
    return 0
    }
  2. 创建泛型class的实例:ClassName<Type1, Type2, ... , TypeK>(arguments)

    与创建非泛型class的实例的差别仅在于需要对泛型参数进行实例化, 泛型实参可以显式指定, 也可以省略(此时由编译器根据程序上下文推断出具体的类型)

    举例如下:

    class C<T, U> {
    var a: T
    var b: U
    init(a: T, b: U) {
    this.a = a
    this.b = b
    }
    }
    main() : Int64 {
    var myC = C<Int32, Int64>(3, 4)
    var myC2 = C(3,4) // myC2 的类型推断为 C<Int64, Int64>
    return 0
    }

仓颉类的实例化, 与 C++类的实例化类似

均为通过类名调用对应的构造函数进行实例化

Object#

Object类是所有class类型的父类(不包括interface类型),Object类中不包含任何成员, 即Object是一个“空”的类

Objectpublic修饰的无参构造函数

从文档来看, 仓颉中所有class类型 均存在一个共同的祖先类Object

This类型#

在类内部, 我们支持This类型占位符, 它只能被作为实例成员函数的返回类型来使用, 并且在编译时会被替换为该函数所在类的类型, 从而进行类型检查

  1. 返回类型是This的函数, 只能返回This类型表达式, 其它表达式都不允许

  2. This类型的表达式包含this和 调用其它返回This的函数

  3. This类型是当前类型的子类型,This可以自动cast成当前类型, 但反之不行

  4. 函数体内不能显式使用This类型

    在返回值以外的地方使用This类型表达式都会被推断为当前类型

  5. 如果实例成员函数没有声明返回类型, 并且只存在返回This类型表达式时, 当前函数的返回类型会推断为This

  6. Thisopen函数在override时, 返回类型必须保持This类型

  7. 父类中的open函数返回类型如果是父类, 子类在override时可以使用This作为返回类型

open class C1 {
func f(): This { // 此函数类型为`() -> C1`
return this
}
func f2() { // 此函数类型为`() -> C1`
return this
}
public open func f3(): C1 {
return this
}
}
class C2 <: C1 {
// 成员函数 f 继承自 C1, 其类型现在为`() -> C2`
public override func f3(): This { // ok
return this
}
}
var obj1: C2 = C2()
var obj2: C1 = C2()
var x = obj1.f() // 在编译期间, x 的类型是 C2
var y = obj2.f() // 在编译期间, y 的类型是 C1

仓颉类中存在类型占位符This, 它实际表示当前class类型, 但是只能作为类内成员函数的返回值类型

返回值类型为This的成员函数, 只能返回this

如果返回值类型为This的成员函数被继承, 那么子类 所继承的成员函数返回值类型也是This, 只不过此This将是子类类型

如果子类要重写父类的 返回值类型为This的成员函数, 那么 子类重写的函数返回值类型要保持为This, 不能是子类或父类类型

不过, 如果子类要重写父类的 返回值类型为父类的成员函数, 那么 子类重写的函数返回值类型可以为This, 也可以为子类或父类

作者
Humid1ch
发布于
2025-10-09
许可协议
CC BY-NC-SA 4.0