5309 字
27 分钟
仓颉文档阅读-开发指南VII: 类和接口(II) - This、创建对象以及class的继承
NOTE

阅读文档版本:

语言规约 Cangjie-0.53.18-Spec

具体开发指南 Cangjie-LTS-1.0.4

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

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

WARNING

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

且, 本系列是文档阅读, 而不是仓颉的零基础教学, 所以如果要跟着阅读的话最好有一门编程语言的开发经验

WARNING

在阅读仓颉编程语言的开发指南之前, 已经大概阅读了一遍 仓颉编程语言的语言规约, 已经对仓颉编程语言有了一个大概的了解

所以在阅读开发指南时, 不会对类似: 类、函数、结构体、接口等解释起来较为复杂名称 做出解释

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

类和接口#

#

class类型是面向对象编程中的经典概念, 仓颉中同样支持使用class来实现面向对象编程

classstruct的主要区别在于, class是引用类型, struct是值类型, 它们在赋值或传参时行为是不同的

class之间可以继承, 但struct之间不能继承

本节依次介绍如何定义class类型, 如何创建对象, 以及class的继承

This类型#

在类内部, 支持This类型占位符, 代指当前类的类型

只能被作为实例成员函数的返回类型来使用

当使用子类对象调用在父类中定义的返回This类型的函数时, 该函数调用的类型会被识别为子类类型, 而非定义所在的父类类型

如果实例成员函数没有声明返回类型, 并且只存在返回This类型表达式时, 当前函数的返回类型会推断为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这个类型, 但只能用与class成员函数的返回值类型

但要注意, 如果使用子类实例 调用父类的This作为返回值类型的函数, This表示子类类型

文档中:

var obj1: C2 = C2()
var obj2: C1 = C2()
var x = obj1.f() // 在编译过程中, x 的类型为 C2
var y = obj2.f() // 在编译过程中, y 的类型为 C1

编译过程中, xy的识别类型是不同的, 即 是obj1obj2的声明类型

但, 运行过程中, 是一种动态多态行为, 实际调用类型均为C2

为什么存在这个差异呢?

因为要保证语法合法, 你不能尝试使用obj2直接调用C2自己的成员函数, 这是非法行为

import std.reflect.*
main() {
println(TypeInfo.of(x))
println(TypeInfo.of(y))
return 0
}

这个打印:

Main.C2
Main.C2

创建对象#

定义了class类型后, 即可通过调用其构造函数来创建对象(通过class类型名调用构造函数)

例如, 下例中通过Rectangle(10, 20)创建Rectangle类型的对象并赋值给变量r

创建对象之后, 可以通过对象访问(public修饰的)实例成员变量和实例成员函数

例如, 下例中通过r.widthr.height可分别访问rwidthheight的值, 通过r.area()可以调用成员函数area

class Rectangle {
let width: Int64
let height: Int64
public init(width: Int64, height: Int64) {
this.width = width
this.height = height
}
public func area() {
this.width * this.height
}
}
let r = Rectangle(10, 20) // r.width = 10, r.height = 20
let width = r.width // width = 10
let height = r.height // height = 20
let a = r.area() // a = 200

如果希望通过对象去修改成员变量的值(不鼓励这种方式, 最好还是通过成员函数去修改), 需要将class类型中的成员变量定义为可变成员变量(即使用var定义)

举例如下:

class Rectangle {
public var width: Int64
public var height: Int64
public init(width: Int64, height: Int64) {
this.width = width
this.height = height
}
public func area() {
width * height
}
}
main() {
let r = Rectangle(10, 20) // r.width = 10, r.height = 20
r.width = 8 // r.width = 8
r.height = 24 // r.height = 24
let a = r.area() // a = 192
}

class对象(实例)的创建, 很简单, 即 使用类名调用构造函数, 类名(参数列表)

创建完成之后, 就可以通过实例访问可访问的成员变量、成员函数

使用var定义的成员变量可以被修改, 但 仓颉不建议直接通过实例修改成员变量的值, 最好还是通过成员函数来修改

不同于struct, 对象在赋值或传参时, 不会将对象进行复制, 多个变量指向的是同一个对象, 通过一个变量去修改对象中成员的值, 其他变量中对应的成员变量也会被修改

以赋值为例, 下面的例子中, 将r1赋值给r2之后, 修改r1widthheight的值, r2widthheight值也同样会被修改

