自定义类型

struct 类型

struct 是一种自定义类型,可以将若干不同类型的值组合在一起,成为一个新类型。本节依次介绍如何定义 struct 类型,如何创建 struct 实例,以及 struct 中的 mut 函数。

定义 struct 类型

struct 类型的定义以关键字 struct 开头,后跟 struct 的名字,接着是定义在一对花括号中的 struct 定义体。struct 定义体中可以定义一系列的成员变量、成员属性(参见[属性])、静态初始化器、构造函数和成员函数。

struct Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64, height: Int64) {
        this.width = width
        this.height = height
    }

    public func area() {
        width * height
    }
}

上例中定义了名为 Rectanglestruct 类型,它有两个 Int64 类型的成员变量 widthheight,一个有两个 Int64 类型参数的构造函数(使用关键字 init 定义,函数体中通常是对成员变量的初始化),以及一个成员函数 area(返回 widthheight 的乘积)。

注:struct 只能定义在源文件顶层。

struct 成员变量

struct 成员变量分为实例成员变量和静态成员变量(使用 static 修饰符修饰,且必须有初值),二者的区别在于实例成员变量只能通过 struct 实例(我们说 aT 类型的实例,指的是 a 是一个 T 类型的值)访问,静态成员变量只能通过 struct 类型名访问。

实例成员变量定义时可以不设置初值(但必须标注类型,如上例中的 widthheight),也可以设置初值,例如:

struct Rectangle {
    let width = 10
    let height = 20
}

struct 静态初始化器

struct 支持定义静态初始化器,并在静态初始化器中通过赋值表达式来对静态成员变量进行初始化。

静态初始化器以关键字组合 static init 开头,后跟无参参数列表和函数体,且不能被可见性修饰符修饰。函数体中必须完成对所有未初始化的静态成员变量的初始化,否则编译报错。

struct Rectangle {
    static let degree: Int64
    static init() {
        degree = 180
    }
}

一个 struct 中最多允许定义一个静态初始化器,否则报重定义错误。

struct Rectangle {
    static let degree: Int64
    static init() {
        degree = 180
    }
    static init() { // Error: redefinition with the previous static init function
        degree = 180
    }
}

struct 构造函数

struct 支持两类构造函数:普通构造函数和主构造函数。

普通构造函数以关键字 init 开头,后跟参数列表和函数体,函数体中必须完成对所有未初始化的实例成员变量的初始化(如果参数名和成员变量名无法区分,可以在成员变量前使用 this 加以区分,this 表示 struct 的当前实例),否则编译报错。

struct Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64, height: Int64) { // Error: 'height' is not initialized in the constructor
        this.width = width
    }
}

一个 struct 中可以定义多个普通构造函数,但它们必须构成重载(参见[函数重载]),否则报重定义错误。

struct Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64) {
        this.width = width
        this.height = width
    }

    public init(width: Int64, height: Int64) { // Ok: overloading with the first init function
        this.width = width
        this.height = height
    }

    public init(height: Int64) { // Error: redefinition with the first init function
        this.width = height
        this.height = height
    }
}

除了可以定义若干普通的以 init 为名字的构造函数外,struct 内还可以定义(最多)一个主构造函数。主构造函数的名字和 struct 类型名相同,它的参数列表中可以有两种形式的形参:普通形参和成员变量形参(需要在参数名前加上 letvar),成员变量形参同时扮演定义成员变量和构造函数参数的功能。

使用主构造函数通常可以简化 struct 的定义,例如,上述包含一个 init 构造函数的 Rectangle 可以简化为如下定义:

struct Rectangle {
    public Rectangle(let width: Int64, let height: Int64) {}
}

主构造函数的参数列表中也可以定义普通形参,例如:

struct Rectangle {
    public Rectangle(name: String, let width: Int64, let height: Int64) {}
}

如果 struct 定义中不存在自定义构造函数(包括主构造函数),并且所有实例成员变量都有初始值,则会自动为其生成一个无参构造函数(调用此无参构造函数会创建一个所有实例成员变量的值均等于其初值的对象);否则,不会自动生成此无参构造函数。例如,对于如下 struct 定义,注释中给出了自动生成的无参构造函数:

struct Rectangle {
    let width: Int64 = 10
    let height: Int64 = 10
    /* Auto-generated memberwise constructor:
    public init() {
    }
    */
}

struct 成员函数

struct 成员函数分为实例成员函数和静态成员函数(使用 static 修饰符修饰),二者的区别在于:实例成员函数只能通过 struct 实例访问,静态成员函数只能通过 struct 类型名访问;静态成员函数中不能访问实例成员变量,也不能调用实例成员函数,但在实例成员函数中可以访问静态成员变量以及静态成员函数。

下例中,area 是实例成员函数,typeName 是静态成员函数。

struct Rectangle {
    let width: Int64 = 10
    let height: Int64 = 20

    public func area() {
        this.width * this.height
    }

    public static func typeName(): String {
        "Rectangle"
    }
}

实例成员函数中可以通过 this 访问实例成员变量,例如:

struct Rectangle {
    let width: Int64 = 1
    let height: Int64 = 1

    public func area() {
        this.width * this.height
    }
}

struct 成员的可见修饰符

struct 的成员(包括成员变量、成员属性、构造函数、成员函数、操作符函数(详见操作符重载章节))用两种可见性修饰符修饰:publicprivate,缺省的含义是仅包内可见。

使用 public 修饰的成员在 struct 定义内部和外部均可见;使用 private 修饰的成员仅在 struct 定义内部可见,外部无法访问。

下面的例子中,widthpublic 修饰的成员,在类外可以访问,height 是缺省可见修饰符的成员,仅在本包可见,包外部无法访问。

package a
public struct Rectangle {
    public var width: Int64
    var height: Int64
    private var area: Int64
    ...
}

func samePkgFunc() {
    var r = Rectangle(10, 20)
    r.width = 8               // Ok: public 'width' can be accessed here
    r.height = 24             // Ok: 'height' has no modifier and can be accessed here
    r.area = 30               // Error: private 'area' can't be accessed here
}
package b
import a.*
main() {
    var r = Rectangle(10, 20)
    r.width = 8               // Ok: public 'width' can be accessed here
    r.height = 24             // Error: no modifier 'height' can't be accessed here
    r.area = 30               // Error: private 'area' can't be accessed here
}

禁止递归 struct

递归和互递归定义的 struct 均是非法的。例如:

struct R1 { // Error: 'R1' recursively references itself
    let other: R1
}
struct R2 { // Error: 'R2' and 'R3' are mutually recursive
    let other: R3
}
struct R3 { // Error: 'R2' and 'R3' are mutually recursive
    let other: R2
}

使用 struct 构造函数创建 struct 实例

定义了 struct 类型后,即可通过调用 struct 的构造函数来创建 struct 实例。在 struct 定义之外,通过 struct 类型名调用构造函数。例如,下例中定义了一个 Rectangle 类型的变量 r

let r = Rectangle(10, 20)

创建了 struct 实例之后,可以通过实例访问它的(public 修饰的)实例成员变量和实例成员函数。例如,下例中通过 r.widthr.height 可分别访问 rwidthheight 的值,通过 r.area() 可以调用 r 的成员函数 area

let r = Rectangle(10, 20)
let width = r.width   // width = 10
let height = r.height // height = 20
let a = r.area()      // a = 200

