4884 字
24 分钟
仓颉文档阅读-开发指南IV: 函数(V) - 函数重载 与 操作符重载
NOTE

阅读文档版本:

语言规约 Cangjie-0.53.18-Spec

具体开发指南 Cangjie-LTS-1.0.4

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

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

WARNING

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

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

WARNING

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

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

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

函数#

函数重载#

函数重载定义#

在仓颉编程语言中, 如果一个作用域中, 一个函数名对应多个函数定义, 这种现象称为函数重载

函数重载, 就是同一个作用域中, 可以存在多个同名函数, 但他们的参数列表(参数类型、参数数量、类型顺序)必须不同

如果同名函数的参数列表不同, 则同名函数之间构成重载

否则, 同名函数会发生重复定义, 编译是会报错的

C语言中就不存在函数重载, 同一作用域中 所有函数不能同名

  • 函数名相同, 函数参数不同(是指参数个数不同, 或者参数个数相同但参数类型不同)的两个函数构成重载

    示例如下:

    // Scenario 1
    func f(a: Int64): Unit {
    }
    func f(a: Float64): Unit {
    }
    func f(a: Int64, b: Float64): Unit {
    }

对于相同作用域内的普通函数, 只要函数名相同, 参数列表不同(参数类型、参数数量), 就构成重载

  • 对于两个同名泛型函数 (详见泛型函数章节))

    如果重命名一个函数的泛型形参后(使泛型参数顺序相同), 其非泛型部分与另一个函数的非泛型部分函数参数不同, 则两个函数构成重载, 否则这两个泛型函数构成重复定义错误(类型变元的约束不参与判断)

    示例如下:

    interface I1{}
    interface I2{}
    func f1<X, Y>(a: X, b: Y) {}
    func f1<Y, X>(a: X, b: Y) {} // Ok: 重命名泛型类型参数后, 它将是 'func f1<X, Y>(a: Y, b: X)'
    func f2<T>(a: T) where T <: I1 {}
    func f2<T>(a: T) where T <: I2 {} // Error, 不重载

对于泛型函数, 重载的判断相对复杂一些

因为, 泛型函数的类型参数列表只是一个标记, 不能表示实际类型

所以, 对于同名泛型函数 即使类型参数列表中 参数名字不一样或顺序不一样, 实际也不能看作是不同的泛型函数

重点还是要看非泛型部分, 即 函数形参列表

具体的分辨方式为: 将同名泛型函数的 类型参数列表 重命名为一样时, 如果对应函数形参列表不同, 则构成重载, 否则就是重定义

且, 泛型类型参数的约束 不参与重载判断, 所以对于泛型函数, 实际也是只看同名函数的参数列表

  • 同一个类内的两个构造函数参数不同, 构成重载

    示例如下:

    // Scenario 2
    class C {
    var a: Int64
    var b: Float64
    public init(a: Int64, b: Float64) {
    this.a = a
    this.b = b
    }
    public init(a: Int64) {
    b = 0.0
    this.a = a
    }
    }
  • 同一个类内的主构造函数和init构造函数参数不同, 构成重载(认为主构造函数和init函数具有相同的名字)

    示例如下:

    // Scenario 3
    class C {
    C(var a!: Int64, var b!: Float64) {
    this.a = a
    this.b = b
    }
    public init(a: Int64) {
    b = 0.0
    this.a = a
    }
    }

类的构造函数之间是可以构成重载的, 但要注意主构造函数也参与到init构造函数之间的重载判断

因为主构造函数只是语法糖, 实际上编译器还是会将其转换为init构造函数

  • 两个函数名相同, 参数不同的函数定义在不同的作用域, 在两个函数都可见的作用域中构成重载

    示例如下:

    // Scenario 4
    func f(a: Int64): Unit {
    }
    func g() {
    func f(a: Float64): Unit {
    }
    }

在上述的例子中, g()函数的函数体内, f(Int64)f(Float64)构成重载, 因为两个函数都在g()函数体内可见

  • 如果子类中存在与父类同名的函数, 并且函数的参数类型不同, 则构成函数重载

    示例如下:

    // Scenario 5
    open class Base {
    public func f(a: Int64): Unit {
    }
    }
    class Sub <: Base {
    public func f(a: Float64): Unit {
    }
    }