class Rectangle {
var width: Int64
var height: Int64
public init(width: Int64, height: Int64) {
this.width = width
this.height = height
}
public func area() {
this.width * this.height
}
}
main() {
var r1 = Rectangle(10, 20) // r1.width = 10, r1.height = 20
var r2 = r1 // r2.width = 10, r2.height = 20
r1.width = 8 // r1.width = 8
r1.height = 24 // r1.height = 24
let a1 = r1.area() // a1 = 192
let a2 = r2.area() // a2 = 192
}

class是引用类型的, class对象在赋值或传参是, 不会发生拷贝, 而是引用

即, 新变量或形参 会引用(指向)原对象

多次赋值, 所有变量都是引用同一个原对象

此时, 通过其中一个变量修改对象成员的值, 所有变量再访问对象成员的值都会发生改变

因为, 都引用同一个对象

这样理解仓颉中的引用类型:

创建引用类型实例(对象) 赋值给变量, 就是变量引用(指向)了实例, 并不表示此变量就是实例, 变量与引用类型实例之间的关系是引用(指向)关系

如此, 你通过变量传参或赋值, 都只是建立了新的引用关系

如果, 给此变量重新赋值, 就是改变了此变量的引用关系, 此变量就不再引用之前的实例

仓颉的引用类型, 变量与实例之间的关系, 不同于C++的引用变量, C++引用变量定义之后无法改变引用对象

变量和实例是两个有关联的东西, 变量也可以重新修改指向, 可以类比一下C/C++的指针, 但又有很大不同

class的继承#

像大多数支持class的编程语言一样, 仓颉中的class同样支持继承

如果类B继承自类A, 则称A为父类, B为子类

子类将继承父类中 除private成员和构造函数以外 的所有成员

抽象类总是可被继承的, 故抽象类定义时的open修饰符是可选的, 也可以使用 sealed修饰符修饰抽象类, 表示该抽象类只能在本包被继承

但非抽象的类可被继承是有条件的: 定义时必须使用修饰符open修饰

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

当非open修饰的类中存在open修饰的成员时, 编译器会给出告警

可以在子类定义处通过<:指定其继承的父类, 但要求父类必须是可继承的

例如, 下面的例子中, class A使用open修饰, 是可以被类B继承的, 但是因为类B是不可继承的, 所以C在继承B的时候会报错

open class A {
let a: Int64 = 10
}
class B <: A { // Ok, 'B' 继承自 'A'
let b: Int64 = 20
}
class C <: B { // Error, 'B' 不能被继承
let c: Int64 = 30
}

class仅支持单继承, 因此下面这样一个类继承两个类的代码是不合法的(&是类实现多个接口时的语法, 详见 接口 )

open class A {
let a: Int64 = 10
}
open class B {
let b: Int64 = 20
}
class C <: A & B { // Error, 'C' 只能继承与一个类(单继承)
let c: Int64 = 30
}

仓颉class存在继承机制, 但 只允许单继承

继承的语法为:

open class ClassBase {}
class ClassSuper <: ClassBase {}

只有open修饰的类能够被继承, 即 作为父类

如果要继承于一个父类, 类名后紧跟<:并指明父类, 如果尝试使用&符号连接多个父类进行继承, 是不被允许的, 因为仓颉只允许单继承

TIP

仓颉class继承的语法, 与实现接口的语法是一致的

不同的是, 实现接口可以使用&连接多个接口, 以实现多个接口, 但多继承不被允许

如果同时 进行继承和实现接口, 那么 父类必须为<:之后的首个标识符

即, class ClassSuper <: ClassBase & Interface1 & Interface2 & Interface3 {}

抽象类总是可被继承的, 默认具有open语义, 所以显式的open声明是可忽略的

抽象类还可以使用sealed修饰, 以此限制此抽象类只能在本包中被继承

因为类是单继承的, 所以任何类都最多只能有一个直接父类

对于定义时指定了父类的class, 它的直接父类就是定义时指定的类, 对于定义时未指定父类的class, 它的直接父类是Object类型

Object是所有类的父类(注意, Object没有直接父类, 并且Object中不包含任何成员)

因为子类是继承自父类的, 所以子类的对象天然可以当做父类的对象使用, 但是反之不然

例如, 下例中BA的子类, 那么B类型的对象可以赋值给A类型的变量, 但是A类型的对象不能赋值给B类型的变量

open class A {
let a: Int64 = 10
}
class B <: A {
let b: Int64 = 20
}
let a: A = B() // Ok, 子类对象可以赋值给父类变量
open class A {
let a: Int64 = 10
}
class B <: A {
let b: Int64 = 20
}
let b: B = A() // Error, 父类对象不能赋值给子类变量

