加载中...
仓颉文档阅读-语言规约VI: 类和接口(I)

仓颉文档阅读-语言规约VI: 类和接口(I)

周六 10月 04 2025
2855 字 · 13 分钟

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

类和接口

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

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

类的定义

在仓颉编程语言中, 通过关键字class定义一个类, 类定义包含以下几个部分:

  • 可选的修饰符

-class关键字

  • 类名, 必须是合法的identifier

  • 可选的类型参数

  • 可选的指定父类或者父接口(写在<:后面, 使用&分隔), 如果存在父类, 父类必须写在第一个, 否则编译报错;

  • 可选的泛型约束

  • 类体

类只能定义在源文件顶层, 类定义的语法如下:

PLAINTEXT
classDefinition
     : classModifierList? 'class' identifier
     typeParameters?
     ('<:' superClassOrInterfaces)?
     genericConstraints?
     classBody
     ;
superClassOrInterfaces
    : classType ('&' superInterfaces)?
    | superInterfaces
    ;    

类定义的例子如下:

CANGJIE
interface I1<X> {}
interface I2 {}
open class A{}

// 下面的类 B 继承了类 A, 并实现了接口 I2
open class B <: A & I2 {}

/* 以下类 C 声明了 1 个类型参数 U. 它继承自 B, 并实现了接口 I1<U>和 I2. 此外, 它的类型参数有约束 U <: A */
class C<U> <: B & I1<U> & I2 where U <: A {}

文档中说: 如果继承, 且继承存在父类, 则父类必须写在<:之后的第一个, 否则编译报错

那么也就可以推测, 仓颉的class类是禁止多继承的, 只能单继承

但是, class可以实现多个接口, 所以之后可以使用&将所有实现的接口继承下来, 但是如果继承类 只能单继承

类的修饰符

在此小节中, 我们将介绍定义类可以使用的修饰符

访问修饰符

类可以被所有访问修饰符修饰, 默认的可访问性为internal

详细内容请参考包和模块管理章节 访问修饰符

访问修饰符还是要在 包和模块管理 章节了解

继承性修饰符

-open: 类用open修饰, 表示允许其他类从这个类继承

CANGJIE
 /* 以下类使用修饰符`open`声明, 表示它可以被继承 */
 open class C1 {  	
     func foo(): Unit {
         return
     }
 }
 
 class C2 <: C1 {}

需要注意的是:

没有用open修饰的非抽象类不能被任何类继承

仓颉中的普通类, 默认是禁止被继承的, 使用open修饰, 表示此类可以被继承

-sealed: 修饰符表示只能在class定义所在的包内 继承该class

-sealed已经蕴含了public的语义, 所以定义sealed class时的public修饰符是可选的

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

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

-sealed的子类可以不被public修饰

CANGJIE
 // package A
 package A
 public sealed class C1 {}         // OK
 sealed class C2 {}                // OK, 使用 'sealed' 时, 'public' 是可选的 

 class S1 <: C1 {}  // OK
 public open class S2 <: C1 {}     // OK
 public sealed class S3 <: C1 {}   // OK
 open class S4 <: C1 {}            // OK

 // package B
 package B
 import A.*

 class SS1 <: S2 {}                // OK
 class SS2 <: S3 {}                // Error, S3 是一个密封类, 不能在此处继承

需要注意的是:

没有用opensealed修饰的非抽象类不能被任何类继承

仓颉中, class默认禁止被继承, 如果用open修饰, 则表示可以被继承, 只要能看到这个类; 如果用sealed修饰, 则表示可以在所在包中被继承, 不能在其他包中被继承, 即使其他包中可以看到这个类

sealed的子类, 可以被open修饰, 可以将子类暴露在包外, 在包外进行继承

抽象类修饰符

abstract: 该类属于抽象类, 与普通类不同的是, 在抽象类中除了可以定义普通的函数, 还允许声明抽象函数, 只有用该修饰符修饰的类才为抽象类

如果一个函数没有函数体, 我们称其为抽象函数

需要注意的是:

  • 抽象类abstract修饰符已经包含了可被继承的语义, 因此抽象类定义时的open修饰符是可选的, 也可以使用sealed修饰符修饰抽象类, 表示该抽象类只能在本包被继承

  • 抽象类中禁止定义private的抽象函数

  • 不能为抽象类创建实例

  • 允许抽象类的抽象子类不实现父类中的抽象函数

  • 抽象类的非抽象子类必须实现父类中的所有抽象函数

仓颉中的抽象类, 要用abstract修饰符来修饰, 而不是像C++中: 如果类内定义了纯虚函数, 那么这个类就是抽象类

并且, 仓颉中, 只有抽象类内能够定义抽象函数, 普通类中无法定义抽象函数

抽象函数类似与纯虚函数, 即 没有函数体实现的函数

类继承

类只支持单继承

类通过<: superClass的方式来指定当前类的直接父类(父类是某个已经定义了的类)

以下示例展示了类C2继承类C1的语法:

CANGJIE
open class C1 {}
class C2 <: C1 {}