如果希望通过 struct 实例去修改成员变量的值,需要将 struct 类型的变量定义为可变变量,并且被修改的成员变量也必须是可变成员变量(使用 var 定义)。举例如下:

struct 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() {
    var 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
}

在赋值或传参时,会对 struct 实例进行复制,生成新的实例,对其中一个实例的修改并不会影响另外一个实例。以赋值为例,下面的例子中,将 r1 赋值给 r2 之后,修改 r1widthheight 的值,并不会影响 r2widthheight 值。

struct 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() {
    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 = 200
}

struct 中的 mut 函数

默认情况下,struct 中的实例成员函数是无法修改它的实例成员变量和实例成员属性的。如果需要修改,可以在定义实例成员函数时使用关键字 mut 修饰,使之成为 mut 函数。mut 函数是一种特殊的实例成员函数,mut 函数内部可以“原地”修改当前实例,以实现 struct 通过调用函数修改自身的目的。

语法上,定义 mut 实例成员函数是在 func 关键字之前加上 mut 修饰符。例如,可以在 Rectangle 中定义一个用于修改 width 值的 mut 函数 setW

struct Rectangle {
    public var width: Int64
    public var height: Int64

    public init(width!: Int64, height!: Int64) {
        this.width = width
        this.height = height
    }

    public mut func setW(v: Int64): Unit {
        width = v
    }

    public func area() {
        width * height
    }
}

调用 mut 函数时,mut 函数允许修改当前实例的成员值,例如,下例中通过调用 setWrwidth 值设置为 8

main() {
    var r = Rectangle(width: 10, height: 20) // r.width = 10, r.height = 20
    r.setW(8)                 // r.width = 8
    let a = r.area()          // a = 160
}

注意,上例中 r 如果使用 let 定义,则不能调用 Rectangle 中的 mut 函数。

enum 类型

本节介绍仓颉中的 enum 类型。enum 类型提供了通过列举一个类型的所有可能取值来定义此类型的方式。

在很多语言中都有 enum 类型(或者称枚举类型),但是不同语言中的 enum 类型的使用方式和表达能力均有所差异,仓颉中的 enum 类型可以理解为函数式编程语言中的代数数据类型(Algebraic Data Types)。

接下来,首先介绍如何定义和使用 enum,然后介绍如何使用模式匹配使得 enum 取不同值时执行不同的操作,最后介绍一个名为 Option 的常用 enum 类型,用于表示某个类型的实例要么有值要么没值。

enum 的定义和使用

定义 enum 时需要把它所有可能的取值一一列出,我们称这些值为 enum 的构造器(或者 constructor)。

enum RGBColor {
    | Red | Green | Blue
}

enum 类型的定义以关键字 enum 开头,接着是 enum 的名字,之后是定义在一对花括号中的 enum 体,enum 体中定义了若干构造器,多个构造器之间使用 | 进行分隔(第一个构造器之前的 | 是可选的)。上例中定义了一个名为 RGBColorenum 类型,它有 3 个构造器:RedGreenBlue,分别表示 RGB 色彩模式中的红色、绿色和蓝色。

上述 enum 中的构造器还可以携带若干(至少一个)参数,称为有参构造器。例如,可以为 RedGreenBlue 设置一个 UInt8 的类型的参数,用来表示每个颜色的亮度级别:

enum RGBColor {
    | Red(UInt8) | Green(UInt8) | Blue(UInt8)
}

仓颉支持同一个 enum 中定义多个同名构造器,但是要求这些构造器的参数个数不同(认为没有参数的构造器的参数个数等于 0),例如:

enum RGBColor {
    | Red | Green | Blue
    | Red(UInt8) | Green(UInt8) | Blue(UInt8)
}

enum 支持递归定义,例如,下面的例子中使用 enum 定义了一种表达式(即 Expr),此表达式只能有 3 种形式:单独的一个数字 Num(携带一个 Int64 类型的参数)、加法表达式 Add(携带两个 Expr 类型的参数)、减法表达式 Sub(携带两个 Expr 类型的参数)。对于 AddSub 这两个构造器,其参数中递归地使用到了 Expr 自身。

enum Expr {
    | Num(Int64)
    | Add(Expr, Expr)
    | Sub(Expr, Expr)
}

另外,在 enum 体中还可以定义一系列成员函数、操作符函数(详见操作符重载章节)和成员属性(详见属性章节),但是要求构造器、成员函数、成员属性之间不能重名。例如,下面的例子在 RGBColor 中定义了一个名为 printType 的函数,它会输出字符串 RGBColor

enum RGBColor {
    | Red | Green | Blue

    public static func printType() {
        print("RGBColor")
    }
}

注:enum 只能定义在源文件顶层。

使用限制:当 enumstruct 类型存在互递归关系时,且 enum 类型作为 Option 的类型参数, 可能存在编译错误。

enum 值

定义了 enum 类型之后,就可以创建此类型的实例(即 enum 值),enum 值只能取 enum 类型定义中的一个构造器。enum 没有构造函数,可以通过 类型名.构造器,或者直接使用构造器的方式来构造一个 enum 值(对于有参构造器,需要传实参)。

下例中,RGBColor 中定义了三个构造器,其中有两个无参构造器(RedGreen)和一个有参构造器(Blue(UInt8)),main 中定义了三个 RGBColor 类型的变量 rgb,其中,r 的值使用 RGBColor.Red 进行初始化,g 的值直接使用 Green 进行初始化,b 的值使用 Blue(100) 进行初始化:

enum RGBColor {
    | Red | Green | Blue(UInt8)
}

main() {
    let r = RGBColor.Red
    let g = Green
    let b = Blue(100)
}

当省略类型名时,enum 构造器的名字可能和类型名、变量名、函数名发生冲突。此时必须加上 enum 类型名来使用 enum 构造器,否则只会选择同名的类型、变量、函数定义。

下面的例子中,只有构造器 Blue(UInt8) 可以不带类型名使用,RedGreen(UInt8) 皆会因为名字冲突而不能直接使用,必须加上类型名 RGBColor

let Red = 1

func Green(g: UInt8) {
    return g
}

enum RGBColor {
    | Red | Green(UInt8) | Blue(UInt8)
}

let r1 = Red                 // Will choose 'let Red'
let r2 = RGBColor.Red        // Ok: constructed by enum type name

let g1 = Green(100)          // Will choose 'func Green'
let g2 = RGBColor.Green(100) // Ok: constructed by enum type name

let b = Blue(100)            // Ok: can be uniquely identified as an enum constructor

如下的例子中,只有构造器 Blue 会因为名称冲突而不能直接使用,必须加上类型名 RGBColor

class Blue {}

enum RGBColor {
    | Red | Green(UInt8) | Blue(UInt8)
}

let r = Red                 // Ok: constructed by enum type name

let g = Green(100)          // Ok: constructed by enum type name

let b = Blue(100)           // Will choose constructor of 'class Blue' and report an error

enum 的模式匹配

对于一个 enum 值,我们通常希望它是不同的构造器时执行不同的操作,在仓颉中,可以通过模式匹配来实现。本节只是对 enum 的模式匹配做一个简单的介绍,关于模式匹配的更多内容,会在之后的[模式匹配]章节中详细说明。

对于如下使用 enum 定义的 RGBColor,如果希望 enum 值是不同构造器时分别输出其字符串表示,则可以使用 match 表达式和 enum 模式实现(关于 match 表达式和模式的详细介绍,参见[模式匹配]):