class定义的类型不允许继承类型本身

class A <: A {} // Error, 'A' 不能继承于自身

仓颉所有类都是Object类的子类, 即 如果一个类没有显式指定继承的父类, 也默认继承于Object类(此类没有任何成员, 且没有直接父类)

子类是父类的子类型, 所以子类对象天然可以做为父类对象使用

所以, 子类对象能够赋值给父类变量, 但反之的不行

抽象类可以使用 sealed修饰符, 表示被修饰的类定义 只能在本定义所在的包内被其他类继承

sealed已经蕴含了public/open的语义, 因此定义sealed abstract class时若提供public/open修饰符, 编译器将会告警

sealed的子类可以不是sealed类, 仍可被open/sealed修饰, 或不使用任何继承性修饰符

sealed类的子类被open修饰, 则其子类可在包外被继承

sealed的子类可以 不被public修饰(注意, 不是 不可以, 而是 可以不)

package A
public sealed abstract class C1 {} // Warning, 冗余修饰符, 'sealed' 已存在 'public' 语义
sealed open abstract class C2 {} // Warning, 冗余修饰符, 'sealed' 已存在 'open' 语义
sealed abstract class C3 {} // OK, 使用 'sealed' 时, 'public' 是可选的
class S1 <: C1 {} // OK
public open class S2 <: C1 {} // OK
public sealed abstract class S3 <: C1 {} // OK
open class S4 <: C1 {} // OK
package B
import A.*
class SS1 <: S2 {} // OK
class SS2 <: S3 {} // Error, S3 在 package A 中被 sealed 修饰, 不能在这里被继承
sealed class SS3 {} // Error, 'sealed' 不能修饰非抽象类

sealed修饰符, 用来修饰抽象类, 不能修饰非抽象类, 隐式包含publicopen语义

sealed修饰的类, 被限制 只能在定义此类的包中被继承

但, sealed类的子类, 其可访问性、可继承性与其他普通的类保持一致, 即 根据 是否open和其可访问性修饰符决定

父类构造函数调用#

子类的init构造函数可以使用super(args)的形式调用父类构造函数, 或使用this(args)的形式调用本类其他构造函数, 但两者之间只能调用一个

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

open class A {
A(let a: Int64) {}
}
class B <: A {
let b: Int64
init(b: Int64) {
super(30)
this.b = b
}
init() {
this(20)
}
}

子类的主构造函数中, 可以使用super(args)的形式调用父类构造函数, 但不能使用this(args)的形式调用本类其他构造函数

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

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

open class A {
let a: Int64
init() {
a = 100
}
}
open class B <: A {
let b: Int64
init(b: Int64) {
// OK, `super()`被编译器自动添加
this.b = b
}
}
open class C <: B {
let c: Int64
init(c: Int64) { // Error, 父类没有无参构造函数
this.c = c
}
}

子类构造函数中, 可以通过super(参数)显式调用父类构造函数, 但只能作为构造函数的第一个表达式被调用

类的非主构造函数中, 也可以通过this(参数)显式调用本类的其他构造函数, 同样也只能作为构造函数的第一个表达式被调用

如果子类构造函数中没有显式调用父类构造函数, 那么编译器会尝试自动插入父类无参构造函数, 如果父类没有无参构造函数, 则会编译报错

覆盖和重定义#

子类中可以覆盖(override)父类中的同名非抽象实例成员函数, 即在子类中为父类中的某个实例成员函数定义新的实现

覆盖时, 要求父类中的成员函数使用open修饰, 子类中的同名函数使用override修饰, 其中override是可选的

例如, 下面的例子中, 子类B中的函数f覆盖了父类A中的函数f

open class A {
public open func f(): Unit {
println("I am superclass")
}
}
class B <: A {
public override func f(): Unit {
println("I am subclass")
}
}
main() {
let a: A = A()
let b: A = B()
a.f()
b.f()
}

对于被覆盖的函数, 调用时将根据变量的运行时类型(由实际赋给该变量的对象决定)确定调用的版本(即所谓的动态派发)

例如, 上例中a的运行时类型是A, 因此a.f()调用的是父类A中的函数f

b的运行时类型是B(编译时类型是A), 因此b.f()调用的是子类B中的函数f

所以程序会输出:

I am superclass
I am subclass

