仓颉语言 -- 类和接口

1、类

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

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

1.1 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
    }
}

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

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

1.1.1 class 成员变量

class 成员变量分为实例成员变量静态成员变量,静态成员变量使用 static 修饰符修饰,必须有初值,只能通过类型名访问,参考如下示例:

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

let l = Rectangle.height // l = 20

实例成员变量定义时可以不设置初值(但必须标注类型),也可以设置初值,只能通过对象(即类的实例)访问,参考如下示例:

class Rectangle {
    let width = 10
    let height: Int64
    init(h: Int64){
        height = h
    }
}
let rec = Rectangle(20)
let l = rec.height // l = 20

1.1.2 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
    }
}

1.1.3 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 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

1.1.4 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. 终结器执行结束之后,如果这个对象还可以被继续访问,则属于未定义行为

1.1.5 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
    }
}

1.1.6 class 成员的访问修饰符

对于 class 的成员(包括成员变量成员属性构造函数成员函数),可以使用的访问修饰符有 4 种访问修饰符修饰:privateinternalprotectedpublic,缺省的含义是 internal

  • private 表示在 class 定义内可见。
  • internal 表示仅当前包及子包(包括子包的子包,详见章节)内可见。
  • protected 表示当前模块(详见章节)及当前类的子类可见。
  • public 表示模块内外均可见。
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
}

1.2 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

1.3 创建对象

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

let r = Rectangle(10, 20)

创建对象之后,可以通过对象访问(public 修饰的)实例成员变量和实例成员函数。例如,下例中通过 r.width 和 r.height 可分别访问 r 中 width 和 height 的值,通过 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 定义)。举例如下:

lass 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 之后,修改 r1 的 width 和 height 的值,r2 的 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 = 192
}

1.4 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' Inheritance 'A'
    let b: Int64 = 20
}

class C <: B { // Error, 'B' is not inheritable
    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' can only inherit one class
    let c: Int64 = 30
}

因为类是单继承的,所以任何类都最多只能有一个直接父类对于定义时指定了父类的 class,它的直接父类就是定义时指定的类,对于定义时未指定父类的 class,它的直接父类是 Object 类型Object 是所有类的父类(注意,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 修饰符只能修饰抽象类,表示被修饰的类定义只能在本定义所在的包内被其他类继承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.

1.4.1 父类构造函数调用

子类的 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
    }
}

1.4.2 覆盖和重定义

子类中可以覆盖(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

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

如果抽象函数或 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
}

2、接口

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

接口的成员可以包含

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

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

2.1 接口定义

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

interface I { // 'open' modifier is optional.
    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"
}

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

对于上面的例子,Foo 是 I 的子类型,因此任何一个 Foo 类型的实例,都可以当作 I 类型的实例使用

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

Foo

interface 也可以使用 sealed 修饰符表示只能在 interface 定义所在的包内继承、实现或扩展该 interface。sealed 已经蕴含了 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

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

当其没有默认实现时,将无法通过接口类型名对其进行访问。例如下面的代码,直接访问 NamedType 的 typename 函数会发生编译报错,因为 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 函数,当我们约束泛型变元 T 是 NamedType 的子类型时,我们需要保证 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
}

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

interface I {
    func f(): Unit
}

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

2.2 接口继承

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

例如下面的例子,我们可以让 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 {}
}

2.3 接口实现

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

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

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

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

  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"
    }
}

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

例如下面的例子,SayHi 和 SayHello 中都包含了 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
    }
}

2.4 Any 类型

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

interface Any {}

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

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

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

3、属性

属性(Properties)提供了一个 getter 和一个可选的 setter 来间接获取和设置值

使用属性的时候与普通变量无异,我们只需要对数据操作,对内部的实现无感知,可以更便利地实现访问控制、数据监控、跟踪调试、数据绑定等机制。

属性在使用时可以作为表达式或被赋值。此处以类和接口为例进行说明,但属性不仅限于类和接口。

以下是一个简单的例子,b 是一个典型的属性,封装了外部对 a 的访问:

class Foo {
    private var a = 0

    public mut prop b: Int64 {
        get() {
            println("get")
            a
        }
        set(value) {
            println("set")
            a = value
        }
    }
}

main() {
    var x = Foo()
    let y = x.b + 1 // get
    x.b = y // set
}