enum RGBColor {
    | Red | Green | Blue
}

main() {
    let c = Green
    let cs = match (c) {
        case Red => "Red"
        case Green => "Green"  // Matched
        case Blue => "Blue"
    }
    print(cs)
}

上例中,RedGreenBlue 分别用于匹配 RGBColor 中不同的构造器,上述代码的执行结果为:

Green

对于 enum 中的有参构造器,同样可以使用 match 表达式来匹配,并且可以解构出有参构造器中参数的值,举例如下:

enum RGBColor {
    | Red(UInt8) | Green(UInt8) | Blue(UInt8)
}

main() {
    let c = Green(100)
    let cs = match (c) {
        case Red(r) => "Red = ${r}"
        case Green(g) => "Green = ${g}"  // Matched
        case Blue(b) => "Blue = ${b}"
    }
    print(cs)
}

上例中,RGBColor 的三个构造器均有一个 UInt8 类型的参数,在匹配变量 c 的值时,case 之后使用 enum 模式来匹配不同的构造器,并将参数值分别与变量 rgb 进行绑定,一旦某条 case 匹配成功(本例中会与第二条 case 匹配),则返回对应的字符串。

上述代码的执行结果为:

Green = 100

Option 类型

Option 类型使用 enum 定义,它包含两个构造器:SomeNone。其中,Some 会携带一个参数,表示有值,None 不带参数,表示无值。当需要表示某个类型可能有值,也可能没有值的时候,可选择使用 Option 类型。

Option 类型被定义为一个泛型 enum 类型,定义如下(这里我们仅需要知道尖括号中的 T 是一个类型形参,当 T 为不同类型时会得到不同的 Option 类型即可。关于泛型的详细介绍,可参见[泛型]。):

enum Option<T> {
    | Some(T)
    | None
}

其中,Some 构造器的参数类型就是类型形参 T,当 T 被实例化为不同的类型时,会得到不同的 Option 类型,例如:Option<Int64>Option<String>等。

Option 类型还有一种简单的写法:在类型名前加 ?。也就是说,对于任意类型 Ty?Ty 等价于 Option<Ty>。例如,?Int64 等价于 Option<Int64>?String 等价于 Option<String> 等等。

下面的例子展示了如何定义 Option 类型的变量:

let a: Option<Int64> = Some(100)
let b: ?Int64 = Some(100)
let c: Option<String> = Some("Hello")
let d: ?String = None

另外,虽然 TOption<T> 是不同的类型,但是当明确知道某个位置需要的是 Option<T> 类型的值时,可以直接传一个 T 类型的值,编译器会用 Option<T> 类型的 Some 构造器将 T 类型的值封装成 Option<T> 类型的值(注意:这里并不是类型转换)。例如,下面的定义是合法的(等价于上例中变量 abc 的定义):

let a: Option<Int64> = 100
let b: ?Int64 = 100
let c: Option<String> = "100"

在上下文没有明确的类型要求时,无法使用 None 直接构造出想要的类型,此时应使用 None<T> 这样的语法来构造 Option<T> 类型的数据,例如

let a = None<Int64> // a: Option<Int64>
let b = None<Bool> // b: Option<Bool>

最后,关于 Option 的使用,请参见“错误处理”中“Option 类型用于错误处理”的内容。

class 类型

class 类型是面向对象编程中的经典概念,仓颉中同样支持使用 class 来实现面向对象编程。class 与上面介绍的 struct 的主要区别在于:class 是引用类型,struct 是值类型,它们在赋值或传参时行为是不同的(下文会有介绍);class 之间可以继承,但 struct 之间不能继承。

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

定义 class 类型

class 类型的定义以关键字 class 开头,后跟 class 的名字,接着是定义在一对花括号中的 class 定义体。class 定义体中可以定义一系列的成员变量、成员属性(参见[属性])、静态初始化器、构造函数、成员函数和操作符函数(详见操作符重载章节)。

class Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64, height: Int64) {
        this.width = width
        this.height = height
    }

    public func area() {
        width * height
    }
}

上例中定义了名为 Rectangleclass 类型,它有两个 Int64 类型的成员变量 widthheight,一个有两个 Int64 类型参数的构造函数,以及一个成员函数 area(返回 widthheight 的乘积)。

注:class 只能定义在源文件顶层。

class 成员变量

class 成员变量分为实例成员变量和静态成员变量(使用 static 修饰符修饰,且必须有初值)。实例成员变量只能通过对象(即 class 的实例)访问,静态成员变量只能通过类型名访问。

class Rectangle {
    let width = 10
    static let height = 20
}

let l = Rectangle.height // l = 20

实例成员变量定义时可以不设置初值(但必须标注类型),也可以设置初值,例如:

class Rectangle {
    let width = 10
    let height = 20
}

class 静态初始化器

class 支持定义静态初始化器,并在静态初始化器中通过赋值表达式来对静态成员变量进行初始化。

静态初始化器以关键字组合 static init 开头,后跟无参参数列表和函数体,且不能被可见性修饰符修饰。函数体中必须完成对所有未初始化的静态成员变量的初始化,否则编译报错。

class Rectangle {
    static let degree: Int64
    static init() {
        degree = 180
    }
}

一个 class 中最多允许定义一个静态初始化器,否则报重定义错误。

class Rectangle {
    static let degree: Int64
    static init() {
        degree = 180
    }
    static init() { // Error: redefinition with the previous static init function
        degree = 180
    }
}

class 构造函数

struct 一样,class 中也支持定义普通构造函数和主构造函数。

普通构造函数以关键字 init 开头,后跟参数列表和函数体,函数体中必须完成所有未初始化实例成员变量的初始化,否则编译报错。

class Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64, height: Int64) { // Error: 'height' is not initialized in the constructor
        this.width = width
    }
}

一个 class 中可以定义多个普通构造函数,但它们必须构成重载(参见[函数重载]),否则报重定义错误。

class Rectangle {
    let width: Int64
    let height: Int64

    public init(width: Int64) {
        this.width = width
        this.height = width
    }

    public init(width: Int64, height: Int64) { // Ok: overloading with the first init function
        this.width = width
        this.height = height
    }

    public init(height: Int64) { // Error: redefinition with the first init function
        this.width = height
        this.height = height
    }
}

除了可以定义若干普通的以 init 为名字的构造函数外,class 内还可以定义(最多)一个主构造函数。主构造函数的名字和 class 类型名相同,它的参数列表中可以有两种形式的形参:普通形参和成员变量形参(需要在参数名前加上 letvar),成员变量形参同时具有定义成员变量和构造函数参数的功能。

使用主构造函数通常可以简化 class 的定义,例如,上述包含一个 init 构造函数的 Rectangle 可以简化为如下定义:

class Rectangle {
    public Rectangle(let width: Int64, let height: Int64) {}
}

主构造函数的参数列表中也可以定义普通形参,例如:

class Rectangle {
    public Rectangle(name: String, let width: Int64, let height: Int64) {}
}

如果 class 定义中不存在自定义构造函数(包括主构造函数),并且所有实例成员变量都有初始值,则会自动为其生成一个无参构造函数(调用此无参构造函数会创建一个所有实例成员变量的值均等于其初值的对象);否则,不会自动生成此无参构造函数。例如,对于如下 class 定义,编译器会为其自动生成一个无参构造函数:

class Rectangle {
    let width = 10
    let height = 20

    /* Auto-generated parameterless constructor:
    public init() {

    }
    */
}

// Invoke the auto-generated parameterless constructor
let r = Rectangle() // r.width = 10,r.height = 20

class 终结器

class 支持定义终结器,这个函数在类的实例被垃圾回收的时候被调用。终结器的函数名固定为 ~init。终结器一般被用于释放系统资源:

class C {
    var p: CString

    init(s: String) {
        p = unsafe { LibC.mallocCString(s) }
        println(s)
    }
    ~init() {
        unsafe { LibC.free(p) }
    }
}

使用终结器有些限制条件,需要开发者注意:

  1. 终结器没有参数,没有返回类型,没有泛型类型参数,没有任何修饰符,也不可以被显式调用。
  2. 带有终结器的类不可被 open 修饰,只有非 open 的类可以拥有终结器。
  3. 一个类最多只能定义一个终结器。
  4. 终结器不可以定义在扩展中。
  5. 终结器被触发的时机是不确定的。
  6. 终结器可能在任意一个线程上执行。
  7. 多个终结器的执行顺序是不确定的。
  8. 终结器向外抛出未捕获异常属于未定义行为。
  9. 终结器中创建线程或者使用线程同步功能属于未定义行为。
  10. 终结器执行结束之后,如果这个对象还可以被继续访问,则属于未定义行为。

class 成员函数

class 成员函数同样分为实例成员函数和静态成员函数(使用 static 修饰符修饰),实例成员函数只能通过对象访问,静态成员函数只能通过 class 类型名访问;静态成员函数中不能访问实例成员变量,也不能调用实例成员函数,但在实例成员函数中可以访问静态成员变量以及静态成员函数。

下例中,area 是实例成员函数,typeName 是静态成员函数。

class Rectangle {
    let width: Int64 = 10
    let height: Int64 = 20

    public func area() {
        this.width * this.height
    }

    public static func typeName(): String {
        "Rectangle"
    }
}

根据有没有函数体,实例成员函数又可以分为抽象成员函数和非抽象成员函数。抽象成员函数没有函数体,只能定义在抽象类或接口(详见接口章节)中。例如,下例中在抽象类 AbRectangle(使用关键字 abstract 修饰)中定义了抽象函数 foo

abstract class AbRectangle {
    public func foo(): Unit
}

需要注意的是,抽象实例成员函数默认具有 open 的语义,open 修饰符是可选的,且必须使用 publicprotected 进行修饰。

非抽象函数必须有函数体,在函数体中可以通过 this 访问实例成员变量,例如:

class Rectangle {
    let width: Int64 = 10
    let height: Int64 = 20

    public func area() {
        this.width * this.height
    }
}

class 成员的可见修饰符

对于 class 的成员(包括成员变量、成员属性、构造函数、成员函数),可以使用的可见性修饰符有三种:publicprotectedprivate,缺省的含义是仅包内可见。

使用 public 修饰的成员在 class 定义内部和外部均可见,成员变量、成员属性和成员函数在 class 外部可以通过对象访问;使用 protected 修饰的成员在本包、本 class 及其子类中可见,外部无法访问;使用 private 修饰的成员仅在本 class 定义内部可见,外部无法访问;缺省可见修饰符的成员仅在本包可见,外部无法访问。

package a
public open class Rectangle {
    public var width: Int64
    protected var height: Int64
    private var area: Int64
    public init(width: Int64, height: Int64) {
        this.width = width
        this.height = height
        this.area = this.width * this.height
    }
    init(width: Int64, height: Int64, multiple: Int64) {
        this.width = width
        this.height = height
        this.area = width * height * multiple
    }
}

func samePkgFunc() {
    var r = Rectangle(10, 20) // Ok: constructor 'Rectangle' can be accessed here
    r.width = 8               // Ok: public 'width' can be accessed here
    r.height = 24             // Ok: protected 'height' can be accessed here
    r.area = 30               // Error: private 'area' cannot be accessed here
}
package b
import a.*
public class Cuboid <: Rectangle {
    private var length: Int64
    public init(width: Int64, height: Int64, length: Int64) {
        super(width, height)
        this.length = length
    }
    public func volume() {
        this.width * this.height * this.length // Ok: protected 'height' can be accessed here
    }
}

main() {
    var r = Rectangle(10, 20, 2) // Error: Rectangle has no `public` constructor with three parameters
    var c = Cuboid(20, 20, 20)
    c.width = 8               // Ok: public 'width' can be accessed here
    c.height = 24             // Error: protected 'height' cannot be accessed here
    c.area = 30               // Error: private 'area' cannot be accessed here
}

This 类型

在类内部,我们支持 This 类型占位符,代指当前类的类型。它只能被作为实例成员函数的返回类型来使用,当使用子类对象调用在父类中定义的返回 This 类型的函数时,该函数调用的类型会被识别为子类类型,而非定义所在的父类类型。

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

open class C1 {
    func f(): This {  // its type is `() -> C1`
        return this
    }

    func f2() { // its type is `() -> C1`
        return this
    }

    public open func f3(): C1 {
        return this
    }
}
class C2 <: C1 {
    // member function f is inherited from C1, and its type is `() -> C2` now
    public override func f3(): This { // ok
        return this
    }
}

var obj1: C2 = C2()
var obj2: C1 = C2()

var x = obj1.f()    // During compilation, the type of x is C2
var y = obj2.f()    // During compilation, the type of y is C1

创建对象

定义了 class 类型后,即可通过调用 class 的构造函数来创建对象(通过 class 类型名调用构造函数)。例如,下例中通过 Rectangle(10, 20) 创建 Rectangle 类型的对象并赋值给变量 r

let r = Rectangle(10, 20)

创建对象之后,可以通过对象访问(public 修饰的)实例成员变量和实例成员函数。例如,下例中通过 r.widthr.height 可分别访问 rwidthheight 的值,通过 r.area() 可以调用成员函数 area

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

    ...
}

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
}

不同于 struct,对象在赋值或传参时,不会将对象进行复制,多个变量指向的是同一个对象,通过一个变量去修改对象中成员的值,其他变量中对应的成员变量也会被修改。以赋值为例,下面的例子中,将 r1 赋值给 r2 之后,修改 r1widthheight 的值,r2widthheight 值也同样会被修改。

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 的编程语言一样,仓颉中的 class 同样支持继承。如果 class B 继承 class A,则我们称 A 为父类,B 为子类。子类将继承父类中除 private 成员和构造函数以外的所有成员。

抽象 class 总是可被继承的,故抽象类定义时的 open 修饰符是可选的,也可以使用 sealed 修饰符修饰抽象类,表示该抽象类只能在本包被继承。但非抽象的 class 可被继承是有条件的:定义时必须使用修饰符 open 修饰。当带 open 修饰的实例成员被 class 继承时,该 open 的修饰符也会被继承。当非 open 修饰的类中存在 open 修饰的成员时,编译器会给出告警。

可以在子类定义处通过 <: 指定其继承的父类,但要求父类必须是可继承的。例如,下面的例子中,class A 使用 open 修饰,是可以被 class B 继承的,但是因为 class B 是不可继承的,所以 C 在继承 B 的时候会报错。

open class A {
    let a: Int64 = 10
}

class B <: A { // Ok: 'B' Inheritance 'A'
    let b: Int64 = 20
}

