阅读文档版本:
具体开发指南 Cangjie-LTS-1.0.3↗
在阅读 了解仓颉的语言规约时, 难免会涉及到一些仓颉的示例代码, 但 我们对仓颉并不熟悉, 所以可以用仓颉在线体验↗快速验证
有条件当然可以直接配置Canjie-SDK↗
博主在此之前, 基本就只接触过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' ;
主构造函数的定义包括以下几个部分:
修饰符: 可选
主构造函数可以使用
public
、protected
、private
其中之一修饰, 都不使用是包内可见; 详见访问修饰符↗主构造函数名: 与类型名一致
主构造函数名前不允许使用
func
关键字形参列表: 主构造函数与
init
构造函数不同的是, 前者有两种形参: 普通形参和成员变量形参普通形参的语法和语义与函数定义中的形参一致
引入成员变量形参是为了减少代码冗余
成员变量形参的定义, 同时包含形参和成员变量的定义, 除此之外还表示了通过形参给成员变量赋值的语义
省略的定义和表达式会由编译器自动生成
成员变量形参的语法和成员变量定义语法一致, 此外, 和普通形参一样支持使用
!
来标注是否为命名形参成员变量形参的修饰符有:
public
,protected
,private
成员变量形参只允许实例成员变量, 即不允许使用
static
修饰成员变量形参不能与主构造函数外的成员变量同名
成员变量形参可以没有初始值
这是因为主构造函数会由编译器生成一个对应的构造函数, 将在构造函数体内完成将形参给成员变量的赋值
成员变量形参也可以有初始值, 初始值仅用于构造函数的参数默认值
成员变量形参的初值表达式中可以引用该成员变量定义之前已经定义的其他形参或成员变量(不包括定义在主构造函数外的实例成员变量), 但不能修改这些形参和成员变量的值
需要注意的是, 成员变量形参的初始值只在主构造函数中有效, 不会在成员变量定义中包含初始值
成员变量形参后不允许出现普通形参, 并且要遵循函数定义时的参数顺序, 命名形参后不允许出现非命名形参
仓颉类中的主构造函数, 函数名与类名相同, 与C++中的构造函数有些类似
主构造函数, 可以声明成员变量形参, 声明的形参 实际会被编译器自动生成为 成员变量
成员变量形参的定义方式与成员变量的定义方式一致, 即 需要let
或var
进行修饰
成员变量形参, 可以声明为命名形参, 可以用 同在主构造函数参数列表中且已经声明完毕的参数 进行赋值, 但不能使用在主构造函数外的成员变量进行赋值
与struct
类型的主构造函数应该是大致相同的
主构造函数体: 如果显式调用父类构造函数, 函数体内第一个表达式必须是调用父类构造函数的表达式
同时, 主构造函数中不允许使用
this
调用本类中其它构造函数父类构造函数调用之后, 主构造函数体内允许写表达式、局部变量声明、局部函数定义, 其中声明、定义和表达式需要满足
init
构造函数中对this
和super
使用的规则具体规则详见[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++一样, 仓颉中的类, 如果没有显示定义任何构造函数, 编译器会尝试生成一个无参构造函数, 但如果父类没有无参构造函数 会编译错误
构造函数、
this
和super
的使用规则:
禁止使用实例成员变量
this.variableName
及其语法糖variableName
和super.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() {} }
仓颉的类, 构造函数最核心的一点就是, 需要在构造函数内完成所有成员变量的初始化, 且只能在构造函数内完成
如果 成员变量还未被初始化, 则无法将this
或super
作为参数传递或闭包捕获
且, 仓颉的构造函数, 编译器最终只认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
表达式在静态初始化器中抛出异常会导致程序的终止, 就像在静态变量的右侧表达式中抛出异常一样
实例成员变量, 或者未初始化完成的静态成员变量不能在静态初始化器中使用
静态初始化器的代码是同步的, 以防止部分初始化的类或结构的泄漏
静态属性仍应以完整的方式声明, 包括
getter
和setter
;与静态函数不同, 静态初始化器不能在扩展中(在
extend
内)使用由于静态初始化器是自动调用的, 并且无法显式调用它, 因此可见性修饰符(即
public
、private
)不能用于修饰静态初始化器下面用一个例子来演示初始化分析的规则:
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 }
仓颉中, 类的静态成员变量, 只能在定义时直接赋值 或 在静态初始化器中进行初始化
类的静态初始化器会在包导入时, 自动调用(无法显式手动调用), 且调用顺序是代码自上而下的书写顺序, 即 如果存在子类书写在父类之上, 也是先执行子类的静态初始化器
类的静态初始化器, 是在所有静态成员变量完成初始化之后, 再自动调用的, 说明在定义时直接初始化的静态成员变量 也是再导入包之后就被定义好的
从文档内容来看, 仓颉类的成员变量, 如果在定义时直接赋值进行初始化, 那么类对象进行实例化时, 也会先将 所有定义时已经初始化的成员变量 初始化之后, 再执行构造函数进行初始化
那么, 有一个疑问:
既然, 仓颉类在实例化时, 定义时赋值的成员变量就已经完成了初始化, 那么为什么构造函数中, 给命名形参赋值时不能使用已经在定义时就完成初始化的成员变量呢?
成员变量
成员变量的声明
声明在类、接口中的变量称为成员变量
成员变量可以用关键字
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
修饰符是可选的, 但必须显式指定它的可见性修饰符为public
或protected
仓颉中, 只要类的构造函数执行完毕, 就标志着类对象创建完成
且, 仓颉中的类成员函数 与 C++类的成员函数类似, 也存在一个 位于第一个参数的隐式的this
参数, 调用时会被自动传入
仓颉中, 没有函数体实现的成员函数是抽象成员函数, 抽象成员函数只能存在于 抽象类中, 即 类需要用abstract
修饰
抽象成员函数必须显式使用public
或protected
修饰
静态成员函数
静态成员函数用
static
关键字修饰, 它不属于某个实例, 而是属于它所在的类型, 同时静态函数必须有函数体
静态成员函数中不能使用实例成员变量, 不能调用实例成员函数, 不能调用
super
或this
关键字静态成员函数中可以引用其他静态成员函数或静态成员变量
静态成员函数可以用
private
、protected
、public
、internal
修饰, 详见[访问修饰符]静态成员函数在被其它子类继承时, 这个静态成员函数不会被拷贝到子类中
抽象类和非抽象类中的静态成员函数都必须拥有实现
例如:
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) }
仓颉中, 类的静态成员函数, 不能只声明
静态成员函数内, 无法调用普通成员函数, 因为静态成员函数不属于任何实例, 而是属于类, 更没有对应的this
或super
静态成员函数被继承时, 整个继承链中的静态成员函数都是同一个, 不会在子类中拷贝一份, 如果是泛型父类, 则 指定变元父类的继承链中共享
类成员函数的修饰符
类成员函数可以被所有访问修饰符修饰, 详见访问修饰符
其他可修饰的非访问修饰符如下:
open
: 一个成员函数想要被覆盖, 需要用open
修饰符修饰, 它与static
修饰符有冲突当带
open
修饰的实例成员被class
继承时, 该open
的修饰符也会被继承如果
class
中存在被open
修饰的成员, 而当前class
没有被open
修饰或不包含open
语义, 那么这些open
修饰的成员仍然没有open
效果, 编译器对这种情况会报warning
提示(对于继承下来的open
成员或者override
的成员不需要报warning
)一个被 open 修饰的函数, 必须被
public
或protected
修饰
// 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
override
和static
不用过多理解,override
只是用来表示此函数是被重写的(不具有open
语义),static
表示此函数是一个静态函数
open
修饰成员函数, 表示此函数可以被子类重写, 但 要求此类(拥有open
成员函数的父类)是open
的, 毕竟只有具有open
语义的类才能被继承
open
成员函数, 必须要有public
或protected
的修饰
仓颉中, 类的成员函数 存在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
实例的方式按照是否包含类型变元可分为两种:
创建非泛型
class
的实例:ClassName(arguments)
其中
ClassName
为class
类型的名字,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 }
创建泛型
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
是一个“空”的类
Object
有public
修饰的无参构造函数
从文档来看, 仓颉中所有class
类型 均存在一个共同的祖先类Object
This
类型
在类内部, 我们支持
This
类型占位符, 它只能被作为实例成员函数的返回类型来使用, 并且在编译时会被替换为该函数所在类的类型, 从而进行类型检查
返回类型是
This
的函数, 只能返回This
类型表达式, 其它表达式都不允许
This
类型的表达式包含this
和 调用其它返回This
的函数
This
类型是当前类型的子类型,This
可以自动cast
成当前类型, 但反之不行函数体内不能显式使用
This
类型在返回值以外的地方使用
This
类型表达式都会被推断为当前类型如果实例成员函数没有声明返回类型, 并且只存在返回
This
类型表达式时, 当前函数的返回类型会推断为This
含
This
的open
函数在override
时, 返回类型必须保持This
类型父类中的
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
, 也可以为子类或父类