此处 Foo 提供了一个名为 b 的属性,针对 getter/setter 这两个功能,仓颉提供了 get 和 set 两种语法来定义。当一个类型为 Foo 的变量 x 在访问 b 时,会调用 b 的 get 操作返回类型为 Int64 的值,因此可以用来与 1 相加;而当 x 在对 b 进行赋值时,会调用 b 的 set 操作,将 y 的值传给 set 的 value,最终将 value 的值赋值给 a。

通过属性 b,外部对 Foo 的成员变量 a 完全不感知,但却可以通过 b 做到同样地访问和修改操作,实现了有效的封装性。所以程序的输出如下:

get
set

3.1 属性定义

属性可以在 interface、class、struct、enum、extend 中定义。

一个典型的属性语法结构如下:

class Foo {
    public prop a: Int64 {
        get() { 0 }
    }
    public mut prop b: Int64 {
        get() { 0 }
        set(v) {}
    }
}

其中使用 prop 声明的 a 和 b 都是属性,a 和 b 的类型都是 Int64。a 是无 mut 修饰符的属性,这类属性有且仅有定义 getter(对应取值)实现。b 是使用 mut 修饰的属性,这类属性必须分别定义 getter(对应取值)和 setter(对应赋值)的实现。

属性的 getter 和 setter 分别对应两个不同的函数。

  1. getter 函数类型是 () -> T,T 是该属性的类型,当使用该属性作为表达式时会执行 getter 函数。
  2. setter 函数类型是 (T) -> Unit,T 是该属性的类型,形参名需要显式指定,当对该属性赋值时会执行 setter 函数。

getter 和 setter 的实现中可以和函数体一样包含声明和表达式,与函数体的规则一样,详见函数体章节。

setter 中的参数对应的是赋值时传入的值。

class Foo {
    private var j = 0
    public mut prop i: Int64 {
        get() {
            j
        }
        set(v) {
            j = v
        }
    }
}

需要注意的是,在属性的 getter 和 setter 中访问属性自身属于递归调用,与函数调用一样可能会出现死循环的情况

3.1.1 修饰符

我们可以在 prop 前面声明需要的修饰符

class Foo {
    public prop a: Int64 {
        get() {
            0
        }
    }
    private prop b: Int64 {
        get() {
            0
        }
    }
}

和成员函数一样,成员属性也支持 open、override、redef 修饰,所以我们也可以在子类型中覆盖/重定义父类型属性的实现

子类型覆盖父类型的属性时,如果父类型属性带有 mut 修饰符,则子类型属性也需要带有 mut 修饰符,同时也必须保持一样的类型。

如下代码所示,A 中定义了 x 和 y 两个属性,B 中可以分别对 x 和 y 进行 override/redef:

open class A {
    private var valueX = 0
    private static var valueY = 0

    public open prop x: Int64 {
        get() { valueX }
    }

    public static mut prop y: Int64 {
        get() { valueY }
        set(v) {
            valueY = v
        }
    }
}
class B <: A {
    private var valueX2 = 0
    private static var valueY2 = 0

    public override prop x: Int64 {
        get() { valueX2 }
    }

    public redef static mut prop y: Int64 {
        get() { valueY2 }
        set(v) {
            valueY2 = v
        }
    }
}

3.1.2 抽象属性

类似于抽象函数,我们在 interface 和抽象类中也可以声明抽象属性,这些抽象属性没有实现。

interface I {
    prop a: Int64
}

abstract class C {
    public prop a: Int64
}

当实现类型实现 interface 或者非抽象子类继承抽象类时,必须要实现这些抽象属性。

与覆盖的规则一样,实现类型或子类在实现这些属性时,如果父类型属性带有 mut 修饰符,则子类型属性也需要带有 mut 修饰符,同时也必须保持一样的类型。

interface I {
    prop a: Int64
    mut prop b: Int64
}
class C <: I {
    private var value = 0

    public prop a: Int64 {
        get() { value }
    }

    public mut prop b: Int64 {
        get() { value }
        set(v) {
            value = v
        }
    }
}

通过抽象属性,我们可以让接口和抽象类对一些数据操作能以更加易用的方式进行约定,相比函数的方式要更加直观。

如下代码所示,如果我们要对一个 size 值的获取和设置进行约定,使用属性的方式 (I1) 相比使用函数的方式 (I2) 代码更少,也更加符合对数据操作的意图。

interface I1 {
    mut prop size: Int64
}