class C <: B { // Error: 'B' is not inheritable
    let c: Int64 = 30
}

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

open class A {
    let a: Int64 = 10
}

open class B {
    let b: Int64 = 20
}

class C <: A & B { // Error: 'C' can only inherit one class
    let c: Int64 = 30
}

因为 class 是单继承的,所以任何 class 都最多只能有一个直接父类。对于定义时指定了父类的 class,它的直接父类就是定义时指定的类,对于定义时未指定父类的 class,它的直接父类是 Object 类型。Object 是所有 class 的父类(注意,Object 没有直接父类,并且 Object 中不包含任何成员)。

因为子类是继承自父类的,所以子类的对象天然可以当做父类的对象使用,但是反之不然。例如,下例中 B 是 A 的子类,那么 B 类型的对象可以赋值给 A 类型的变量,但是 A 类型的对象不能赋值给 B 类型的变量。

open class A {
    let a: Int64 = 10
}

class B <: A {
    let b: Int64 = 20
}

let a: A = B() // Ok: subclass objects can be assigned to superclass variables
open class A {
    let a: Int64 = 10
}

class B <: A {
    let b: Int64 = 20
}

let b: B = A() // Error: superclass objects can not be assigned to subclass variables

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

class A <: A {}  // Error, 'A' inherits itself.

sealed 修饰符只能修饰抽象类,表示被修饰的 class 定义只能在本定义所在的包内被其他 class 继承。sealed 已经蕴含了 public/open 的语义,因此定义 sealed abstract class 时若提供 public/open 修饰符,编译器将会告警。sealed 的子类可以不是 sealed 类,仍可被 open/sealed 修饰,或不使用任何继承性修饰符。若 sealed 类的子类被 open 修饰,则其子类可在包外被继承。sealed 的子类可以不被 public 修饰。

package A
public sealed abstract class C1 {}   // Warning, redundant modifier, 'sealed' implies 'public'
sealed open abstract class C2 {}     // Warning, redundant modifier, 'sealed' implies 'open'
sealed abstract class C3 {}          // OK, 'public' is optional when 'sealed' is used

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 is sealed class, cannot be inherited here.
sealed class SS3 {} // Error, 'sealed' cannot be used on non-abstract class.

父类构造函数调用