对于拥有继承关系的类, 如果子类定义有同名成员函数, 如果形参列表不同, 则构成函数重载

只允许函数声明引入的函数重载, 但是以下情形不构成重载, 不构成重载的两个名字 不能定义或声明 在同一个作用域内:

  • classinterfacestruct类型的静态成员函数和实例成员函数之间不能重载

  • enum类型的constructor、静态成员函数和实例成员函数之间不能重载

如下示例, 两个变量均为函数类型且函数参数类型不同, 但由于它们不是函数声明所以不能重载, 如下示例将编译报错(重定义错):

main() {
var f: (Int64) -> Unit
var f: (Float64) -> Unit
}

如下示例, 虽然变量f为函数类型, 但由于变量和函数之间不能同名, 如下示例将编译报错(重定义错):

main() {
var f: (Int64) -> Unit
func f(a: Float64): Unit { // Error, 函数和变量不能同名
}
}

如下示例, 静态成员函数f与实例成员函数f的参数类型不同, 但由于类内静态成员函数和实例成员函数之间不能重载, 如下示例将编译报错:

class C {
public static func f(a: Int64): Unit {
}
public func f(a: Float64): Unit {
}
}

首先明确一点, 函数重载只发生在函数声明、定义上!!

仓颉中, 类、结构体、接口的静态成员函数 与 实例成员函数之间 不能构成重载

因此, 静态成员函数 和 实例成员函数之间 禁止同名

而在, enum当中, constructor和静态成员函数以及实例成员函数, 三者之间 不能构成重载

函数重载决议#

函数调用时, 所有可被调用的函数(是指当前作用域可见且能通过类型检查的函数)构成候选集, 候选集中有多个函数, 究竟选择候选集中哪个函数, 需要进行函数重载决议

这句话的意思是, 仓颉中, 函数被调用时, 所有可见且合法的同名函数会构成候选集

在函数调用时, 需要从候选集中选择、决定究竟调用哪个函数, 这就是 函数重载决议

即使 被调用的函数可能并没有发生重载

有如下规则:

  • 优先选择作用域级别高的作用域内的函数

    在嵌套的表达式或函数中, 越是内层作用域级别越高

    如下示例中在inner函数体内调用g(Sub())时, 候选集包括inner函数内定义的函数ginner函数外定义的函数g, 函数决议选择作用域级别更高的inner函数内定义的函数g

    open class Base {}
    class Sub <: Base {}
    func outer() {
    func g(a: Sub) {
    print("1")
    }
    func inner() {
    func g(a: Base) {
    print("2")
    }
    g(Sub()) // Output: 2
    }
    }
  • 如果作用域级别相对最高的仍有多个函数, 则需要选择最匹配的函数(对于函数fg以及给定的实参, 如果f可以被调用时g也总是可以被调用的, 但反之不然, 则称fg更匹配)

    如果不存在唯一最匹配的函数, 则报错

    如下示例中, 两个函数g定义在同一作用域, 选择更匹配的函数g(a: Sub): Unit

    open class Base {}
    class Sub <: Base {}
    func outer() {
    func g(a: Sub) {
    print("1")
    }
    func g(a: Base) {
    print("2")
    }
    g(Sub()) // Output: 1
    }
  • 子类和父类认为是同一作用域

    如下示例中, 一个函数g定义在父类中, 另一个函数g定义在子类中, 在调用s.g(Sub())时, 两个函数g当成同一作用域级别决议, 则选择更匹配的父类中定义的函数g(a: Sub): Unit

    open class Base {
    public func g(a: Sub) {
    print("1")
    }
    }
    class Sub <: Base {
    public func g(a: Base) {
    print("2")
    }
    }
    func outer() {
    let s: Sub = Sub()
    s.g(Sub()) // Output: 1
    }

函数重载决议并不复杂:

  1. 决议 函数定义作用域等级更高的

    不过实际可以简单理解为 就近匹配, 因为 越内层作用域等级越高

  2. 同作用域内, 决议 函数调用更匹配的

    更匹配, 就是函数调用是实参 与 函数形参匹配

  3. 子类与父类认为是同一作用域

    是指, 子类实例调用成员函数时, 而非父类

操作符重载#

如果希望 在某个类型上 支持 此类型默认不支持 的操作符, 可以使用操作符重载实现