仓颉中, 父类的非抽象实例成员函数, 如果open修饰, 那么子类就可以实现同名、同形参列表、同返回值类型的函数, 以覆盖父类实例成员函数

可以类比C++中虚函数的重写

是一种实现动态多态的机制:

存在, 子类实例成员函数 覆盖 父类实例成员函数时, 如果将子类对象赋值给父类变量, 通过父类变量调用同名函数, 将会执行子类对应的实例成员函数

对于静态函数, 子类中可以重定义父类中的同名非抽象静态函数, 即 在子类中为父类中的某个静态函数定义新的实现

重定义时, 要求子类中的同名静态函数使用redef修饰, 其中redef是可选的

例如, 下面的例子中, 子类D中的函数foo重定义了父类C中的函数foo

open class C {
public static func foo(): Unit {
println("I am class C")
}
}
class D <: C {
public redef static func foo(): Unit {
println("I am class D")
}
}
main() {
C.foo()
D.foo()
}

对于被重定义的函数, 调用时将根据class的类型决定调用的版本

例如, 上例中C.foo()调用的是父类C中的函数foo, D.foo()调用的是子类D中的函数foo

I am class C
I am class D

仓颉中, 允许静态成员函数的重定义(重新定义, 而非重复定义)

如果父类存在静态成员函数, 那么子类可以直接定义 同名、同形参列表、同返回值类型的静态函数, 此时 子类可以使用redef修饰此函数

如果拥有继承关系的类 存在静态成员函数的重定义, 那么通过子类调用此函数, 就执行子类重定义的静态函数

如果抽象函数或open修饰的函数有命名形参, 那么实现函数或override修饰的函数也需要保持同样的命名形参

open class A {
public open func f(a!: Int32): Int32 {
a + 1
}
}
class B <: A {
public override func f(a!: Int32): Int32 { // Ok
a + 2
}
}
class C <: A {
public override func f(b!: Int32): Int32 { // Error
b + 3
}
}
main() {
B().f(a: 0)
C().f(b: 0)
}

还需要注意的是, 当实现或重定义的函数为泛型函数时, 子类型函数的类型变元约束 需要比 父类型中对应函数更宽松或相同

open class A {}
open class B <: A {}
open class C <: B {}
open class Base {
public open func foo<T>(a: T): Unit where T <: B {}
public open func bar<T>(a: T): Unit where T <: B {}
public static func f<T>(a: T): Unit where T <: B {}
public static func g<T>(): Unit where T <: B {}
}
class D <: Base {
public override func foo<T>(a: T): Unit where T <: C {} // Error, 更严格的约束
public override func bar<T>(a: T): Unit where T <: C {} // Error, 更严格的约束
public redef static func f<T>(a: T): Unit where T <: C {} // Error, 更严格的约束
public redef static func g<T>(): Unit where T <: C {} // Error, 更严格的约束
}
class E <: Base {
public override func foo<T>(a: T): Unit where T <: A {} // OK, 更宽松的约束
public override func bar<V>(a: V): Unit where V <: A {} // OK, 更宽松的约束, 泛型参数名 并不重要
public redef static func f<T>(a: T): Unit where T <: A {} // OK, 更宽松的约束
public redef static func g<T>(): Unit where T <: A {} // OK, 更宽松的约束
}
class F <: Base {
public override func foo<T>(a: T): Unit where T <: B {} // OK, 相同的约束
public override func bar<V>(a: V): Unit where V <: B {} // OK, 相同的约束
public redef static func f<T>(a: T): Unit where T <: B {} // OK, 相同的约束
public redef static func g<T>(): Unit where T <: B {} // OK, 相同的约束
}

仓颉class中, 如果父类的成员函数存在命名形参, 那么子类实现、覆盖、重定义此成员函数时, 要保证子类的命名形参 除类型外, 形参名也要与父类保持一致

如果子类要实现、覆盖、重定义的函数是泛型函数时, 如果父类对应函数存在类型变元约束, 那么子类的类型变元约束 需要保持一致或更宽松

保持一致很容易理解, 更宽松什么意思呢?

假设存在类C <: B <: A:

如果父类成员函数的类型变元, 约束 目标类型需要为BB的子类型

那么子类实现此成员函数时, 不能约束 目标类型需要为CC的子类型

因为CB的子类型, 如此约束 就是 更加严格的约束, 会导致 子类函数无法使用B类型调用, 而父类函数却可以使用B类型调用的情况

即, 不能使用父类目标约束类型的子类, 作为子类的目标约束类型