子类的 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()` added by compiler
        this.b = b
    }
}

open class C <: B {
    let c: Int64
    init(c: Int64) {  // Error, there is no non-parameter constructor in super class
        this.c = c
    }
}

覆盖和重定义

子类中可以覆盖(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 中的函数 fb 的运行时类型是 B(编译时类型是 A),因此 b.f() 调用的是子类 B 中的函数 f。所以程序会输出:

I am superclass
I am subclass

对于静态函数,子类中可以重定义父类中的同名非抽象静态函数,即在子类中为父类中的某个静态函数定义新的实现。重定义时,要求子类中的同名静态函数使用 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 中的函数 fooD.foo() 调用的是子类 D 中的函数 foo

I am class C
I am class D

如果抽象函数或 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 {
    static func f<T>(a: T): Unit where T <: B {}
    static func g<T>(): Unit where T <: B {}
}

class D <: Base {
    redef static func f<T>(a: T): Unit where T <: C {} // Error: stricter constraint
    redef static func g<T>(): Unit where T <: C {} // Error: stricter constraint
}

class E <: Base {
    redef static func f<T>(a: T): Unit where T <: A {} // OK: looser constraint
    redef static func g<T>(): Unit where T <: A {} // OK: looser constraint
}

class F <: Base {
    redef static func f<T>(a: T): Unit where T <: B {} // OK: same constraint
    redef static func g<T>(): Unit where T <: B {} // OK: same constraint
}

接口

接口用来定义一个抽象类型,它不包含数据,但可以定义类型的行为。一个类型如果声明实现某接口,并且实现了该接口中所有的成员,就被称为实现了该接口。

接口的成员可以包含:

  • 成员函数
  • 操作符重载函数
  • 成员属性

这些成员都是抽象的,要求实现类型必须拥有对应的成员实现。

接口的定义

一个简单的接口定义如下:

/*'open' is optional */ interface I {
    func f(): Unit
}

接口使用关键字 interface 声明,其后是接口的标识符 I 和接口的成员。接口成员可被 open 修饰符修饰,并且 open 修饰符是可选的。

当接口 I 声明了一个成员函数 f 之后,当我们要为一个类型实现 I 时,就必须要在该类型中实现一个对应的 f 函数。

因为 interface 默认具有 open 语义,所以 interface 定义时的 open 修饰符是可选的。

如下面的代码所示,我们定义了一个 class Foo,使用 Foo <: I 的形式声明了 Foo 实现 I 接口。

Foo 中必须包含 I 声明的所有成员的实现,即需要定义一个相同签名的 f,否则会由于没有实现接口而编译报错。

class Foo <: I {
    public func f(): Unit {
        println("Foo")
    }
}

main() {
    let a = Foo()
    let b: I = a
    b.f() // "Foo"
}

当某个类型实现了某个接口之后,该类型就会成为该接口的子类型。

对于上面的例子,我们就可以认为 FooI 的子类型,因此任何一个 Foo 类型的实例,都可以当作 I 类型的实例使用。

main 中我们将一个 Foo 类型的变量 a,赋值给一个 I 类型的变量 b。然后我们再调用 b 中的函数 f,就会打印出 Foo 实现的 f 版本。程序的输出结果为:

Foo

interface 也可以使用 sealed 修饰符表示只能在 interface 定义所在的包内继承、实现或扩展该 interfacesealed 已经蕴含了 public/open 的语义,因此定义 sealed interface 时若提供 public/open 修饰符,编译器将会告警。继承 sealed 接口的子接口或实现 sealed 接口的类仍可被 sealed 修饰或不使用 sealed 修饰。若 sealed 接口的子接口被 public 修饰,且不被 sealed 修饰,则其子接口可在包外被继承、实现或扩展。继承、实现 sealed 接口的类型可以不被 public 修饰。

package A
public interface I1 {}
sealed interface I2 {}         // OK
public sealed interface I3 {}  // Warning, redundant modifier, 'sealed' implies 'public'
sealed open interface I4 {}    // Warning, redundant modifier, 'sealed' implies 'open'

class C1 <: I1 {}
public open class C2 <: I1 {}
sealed class C3 <: I2 {}
extend Int64 <: I2 {}
package B
import A.*

class S1 <: I1 {}  // OK
class S2 <: I2 {}  // Error, I2 is sealed interface, cannot be inherited here.

通过接口的这种约束能力,我们可以对一系列的类型约定共同的功能,达到对功能进行抽象的目的。

例如下面的代码,我们可以定义一个 Flyable 接口,并且让其他具有 Flyable 属性的类实现它。

interface Flyable {
    func fly(): Unit
}

class Bird <: Flyable {
    public func fly(): Unit {
        println("Bird flying")
    }
}

class Bat <: Flyable {
    public func fly(): Unit {
        println("Bat flying")
    }
}

class Airplane <: Flyable {
    public func fly(): Unit {
        println("Airplane flying")
    }
}

func fly(item: Flyable): Unit {
    item.fly()
}

main() {
    let bird = Bird()
    let bat = Bat()
    let airplane = Airplane()
    fly(bird)
    fly(bat)
    fly(airplane)
}

编译并执行上面的代码,我们会看到如下输出:

Bird flying
Bat flying
Airplane flying

接口的成员可以是实例的或者静态的,以上的例子已经展示过实例成员函数的作用,接下来我们来看看静态成员函数的作用。

静态成员函数和实例成员函数类似,都要求实现类型提供实现。

例如下面的例子,我们定义了一个 NamedType 接口,这个接口含有一个静态成员函数 typename 用来获得每个类型的字符串名称。

这样其它类型在实现 NamedType 接口时就必须实现 typename 函数,之后我们就可以安全地在 NamedType 的子类型上获得类型的名称。

interface NamedType {
    static func typename(): String
}

class A <: NamedType {
    public static func typename(): String {
        "A"
    }
}

class B <: NamedType {
    public static func typename(): String {
        "B"
    }
}

main() {
    println("the type is ${ A.typename() }")
    println("the type is ${ B.typename() }")
}

程序输出结果为:

the type is A
the type is B

接口中的静态成员函数(或属性)可以没有默认实现,也可以拥有默认实现。

当其没有默认实现时,将无法通过接口类型名对其进行访问。例如下面的代码,直接访问 NamedTypetypename 函数会发生编译报错,因为 NamedType 不具有 typename 函数的实现。

main() {
    NamedType.typename() // error
}

接口中的静态成员函数(或属性)也可以拥有默认实现,当另一个类型继承拥有默认静态函数(或属性)实现的接口时,该类型可以不再实现这个静态成员函数(或属性),该函数(或属性)可以通过接口名和该类型名直接访问。如下用例,NamedType 的成员函数 typename 拥有默认实现,且在 A 中都可以不用再重新实现它,同时,也可以通过接口名和该类型名对其进行直接访问。

interface NamedType {
    static func typename(): String {
        "interface NamedType"
    }
}

class A <: NamedType {}

main() {
    println(NamedType.typename())
    println(A.typename())
    0
}

程序输出结果为:

interface NamedType
interface NamedType

通常我们会通过泛型约束,在泛型函数中使用这类静态成员。

例如下面的 printTypeName 函数,当我们约束泛型变元 TNamedType 的子类型时,我们需要保证 T 的实例化类型中所有的静态成员函数(或属性)都必须拥有实现,以保证可以使用 T.typename 的方式访问泛型变元的实现,达到了我们对静态成员抽象的目的。详见泛型章节。

interface NamedType {
    static func typename(): String
}

interface I <: NamedType {
    static func typename(): String {
        f()
    }
    static func f(): String
}

class A <: NamedType {
    public static func typename(): String {
        "A"
    }
}

class B <: NamedType {
    public static func typename(): String {
        "B"
    }
}

func printTypeName<T>() where T <: NamedType {
    println("the type is ${ T.typename() }")
}

main() {
    printTypeName<A>() // Ok
    printTypeName<B>() // Ok
    printTypeName<I>() // Error: 'I' must implement all static function. Otherwise, an unimplemented 'f' is called, causing problems.
}

需要注意的是,接口的成员默认就被 public 修饰,不可以声明额外的访问控制修饰符,同时也要求实现类型必须使用 public 实现。

interface I {
    func f(): Unit
}

open class C <: I {
    protected func f() {} // Compiler error, f needs to be public semantics
}

接口继承

当我们想为一个类型实现多个接口,可以在声明处使用 & 分隔多个接口,实现的接口之间没有顺序要求。

例如下面的例子,我们可以让 MyInt 同时实现 Addable 和 Subtractable 两个接口。

interface Addable {
    func add(other: Int64): Int64
}

interface Subtractable {
    func sub(other: Int64): Int64
}

class MyInt <: Addable & Subtractable {
    var value = 0
    public func add(other: Int64): Int64 {
        value + other
    }
    public func sub(other: Int64): Int64 {
        value - other
    }
}

接口可以继承一个或多个接口,但不能继承类。与此同时,接口继承的时候可以添加新的接口成员。

例如下面的例子,Calculable 接口继承了 Addable 和 Subtractable 两个接口,并且增加了乘除两种运算符重载。

interface Addable {
    func add(other: Int64): Int64
}

interface Subtractable {
    func sub(other: Int64): Int64
}

interface Calculable <: Addable & Subtractable {
    func mul(other: Int64): Int64
    func div(other: Int64): Int64
}

这样实现类型实现 Calculable 接口时就必须同时实现加减乘除四种运算符重载,不能缺少任何一个成员。

class MyInt <: Calculable {
    var value = 0
    public func add(other: Int64): Int64 {
        value + other
    }
    public func sub(other: Int64): Int64 {
        value - other
    }
    public func mul(other: Int64): Int64 {
        value * other
    }
    public func div(other: Int64): Int64 {
        value / other
    }
}

MyInt 实现 Calculable 的同时,也同时实现了 Calculable 继承的所有接口,因此 MyInt 也实现了 Addable 和 Subtractable,即同时是它们的子类型。

main() {
    let myInt = MyInt()
    let add: Addable = myInt
    let sub: Subtractable = myInt
    let calc: Calculable = myInt
}

对于 interface 的继承,子接口如果继承了父接口中有默认实现的函数或属性,则在子接口中不允许仅写此函数或属性的声明(即没有默认实现),而是必须要给出新的默认实现,并且函数定义前的 override 修饰符(或 redef 修饰符)是可选的;子接口如果继承了父接口中没有默认实现的函数或属性,则在子接口中允许仅写此函数或属性的声明(当然也允许定义默认实现),并且函数声明或定义前的 override 修饰符(或 redef 修饰符)是可选的。

interface I1 {
   func f(a: Int64) {
        a
   }
   static func g(a: Int64) {
        a
   }
   func f1(a: Int64): Unit
   static func g1(a: Int64): Unit
}

interface I2 <: I1 {
    /*'override' is optional*/ func f(a: Int64) {
       a + 1
    }
    override func f(a: Int32) {} // error: override function 'f' does not have an overridden function from its supertypes
    static /*'redef' is optional*/ func g(a: Int64) {
       a + 1
    }
    /*'override' is optional*/ func f1(a: Int64): Unit {}
    static /*'redef' is optional*/ func g1(a: Int64): Unit {}
}

接口实现

仓颉所有的类型都可以实现接口,包括数值类型、Char、String、struct、class、enum、Tuple、函数以及其它类型。

一个类型实现接口有三种途径:

  1. 在定义类型时就声明实现接口,在以上的内容中我们已经见过相关例子。
  2. 通过扩展实现接口,这种方式详见扩展章节。
  3. 由语言内置实现,具体详见仓颉内置类型及库的相关文档。

实现类型声明实现接口时,需要实现接口中要求的所有成员,为此需要满足下面一些规则。

  1. 对于成员函数和操作符重载函数,要求实现类型提供的函数实现与接口对应的函数名称相同、参数列表相同、返回类型相同。
  2. 对于成员属性,要求是否被 mut 修饰保持一致,并且属性的类型相同。

所以大部分情况都如同上面的例子,我们需要让实现类型中包含与接口要求的一样的成员的实现。

但有个地方是个例外,如果接口中的成员函数或操作符重载函数的返回值类型是 class 类型,那么允许实现函数的返回类型是其子类型。

例如下面这个例子,I 中的 f 返回类型是一个 class 类型 Base,因此 C 中实现的 f 返回类型可以是 Base 的子类型 Sub

open class Base {}
class Sub <: Base {}

interface I {
    func f(): Base
}

class C <: I {
    public func f(): Sub {
        Sub()
    }
}

除此以外,接口的成员还可以为 class 类型提供默认实现。拥有默认实现的接口成员,当实现类型是 class 的时候,class 可以不提供自己的实现而继承接口的实现。

需要注意的是,默认实现只对类型是 class 的实现类型有效,对其它类型无效。

例如下面的代码中,SayHi 中的 say 拥有默认实现,因此 A 实现 SayHi 时可以继承 say 的实现,而 B 也可以选择提供自己的 say 实现。

interface SayHi {
    func say() {
        "hi"
    }
}

class A <: SayHi {}

class B <: SayHi {
    public func say() {
        "hi, B"
    }
}

特别地,如果一个类型在实现多个接口时,多个接口中包含同一个成员的默认实现,这时会发生多重继承的冲突,语言无法选择最适合的实现,因此这时接口中的默认实现也会失效,需要实现类型提供自己的实现。

例如下面的例子,SayHiSayHello 中都包含了 say 的实现,Foo 在实现这两个接口时就必须提供自己的实现,否则会出现编译错误。

interface SayHi {
    func say() {
        "hi"
    }
}

interface SayHello {
    func say() {
        "hello"
    }
}

class Foo <: SayHi & SayHello {
    public func say() {
        "Foo"
    }
}

struct、enum 和 class 在实现接口时,函数或属性定义前的 override 修饰符(或 redef 修饰符)是可选的,无论接口中的函数或属性是否存在默认实现。

interface I {
    func foo(): Int64 {
        return 0
    }
}
enum E <: I{
    elem
    public override func foo(): Int64 {
        return 1
    }
}
struct S <: I {
    public override func foo(): Int64 {
        return 1
    }
}

Any 类型

Any 类型是一个内置的接口,它的定义如下面。

interface Any {}

仓颉中所有接口都默认继承它,所有非接口类型都默认实现它,因此所有类型都可以作为 Any 类型的子类型使用。

如下面的代码,我们可以将一系列不同类型的变量赋值给 Any 类型的变量。

main() {
    var any: Any = 1
    any = 2.0
    any = "hello, world!"
}

子类型关系

与其他面向对象语言一样,仓颉语言提供子类型关系和子类型多态。举例说明(不限于下述用例):

  • 假设函数的形参是类型 T,则函数调用时传入的参数的实际类型既可以是 T 也可以是 T 的子类型(严格地说,T 的子类型已经包括 T 自身,下同)。
  • 假设赋值表达式 = 左侧的变量的类型是 T,则 = 右侧的表达式的实际类型既可以是 T 也可以是 T 的子类型。
  • 假设函数定义中用户标注的返回类型是 T,则函数体的类型(以及函数体内所有 return 表达式的类型)既可以是 T 也可以是 T 的子类型。

那么如何判定两个类型是否存在子类型关系呢?下面我们对此展开说明。

继承 class 带来的子类型关系

继承 class 后,子类即为父类的子类型。如下代码中, Sub 即为 Super 的子类型。

open class Super { }
class Sub <: Super { }

实现接口带来的子类型关系

实现接口(含扩展实现)后,实现接口的类型即为接口的子类型。如下代码中,I3I1I2 的子类型, CI1 的子类型, Int64I2 的子类型:

interface I1 { }
interface I2 { }

interface I3 <: I1 & I2 { }

class C <: I1 { }

extend Int64 <: I2 { }

需要注意的是,部分跨扩展类型赋值后的类型向下转换场景(isas)支持不完善,可能出现判断失败,计划在未来版本修复,见如下示例:

// file1.cj
package p1

public class A{}

public func get(): Any {
    return A()
}

// =====================
// file2.cj
import p1.*

interface I0 {}

extend A <: I0 {}

main() {
    let v: Any = get()
    println(v is I0) // 无法正确判断类型,打印内容不确定
}

元组类型的子类型关系

仓颉语言中的元组类型也有子类型关系。直观的,如果一个元组 t1 的每个元素的类型都是另一个元组 t2 的对应位置元素类型的子类型,那么元组 t1 的类型也是元组 t2 的类型的子类型。例如下面的代码中,由于 C2 <: C1C4 <: C3,因此也有 (C2, C4) <: (C1, C3) 以及 (C4, C2) <: (C3, C1)

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

open class C3 { }
class C4 <: C3 { }

let t1: (C1, C3) = (C2(), C4()) // OK
let t2: (C3, C1) = (C4(), C2()) // OK

函数类型的子类型关系

仓颉语言中,函数是一等公民,而函数类型亦有子类型关系:给定两个函数类型 (U1) -> S2(U2) -> S1(U1) -> S2 <: (U2) -> S1 当且仅当 U2 <: U1S2 <: S1(注意顺序)。例如下面的代码定义了两个函数 f : (U1) -> S2g : (U2) -> S1,且 f 的类型是 g 的类型的子类型。由于 f 的类型是 g 的子类型,所以代码中使用到 g 的地方都可以换为 f

open class U1 { }
class U2 <: U1 { }

open class S1 { }
class S2 <: S1 { }


func f(a: U1): S2 { S2() }
func g(a: U2): S1 { S1() }

func call1() {
    g(U2()) // OK.
    f(U2()) // OK.
}

func h(lam: (U2) -> S1): S1 {
    lam(U2())
}

func call2() {
    h(g) // OK.
    h(f) // OK.
}

对于上面的规则,S2 <: S1 部分很好理解:函数调用产生的结果数据会被后续程序使用,函数 g 可以产生 S1 类型的结果数据,函数 f 可以产生 S2 类型的结果,而 g 产生的结果数据的应当能被 f 产生的结果数据替代,因此要求 S2 <: S1

对于 U2 <: U1 的部分,可以这样理解:在函数调用产生结果前,它本身应当能够被调用,函数调用的实参类型固定不变,同时形参类型要求更宽松时,依然可以被调用,而形参类型要求更严格时可能无法被调用——例如给定上述代码中的定义 g(U2()) 可以被换为 f(U2()),正是因为实参类型 U2 的要求更严格于形参类型 U1

永远成立的子类型关系

仓颉语言中,有些预设的子类型关系是永远成立的:

  • 一个类型 T 永远是自身的子类型,即 T <: T
  • Nothing 类型永远是其他任意类型 T 的子类型,即 Nothing <: T
  • 任意类型 T 都是 Any 类型的子类型,即 T <: Any
  • 任意 class 定义的类型都是 Object 的子类型,即如果有 class C {},则 C <: Object

传递性带来的子类型关系

子类型关系具有传递性。如下代码中,虽然只描述了 I2 <: I1C <: I2,以及 Bool <: I2,但根据子类型的传递性,也隐式存在 C <: I1 以及 Bool <: I1 这两个子类型关系。

interface I1 { }
interface I2 <: I1 { }

class C <: I2 { }

extend Bool <: I2 { }

泛型类型的子类型关系

泛型类型间也有子类型关系,详见泛型章节。

类型转换

仓颉不支持不同类型之间的隐式转换(我们认为子类型天然是父类型,所以子类型到父类型的转换不是隐式类型转换),类型转换必须显式地进行。下面将依次介绍数值类型之间的转换,CharUInt32 和整数类型到 Char 的转换,以及 isas 操作符。

数值类型之间的转换

对于数值类型(包括:Int8Int16Int32Int64IntNativeUInt8UInt16UInt32UInt64UIntNativeFloat16Float32Float64),仓颉支持使用 T(e) 的方式得到一个值等于 e,类型为 T 的值。其中,表达式 e 的类型和 T 可以是上述任意数值类型。

下面的例子展示了数值类型之间的类型转换:

main() {
    let a: Int8 = 10
    let b: Int16 = 20
    let r1 = Int16(a)
    println("The type of r1 is 'Int16', and r1 = ${r1}")
    let r2 = Int8(b)
    println("The type of r2 is 'Int8', and r2 = ${r2}")

    let c: Float32 = 1.0
    let d: Float64 = 1.123456789
    let r3 = Float64(c)
    println("The type of r3 is 'Float64', and r3 = ${r3}")
    let r4 = Float32(d)
    println("The type of r4 is 'Float32', and r4 = ${r4}")

    let e: Int64 = 1024
    let f: Float64 = 1024.1024
    let r5 = Float64(e)
    println("The type of r5 is 'Float64', and r5 = ${r5}")
    let r6 = Int64(f)
    println("The type of r6 is 'Int64', and r6 = ${r6}")
}

上述代码的执行结果为:

The type of r1 is 'Int16', and r1 = 10
The type of r2 is 'Int8', and r2 = 20
The type of r3 is 'Float64', and r3 = 1.000000
The type of r4 is 'Float32', and r4 = 1.123457
The type of r5 is 'Float64', and r5 = 1024.000000
The type of r6 is 'Int64', and r6 = 1024

CharUInt32 和整数类型到 Char 的转换

CharUInt32 的转换使用 UInt32(e) 的方式,其中 e 是一个 Char 类型的表达式,UInt32(e) 的结果是 e 的 Unicode scalar value 对应的 UInt32 类型的整数值。

整数类型到 Char 的转换使用 Char(num) 的方式,其中 num 的类型可以是任意的整数类型,且仅当 num 的值落在 [0x0000, 0xD7FF][0xE000, 0x10FFFF] (即 Unicode scalar value)中时,返回对应的 Unicode scalar value 表示的字符,否则,编译报错(编译时可确定 num 的值)或运行时抛异常。

下面的例子展示了 CharUInt32 之间的类型转换:

main() {
    let x: Char = 'a'
    let y: UInt32 = 65
    let r1 = UInt32(x)
    let r2 = Char(y)
    println("The type of r1 is 'UInt32', and r1 = ${r1}")
    println("The type of r2 is 'Char', and r2 = ${r2}")
}

上述代码的执行结果为:

The type of r1 is 'UInt32', and r1 = 97
The type of r2 is 'Char', and r2 = A

isas 操作符

仓颉支持使用 is 操作符来判断某个表达式的类型是否是指定的类型(或其子类型)。具体而言,对于表达式 e is Te 可以是任意表达式,T 可以是任何类型),当 e 的运行时类型是 T 的子类型时,e is T 的值为 true,否则 e is T 的值为 false

下面的例子展示了 is 操作符的使用:

open class Base {
    var name: String = "Alice"
}
class Derived <: Base {
    var age: UInt8 = 18
}

main() {
    let a = 1 is Int64
    println("Is the type of 1 'Int64'? ${a}")
    let b = 1 is String
    println("Is the type of 1 'String'? ${b}")

    let b1: Base = Base()
    let b2: Base = Derived()
    var x = b1 is Base
    println("Is the type of b1 'Base'? ${x}")
    x = b1 is Derived
    println("Is the type of b1 'Derived'? ${x}")
    x = b2 is Base
    println("Is the type of b2 'Base'? ${x}")
    x = b2 is Derived
    println("Is the type of b2 'Derived'? ${x}")
}

上述代码的执行结果为:

Is the type of 1 'Int64'? true
Is the type of 1 'String'? false
Is the type of b1 'Base'? true
Is the type of b1 'Derived'? false
Is the type of b2 'Base'? true
Is the type of b2 'Derived'? true

as 操作符可以用于将某个表达式的类型转换为指定的类型。因为类型转换有可能会失败,所以 as 操作返回的是一个 Option 类型。具体而言,对于表达式 e as Te 可以是任意表达式,T 可以是任何类型),当 e 的运行时类型是 T 的子类型时,e as T 的值为 Option<T>.Some(e),否则 e as T 的值为 Option<T>.None

下面的例子展示了 as 操作符的使用(注释中标明了 as 操作的结果):

open class Base {
    var name: String = "Alice"
}
class Derived <: Base {
    var age: UInt8 = 18
}

let a = 1 as Int64     // a = Option<Int64>.Some(1)
let b = 1 as String    // b = Option<String>.None

let b1: Base = Base()
let b2: Base = Derived()
let d: Derived = Derived()
let r1 = b1 as Base    // r1 = Option<Base>.Some(b1)
let r2 = b1 as Derived // r2 = Option<Derived>.None
let r3 = b2 as Base    // r3 = Option<Base>.Some(b2)
let r4 = b2 as Derived // r4 = Option<Derived>.Some(b2)
let r5 = d as Base     // r5 = Option<Base>.Some(d)
let r6 = d as Derived  // r6 = Option<Derived>.Some(d)

类型别名

当某个类型的名字比较复杂或者在特定场景中不够直观时,可以选择使用类型别名的方式为此类型设置一个别名。

type I64 = Int64

类型别名的定义以关键字 type 开头,接着是类型的别名(如上例中的 I64),然后是等号 =,最后是原类型(即被取别名的类型,如上例中的 Int64)。

只能在源文件顶层定义类型别名,并且原类型必须在别名定义处可见。例如,下例中 Int64 的别名定义在 main 中将报错,LongNameClassB 类型在为其定义别名时不可见,同样报错。

main() {
    type I64 = Int64 // Error: type aliases can only be defined at the top level of the source file
}

class LongNameClassA { }
type B = LongNameClassB // Error: type 'LongNameClassB' is not defined

一个(或多个)类型别名定义中禁止出现(直接或间接的)循环引用。

type A = (Int64, A) // Error: 'A' refered itself

type B = (Int64, C) // Error: 'B' and 'C' are circularly refered
type C = (B, Int64)

类型别名并不会定义一个新的类型,它仅仅是为原类型定义了另外一个名字,它有如下几种使用场景:

  1. 作为类型使用,例如:

    type A = B
    class B {}
    var a: A = B() // Use typealias A as type B
    
  2. 当类型别名实际指向的类型为 class、struct 时,可以作为构造器名称使用:

    type A = B
    class B {}
    func foo() { A() }  // Use type alias A as constructor of B
    
  3. 当类型别名实际指向的类型为 class、interface、struct 时,可以作为访问内部静态成员变量或函数的类型名:

    type A = B
    class B {
        static var b : Int32 = 0;
        static func foo() {}
    }
    func foo() {
        A.foo() // Use A to access static method in class B
        A.b
    }
    
  4. 当类型别名实际指向的类型为 enum 时,可以作为 enum 声明的构造器的类型名:

    enum TimeUnit {
        Day | Month | Year
    }
    type Time = TimeUnit
    var a = Time.Day  
    var b = Time.Month   // Use type alias Time to access constructors in TimeUnit
    

需要注意的是,当前用户自定义的类型别名暂不支持在类型转换表达式中使用,参考如下示例:

type MyInt = Int32
MyInt(0)  // Error: no matching function for operator '()' function call