父类可以是一个泛型类, 只需要在继承时提供合法的类型即可

例如: 非泛型类C2与泛型类C3继承泛型类C1:

CANGJIE
open class C1<T> {}
class C2 <: C1<Int32> {}
class C3<U> <: C1<U> {}

所有类都有一个父类, 对于没有定义父类的类, 默认其父类为Object

Object例外, 没有父类

CANGJIE
class Empty {} 	//  隐式继承自 Object:  class Empty <: Object {}

类仅支持单继承, 以下示例中的语法将引起编译错误

CANGJIE
open class C1 {}
open class C2 {}
class C3 <: C1 & C2 {}		// Error: 不支持多继承

当一个类继承另一个类时, 将被继承的类称为父类, 将产生继承行为的类称为子类

CANGJIE
open class C1 {}			// C1 is superclass
class C2 <: C1 {}			// C2 is subclass

子类将继承父类的所有成员, 私有成员和构造函数除外

子类可以直接访问父类的成员, 但是在覆盖时, 将不能直接通过名字来访问父类被覆盖的实例成员

这时可以通过super来指定(super指向的是当前类对象的直接父类)或创建对象并通过对象来访问

仓颉中, 类的继承使用的符号是<:

仓颉中的类只支持单继承, 与刚开始的推断是吻合的

其次, 仓颉中所有的类都存在父类, 如果没有定义时没有显式指明, 则隐式继承于Object

仓颉中类除了存在this表示当前类对象, 还存在super表示当前类父类对象

实现接口

类支持实现一个或多个接口, 通过<: I1 & I2 & ... & Ik的方式声明当前类想要实现的接口, 多个接口之间用&分隔

如果当前类也指定了父类, 则接口需要出现在父类后面

例如:

CANGJIE
interface I1 {}
interface I2 {}

// 类 C1 实现了接口 I1
open class C1 <: I1 {}        

// 类 C2 继承类 C1 并实现接口 I1、I2
class C2 <: C1 & I1 & I2 {}	

接口也可以是泛型的, 此时在实现泛型接口时需要给出合法的类型参数

例如:

CANGJIE
interface I1<T> {}
interface I2<U> {}
class C1 <: I1<Int32> & I2<Bool> {}
class C2<K, V> <: I1<V> & I2<K> {}

当类型实现接口时, 对于非泛型接口不能直接实现多次, 对于泛型接口不能用同样的类型参数直接实现多次

例如:

CANGJIE
interface I1 {}
class C1 <: I1 & I1 {}                    // error

interface I2<T> {}
class C2 <: I2<Int32> & I2<Int32> {}      // error
class C3<T> <: I2<T> & I2<Int32> {}       // ok

如果上述定义的泛型类C3在使用时被应用了类型参数Int32, 导致重复实现了两个相同类型的接口, 那么编译器会在类型被使用到的位置报错:

CANGJIE
interface I1<T> {}
open class C3<T> <: I1<T> & I1<Int32> {}  // ok
var a: C3<Int32>                          // error 
var b = C3<Int32>()                       // error
class C4 <: C3<Int32> {}                  // error

关于接口的详细描述在章节[接口]

仓颉中, 类实现接口 与 继承类似, 都是使用符号<:, 且可以同时进行

但, 只能单继承, 且父类要写在<:之后的最开始位置, 之后可以出现若干不同的接口类型, 每个接口通过&连接

实现的接口可以是泛型接口, 但是 具体的接口类型不能相同

类体

类体表示当前类包括的内容, 类体由大括号包围, 包含以下内容:

  • 可选的静态初始化器

  • 可选的主构造函数

  • 可选的构造函数

  • 可选的成员变量定义

  • 可选的成员函数、成员操作符函数定义或声明

  • 可选的成员属性定义或声明

  • 可选的宏调用表达式

  • 可选的类终结器

类体的语法定义如下:

PLAINTEXT
classBody
    : '{'  
           classMemberDeclaration*
           classPrimaryInit?
           classMemberDeclaration*
       '}'
    ;
    
classMemberDeclaration
    : classInit
    | staticInit
    | variableDeclaration
    | functionDefinition
    | operatorFunctionDefinition
    | macroExpression
    | propertyDefinition
    | classFinalizer
    ;

上述类体的语法定义中, 包含以下内容:

staticInit代表一个静态初始化器的定义, 一个类最多只能定义一个静态初始化器;

classPrimaryInit指的是主构造函数的定义, 一个类最多只能定义一个;

classInit表示init构造函数的定义;

variableDeclaration表示成员变量的声明;

operatorFunctionDefinition表示操作符重载成员函数的定义;

macroExpression表示宏调用表达式, 宏展开后依然要符合classMemberDeclaration的语法定义;

propertyDefinition表示属性的定义;

classFinalizer表示类的终结器的定义;

类体中引入的定义或声明均属于类的成员, 将在[类的成员]章节中详细介绍


Thanks for reading!

仓颉文档阅读-语言规约VI: 类和接口(I)

周六 10月 04 2025
2855 字 · 13 分钟