如果需要在某个类型上重载某个操作符, 可以通过为类型定义一个函数名为此操作符的函数的方式实现, 这样, 在该类型的实例使用该操作符时, 就会自动调用此操作符函数

操作符函数定义与普通函数定义相似, 区别如下:

  • 定义操作符函数时需要在func关键字前面添加operator修饰符

  • 操作符函数的参数个数需要匹配对应操作符的要求(详见附录操作符)

  • 操作符函数只能定义在classinterfacestructenumextend

  • 操作符函数具有实例成员函数的语义, 所以禁止使用static修饰符

  • 操作符函数不能为泛型函数

另外, 需要注意的是, 被重载后的操作符不改变它们固有的优先级和结合性(详见附录操作符)

操作符重载, 是指为 自定义类型 支持特定操作符的运算

比如, 整型类型之间能够使用+实现加法运算

但, 如果是一个自定义的日期类型, 此类型默认肯定是不支持+操作符运算的

此时, 通过对+进行操作符重载就可以实现, 对特定类型支持使用+进行目标运算

操作符重载函数只能定义在classinterfacestructenumextend

操作符重载函数不能是泛型, 不能是静态的, 需要使用额外的operator修饰符, 且 形参数量要与对应原操作符的操作数保持匹配

操作符重载函数定义和使用#

定义操作符函数有两种方式:

  1. 对于可以直接包含函数定义的类型 (包括structenumclassinterface), 可以直接在其内部定义操作符函数的方式实现操作符的重载

  2. 使用extend的方式为其添加操作符函数, 从而实现操作符在这些类型上的重载

    对于无法直接包含函数定义的类型(是指除structclassenuminterface之外其他的类型)或无法改变其实现的类型

    比如第三方定义的structclassenuminterface, 只能采用这种方式(参见 扩展 )

操作符函数重载, 可以在定义一个类型时, 直接及进行定义, 这是能够自己控制、实现的类型

还有就是, 使用第三方库时, 可能无法直接看到类型定义, 但可以使用extend对类型进行扩展, 同样可以实现操作符重载

操作符函数对参数类型的约定如下:

  1. 对于一元操作符, 操作符函数没有参数, 对返回值的类型没有要求

  2. 对于二元操作符, 操作符函数只有一个参数, 对返回值的类型没有要求

    如下示例中介绍了一元操作符和二元操作符的定义和使用:

    -负号实现对一个Point实例中两个成员变量xy取负值, 然后返回一个新的Point对象

    +加号实现对两个Point实例中两个成员变量xy分别求和, 然后返回一个新的Point对象

    open class Point {
    var x: Int64 = 0
    var y: Int64 = 0
    public init (a: Int64, b: Int64) {
    x = a
    y = b
    }
    public operator func -(): Point {
    Point(-x, -y)
    }
    public operator func +(right: Point): Point {
    Point(this.x + right.x, this.y + right.y)
    }
    }

    接下来, 就可以在Point的实例上直接使用 一元-操作符 和 二元+操作符:

    main() {
    let p1 = Point(8, 24)
    let p2 = -p1 // p2 = Point(-8, -24)
    let p3 = p1 + p2 // p3 = Point(0, 0)
    }

仓颉中, 针对一元操作符和二元操作符的重载

一元操作符函数重载没有形参, 所以只能操作自己, 即this

二元操作符函数重载有一个形参, 这个形参就是 操作符的右操作数, 函数内this就是左操作数

  1. 索引操作符([])分为取值let a = arr[i]赋值arr[i] = a两种形式, 它们通过 是否存在特殊的命名参数value 来区分不同的重载

    索引操作符重载不要求同时重载两种形式, 可以只重载赋值不重载取值, 反之亦可

    索引操作符 取值形式, 使用时[]内的参数序列 对应 操作符重载的非命名参数, 可以是1个或多个, 可以是任意类型, 不可以有其他命名参数, 返回类型可以是任意类型

    class A {
    operator func [](arg1: Int64, arg2: String): Int64 {
    return 0
    }
    }
    func f() {
    let a = A()
    let b: Int64 = a[1, "2"]
    // b == 0
    }

    索引操作符 赋值形式, 使用时[]内的参数序列 对应 操作符重载的非命名参数, 可以是1个或多个, 可以是任意类型

    =右侧的表达式对应操作符重载的命名参数, 有且只能有一个命名参数, 该 命名参数的名称必须是value, 不能有默认值, value可以是任意类型, 返回类型必须是Unit类型

    需要注意的是, value只是一种特殊的标记, 在索引操作符赋值时并不需要使用命名参数的形式调用

    class A {
    operator func [](arg1: Int64, arg2: String, value!: Int64): Unit {
    return
    }
    }
    func f() {
    let a = A()
    a[1, "2"] = 0
    }

    特别的, 除enum外的不可变类型不支持重载索引操作符赋值形式