interface I2 {
    func getSize(): Int64
    func setSize(value: Int64): Unit
}

class C <: I1 & I2 {
    private var mySize = 0

    public mut prop size: Int64 {
        get() {
            mySize
        }
        set(value) {
            mySize = value
        }
    }

    public func getSize() {
        mySize
    }

    public func setSize(value: Int64) {
        mySize = value
    }
}

main() {
    let a: I1 = C()
    a.size = 5
    println(a.size)

    let b: I2 = C()
    b.setSize(5)
    println(b.getSize())
}
5
5

3.2 属性使用

属性分为实例成员属性静态成员属性成员属性的使用和成员变量的使用方式一样,详见成员变量章节。

class A {
    public prop x: Int64 {
        get() {
            123
        }
    }
    public static prop y: Int64 {
        get() {
            321
        }
    }
}

main() {
    var a = A()
    println(a.x) // 123
    println(A.y) // 321
}

无 mut 修饰符的属性类似 let 声明的变量,不可以被赋值。

class A {
    private let value = 0
    public prop i: Int64 {
        get() {
            value
        }
    }
}

main() {
    var x = A()
    println(x.i) // OK
    x.i = 1 // Error
}

带有 mut 修饰符的属性类似 var 声明的变量,可以取值也可以被赋值。

class A {
    private var value: Int64 = 0
    public mut prop i: Int64 {
        get() {
            value
        }
        set(v) {
            value = v
        }
    }
}

main() {
    var x = A()
    println(x.i) // OK 
    x.i = 1 // OK 
}

4、子类型关系

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

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

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

4.1 继承 class 带来的子类型关系

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

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

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

实现接口(含扩展实现)后,实现接口的类型即为接口的子类型。如下代码中,I3 是 I1 和 I2 的子类型, C 是 I1 的子类型, Int64 是 I2 的子类型:

interface I1 { }
interface I2 { }

interface I3 <: I1 & I2 { }

class C <: I1 { }

extend Int64 <: I2 { }

需要注意的是,部分跨扩展类型赋值后的类型向下转换场景(is 或 as)暂不支持,可能出现判断失败,见如下示例:

// 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) // 无法正确判断类型,打印内容不确定
}

4.3 元组类型的子类型关系

仓颉语言中的元组类型也有子类型关系。直观的,如果一个元组 t1 的每个元素的类型都是另一个元组 t2 的对应位置元素类型的子类型,那么元组 t1 的类型也是元组 t2 的类型的子类型。例如下面的代码中,由于 C2 <: C1 和 C4 <: 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

4.4 函数类型的子类型关系

仓颉语言中,函数是一等公民,而函数类型亦有子类型关系:给定两个函数类型 (U1) -> S2 和 (U2) -> S1,(U1) -> S2 <: (U2) -> S1 当且仅当 U2 <: U1 且 S2 <: S1(注意顺序)。例如下面的代码定义了两个函数 f : (U1) -> S2 和 g : (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 。

4.5 永远成立的子类型关系

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

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

4.6 传递性带来的子类型关系

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

interface I1 { }
interface I2 <: I1 { }

class C <: I2 { }

extend Bool <: I2 { }

4.7 泛型类型的子类型关系

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

5、类型转换

仓颉不支持不同类型之间的隐式转换子类型天然是父类型,所以子类型到父类型的转换不是隐式类型转换),类型转换必须显式地进行。下面将依次介绍数值类型之间的转换,Rune 到 UInt32 和整数类型到 Rune 的转换,以及 is 和 as 操作符。

5.1 数值类型之间的转换

对于数值类型(包括:Int8,Int16,Int32,Int64,IntNative,UInt8,UInt16,UInt32,UInt64,UIntNative,Float16,Float32,Float64),仓颉支持使用 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

5.2 Rune 到 UInt32 和整数类型到 Rune 的转换

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

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

下面的例子展示了 Rune 和 UInt32 之间的类型转换:

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

上述代码的执行结果为:

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

5.3 is 和 as 操作符

仓颉支持使用 is 操作符来判断某个表达式的类型是否是指定的类型(或其子类型)。具体而言,对于表达式 e is T(e 可以是任意表达式,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 T(e 可以是任意表达式,T 可以是任何类型),当 e 的运行时类型是 T 的子类型时,e as T 的值为 Option.Some(e),否则 e as T 的值为 Option.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)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值