仓颉中的[]操作符, 因为存在 取值和赋值 两种使用方式

所以[]操作符重载时 通过一个名为value的命名形参, 来区分取值和赋值的重载

且, []操作符可以存在若干个操作数, 比如[1, 2.0, "3"], 所以[]操作符重载函数可以存在若干个非命名参数

如果是[1, 2.0, "3"], 则重载函数的参数列表可能为(param1: Int64, param2: Float64, param3: String)(param1: Int64, param2: Float64, param3: String, value!: Type)

使用时[]内的参数序列 对应 操作符重载的非命名参数

如果存在value命名参数, 则可以被赋值, 即为[]的赋值方式

如果不存在value命名参数, 则为[]取值方式

  1. 函数调用操作符(())重载函数, 输入参数和返回值类型可以是任意类型

    示例如下:

    open class A {
    public init() {}
    public operator func ()(): Unit {}
    }
    func test1() {
    let a = A() // Ok, A() 是调用A的构造函数
    a() // Ok, a() 是调用操作符()的重载函数
    }

    不能使用thissuper调用()操作符重载函数

    示例如下:

    open class A {
    public init() {}
    public init(x: Int64) {
    this() // Ok, this() 调用 A 的构造函数
    }
    public operator func ()(): Unit {}
    public func foo() {
    this() // Error, this() 调用 A 的构造函数
    super() // Error
    }
    }
    class B <: A {
    public init() {
    super() // Ok, super() 调用父类的构造函数
    }
    public func goo() {
    super() // Error
    }
    }

    对于枚举类型, 当构造器形式和()操作符重载函数形式都满足时, 优先匹配构造器形式

    示例如下:

    enum E {
    Y | X | X(Int64)
    public operator func ()(p: Int64) {}
    public operator func ()(p: Float64) {}
    }
    main() {
    let e = X(1) // Ok, X(1) 是调用构造函数X(Int64)
    X(1.0) // Ok, X(1.0) 是调用操作符 () 重载函数
    let e1 = X
    e1(1) // Ok, e1(1) 是调用操作符 () 重载函数
    Y(1) // Ok, Y(1) 是调用操作符 () 重载函数
    }

仓颉的自定义类型, 还可以对()操作符进行重载, 然后就可以通过实例()调用对应的函数

C++中也存在, C++实现了()重载函数的类的对象 可以被称为仿函数

仓颉中, 在类型内, 不能通过thissuper调用本类和父类的()重载函数

实现了()操作符的类型, 类型实例就可以像函数那样被调用

可以被重载的操作符#

下表列出了所有可以被重载的操作符(优先级从高到低):

OperatorDescription
()函数调用
[]索引
!逻辑非
-符号
**求幂
*
/
%取模
+加法
-减法
<<左移位
>>右移位
<小于
<=小于等于
>大于
>=大于等于
==判等
!=判不等
&按位与
^按位异或
|按位或

需要注意的是:

注意:

  • 一旦在某个类型上重载了除关系操作符(<<=>>===!=)之外的其他二元操作符, 并且操作符函数的返回类型与左操作数的类型一致或是其子类型, 那么此类型支持对应的复合赋值操作符

    当操作符函数的返回类型与左操作数的类型不一致且不是其子类型时, 在使用对应的复合赋值符号时将报类型不匹配错误

  • 仓颉编程语言不支持自定义操作符, 即不允许定义除上表中所列operator之外的其他操作符函数

  • 对于类型T, 如果T已经默认支持了上述若干可重载操作符, 那么通过扩展的方式再次为其实现同签名的操作符函数时将报重定义错误

    例如, 为数值类型重载其已支持的同签名算术操作符、位操作符或关系操作符等操作符时, 为Rune重载同签名的关系操作符时, 为Bool类型重载同签名的逻辑操作符、判等或不等操作符时, 等等这些情况, 均会报重定义错误

这一部分了解就好