重载

函数重载

函数重载的定义

在仓颉编程语言中,如果一个作用域中,同一个函数名对应多个参数类型不完全相同的函数定义,这种现象称为函数重载。

函数重载定义详见[函数重载定义]。

需要注意的是:

  1. class、interface、struct 类型中的静态成员函数和实例成员函数之间不能重载
  2. 同一个类型的扩展中的静态成员函数和实例成员函数之间不能重载,同一个类型的不同扩展中两个函数都是 private 的除外
  3. enum 类型的 constructor、静态成员函数和实例成员函数之间不能重载

下例中,class A 中的实例成员函数 f 和静态成员函数 f 重载,将编译报错。

class A {
    func f() {}
    //Static member function can not be overloaded with instance member function.
    static func f(a: Int64) {}  // Error
}

下例中,class A 的扩展中实例成员函数 g 和静态成员函数 g 重载,将编译报错。

class A {}

extend A {
    func g() {}
    static func g(a: Int64) {}  // Error
}

下例中,实例成员函数 h 和静态成员函数 h 在 class A 的不同扩展中,且都是 private,编译不报错。

extend A {
    private func h() {}
}

extend A {
    private static func h(a: Int64) {}   // OK
}

下例中,enum E 的 constructor f , 实例成员函数 f 和静态成员函数 f 重载,将编译报错。

enum E {
    f(Int64)  // constructor

    // Instance member function can not be overloaded with constructor.
    func f(a: Float64) {}  // Error

    // Static member function can not be overloaded with instance member function or constructor.
    static func f(a: Bool) {}  // Error
}

在进行函数调用时,需要根据函数调用表达式中的实参的类型和上下文信息明确是哪一个函数定义被使用,分为以下几个步骤:

第 1 步、构建函数候选集;

第 2 步、函数重载决议;

第 3 步、确定实参类型。

重载函数候选集

调用函数f时,首先需要确定哪些函数是可以被调用的函数,可以被调用的函数集合称为重载函数候选集(以下简称候选集),用于函数重载决议。

构建函数候选集主要是两个步骤:

  1. 查找可见函数集,即根据调用表达式的形式和上下文确定所有可见的函数;
  2. 对可见函数集中的函数进行函数调用的类型检查,通过类型检查的函数(即可以被调用的函数)进入候选集;

可见函数集

可见函数集需要满足以下规则:

  1. 作用域可见;
  2. 根据 f 是普通函数还是构造函数确定可见函数集: 2.1. 如果 f 是构造函数,则根据以下规则确定可见函数集:
  • 如果 f 是有构造函数的类型名字,则可见函数集仅包括f中定义的构造函数;

  • 否则,如果 fenum 的 constructor 名字,则可见函数集仅包括指定的 enum 中名为 f 的 constructor;

  • 否则,如果 fsuper,则表示该调用表达式出现在 class/interface 中,f 仅包含调用表达式所在 class/interface 的直接父类构造函数;

  • 否则,如果 f 是无构造函数的类型名,则可见函数集为空;

    2.2. 如果 f 是普通函数,则根据是否有限定前缀来确定可见函数集:

  • 不带限定前缀直接通过名字调用 f(...),可见函数集包含以下几种方式引入的名为 f 的函数:

    1. 作用域中可见的局部函数;

    2. 如果函数调用在 class/interface/struct/enum/extend 的静态上下文中,则包含类型的静态成员函数;

    3. 如果函数调用在 class/interface/struct/enum/extend 的非静态上下文中,则包含类型的静态和非静态成员函数;

    4. 函数调用所在 package 内定义的全局函数;

    5. 函数调用所在 package 中定义的 extend 中声明的函数;

    6. 函数调用所在 package 通过import方式导入的函数;

  • 对于带限定前缀的函数调用 c.f(...),可见函数集根据限定前缀来确定:

    1. 如果 c 是包名,可见函数集仅包含 package c 中定义的全局函数 f,包括c中通过 public import 重导出的函数,不包括 c 中仅通过 import 导入的函数;

    2. 如果 cclassinterfacestructenum 定义的类型名,则可见函数集仅包括 c 的静态成员方法 f

    3. 如果 cthis,即 this.f(...),表示该调用表达式出现在 classinterfacestructenum 的定义或扩展中。如果出现在类型定义中,可见函数集仅包含当前类型中的非静态成员方法 f(包括继承得到的,但不包括扩展中的);如果出现在类型扩展中,可见函数集包含类型中的非静态成员方法 f,也包含扩展中的非静态成员方法 f

    4. 如果 csuper,即 super.f(...),表示该调用表达式出现在 classinterface 中,则可见函数集仅包含当前类型的父类或父接口的非静态成员方法 f

    5. 如果函数调用的形式是 对象名.f(...) 的形式:

      如果对象的类型中有成员方法f,则可见函数集仅包含所有名字为f的成员方法;

2.3. 如果 f 是类型实例,则根据其类型或类型扩展中定义的 () 操作符重载函数来确定可见函数集。

  假设 `f` 的类型是 T,则可见函数集包括:

  1) T 内定义的 `()` 操作符重载函数;
  2) T 的扩展 (当前作用域可见) 中定义的 `()` 操作符重载函数;
  3) T 如果有父类,还包括从父类中继承的 `()` 操作符重载函数;
  4) T 如果实现了接口,还包括从接口中获得的带缺省实现的 `()` 操作符重载函数
  1. 对于泛型函数 f,进入可见函数集的可能是部分实例化后的泛型函数或是完全实例化的函数。具体是哪种形式进入可见函数集,由调用表达式的形式决定(详见,[泛型函数重载]).

类型检查

对于可见函数集中的函数进行类型检查,只有通过[函数调用类型检查]的函数才能进入候选集。

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

func f<X, Y>(a: X, b: Y) {}  // f1, number of type parameters is not matched
func f<X>(a: Base, b: X) where X <: Sub {} // f2 

func test() {
    f<Sub>(Base(), Sub())  // candidate set: { f2 }
}

需要注意的是:

  1. 如果实参有多个类型,在类型检查阶段,如果实参的多个类型中有一个能通过类型检查,则认为该实参能通过候选函数的类型检查;
open class A {}
open class B <: A {}

func g(a: A): B {  //g1
    B()
}
func g(a: B): B {  //g2
    B()
}
func g(a: Int64): Unit {}  //g3

// (A)->B <: (B)->B <: (B)->A
func f(a: (A)->B) {}   //f1, g1 can pass the type check
func f(a: (B)->B) {}   //f2, g1, g2 can pass the type check
func f(a: (B)->A) {}   //f3, g1, g2 can pass the type check
func f(a: Bool) {}     //f4, no g can pass the type check

func test() {
    f(g)  // candidate set: { f1, f2, f3 }
}

函数重载决议

如果候选集为空,即没有匹配项,编译报错;

如果候选集中只有一个函数,即只有一个匹配项,选择匹配的函数进行调用;

如果候选集中有多个函数,即有多个匹配项。先按照[作用域优先级]规则,选择候选集中作用域级别最高的。如果作用域级别最高的只有一个,则选择该函数;否则,根据[最匹配规则],选择最匹配项。若无法确定唯一最匹配项则编译报错。

作用域优先级

作用域级别越高,函数重载决议时优先级越高,即:候选集中两个函数,选作用域级别高的,如果作用域级别相同,则根据[最匹配规则]章节中的规则进行选择。

作用域优先级需要注意的是:

  1. 候选集中父类和子类中定义的函数,在函数重载时当成同一作用域优先级处理。

  2. 候选集中类型中定义的函数和扩展中定义的函数,在函数重载时当成同一作用域优先级处理。

  3. 候选集中定义在同一类型的不同扩展中的函数,在函数重载时当成同一作用域优先级处理。

  4. 候选集中定义在扩展中或类型中的操作符重载函数和内置操作符,在函数重载时同一作用域优先级处理。

  5. 除以上提到的类型作用域级别之外,其它的情况根据[作用域级别]章节中的规则,判断作用域级别;

/* According to the scope-level precedence principle, two functions in the candidate set, with different scope-levels, are preferred to the one with higher scope-level. */
func outer() {
    func g(a: B) {
        print("1")
    }
    func g(a: Int32) {
        print("3")
    }

    func inner() {
        func g(a: A) { print("2") }
        func innermost() {
            g(B())  // Output: 2
            g(1)   // Output: 3
        }
        g(B())   // Output: 2
        innermost()
    }

    inner()   
    g(B())    // Output: 1
}

上例中,函数innermost中调用函数g,且传入实参B(),根据作用域级优先原则,优先选作用域级别高的,因此,选第 7 行定义的函数 g。

/* The inherited names are at the same scope level as the names defined or declared in the class. */
open class Father {
    func f(x: Child) { print("in Father") }
}

class Child <: Father {
    func f(x: Father) { print("in Child") }
}

func display() {
    var obj: Child = Child()
    obj.f(Child()) 	// in Father
}

上例中,函数display调用函数f,且传入实参Child(),父类和子类中构成重载的函数均属于同一作用域级别,将根据最匹配规则选择Father类中定义的f(x: Child) {...}

下例中,类型 C 中定义的函数 fC 的扩展中定义的函数 f,在函数重载时当成同一作用域优先级处理。

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

class C {
    func f(a: Sub): Unit {}  // f1
}

extend C {
    func f(a: Base): Unit {}  // f2
    func g() {
        f(Sub()) // f1
    }
}

var obj = C()
var x = obj.f(Sub()) // f1

最匹配规则

候选集中最高优先级有多个函数:{f_1, ..., f_n},这些函数被称为匹配项,若其中存在唯一一个匹配项f_m,比其它所有匹配项都更匹配,则f_m为最匹配函数。

对于两个匹配项谁更匹配的比较是对两个匹配项形参的比较,分两步进行:

第一步,需要明确匹配项中用于比较的形参数量和顺序;

  • 如果函数参数有缺省值,未指定实参的可选形参不参与比较。

    open class Base {}
    class Sub <: Base {}
    
    func f(a!: Sub = Sub(), b!: Int32 = 2) {}    // This is f1,
    func f(a!: Base = Base()) {}    // This is f2.
    
    var x1 = f(a: Sub()) // parameters involved in comparison in f1 is only a
    
  • 如果函数有命名参数,实参顺序可能会和形参声明的顺序不一致,那么用于比较的形参顺序需要和实参顺序保持一致,确保两个候选函数用于比较的形参是对应的。

第二步,比较两个匹配项的形参确定哪个匹配项更匹配;

对于两个匹配项:f_if_j,如果满足以下条件,则称f_if_j更匹配:

对于一个函数调用表达式:e<T1, ..., T_p>(e_1, ...,e_m, a_m+1:e_m+1, a_k:e_k),实参个数为 k 个,则参与用于比较的形参个数为 k 个。

f_if_j参与比较的形参分别为(a_i1, ..., a_ik)(a_j1, ..., a_jk),对应的形参类型分别为 (A_i1, ..., A_ik)(A_j1, ..., A_jk)

若将f_i的形参(a_i1, ..., a_ik)作为实参传递给f_j能通过f_j的函数调用类型检查,且将f_j的形参(a_j1, ..., a_jk)作为参数传递给f_i不能通过函数调用类型检查,则称f_if_j更匹配。如下所示:

func f_i<X1,..., Xp>(a_i1: A_i1,..., a_ik: A_ik) {// f_i may not have type parameters
         f_j(a_i1, ..., a_ik)  // If this call expression can pass the type checking
}

func f_j<X1,..., Xq>(a_j1: A_j1,..., a_jk: A_jk) {// f_j may not have type parameters
         f_i(a_j1, ..., a_jk) // And this expression cannot pass the type checking
}

以下列举了一些函数重载决议的例子:

  interface I3 {}
  interface I1 <: I2 & I3 {}
  interface I2 <: I4 {}
  interface I4 <: I3 {}
  
  func f(x: I4) {}  // f1
  func f(x: I3) {}  // f2
  
  class C1 <: I1 {}
  var obj = C1()
  var result = f(obj)	// choose f1, because I4 <: I3
open class C1 {}
open class C2 <: C1 {}
class C3 <: C2 {}

func f(a: C1, b: C2, c: C1) {}	// f1
func f(a: C3, b: C3, c: C2) {}   // f2
func f(a: C3, b: C2, c: C1) {}   // f3

// function call
var x = f(C3(), C3(), C3())   // f2
open class A {}
class B <: A {}
func foo<X>(a: X, b: X): Int32 {}   	// foo1
func foo(a: A, b: B): Int32 {}      	// foo2
func foo<X>(a: A, b: X): Int32 {}   	// foo3

foo(A(), A())   // Error: cannot resolve.
foo(A(), 1)     // foo3. foo3 is the only function in candidate set.

确定实参类型

如果实参有多个类型:

确定最匹配函数后,如果实参的多个类型中有且只有一个类型T能通过该函数的类型检查,则确定实参的类型为T;否则,编译报错。

// Sub <: Base
func f(a: (Base)->Int64, b : Sub) {} //f1
func f(a: (Sub)->Int64, b: Base) {}  //f2
func g(a: Base): Int64 { 0 }   // g1
func g(a: Sub): Int64 { 0 }    // g2

func test() {
    f(g, Base())  // Error, both of g can pass f2's type check.

    f(g, Sub())  // OK,only g1 passes f1's type check.
}

操作符重载

仓颉编程语言中定义了一系列使用特殊符号表示的操作符(详见第 4 章表达式),例如,算术操作符(+-*/等),逻辑操作符(!&&||),函数调用操作符()等。默认情况下,这些操作符的操作数只能是特定的类型,例如,算术操作符的操作数只能是数值类型,逻辑操作符的操作数只能是Bool类型。如果希望扩展某些操作符支持的操作数类型,或者允许自定义类型也能使用这些操作符,可以使用操作符重载(operator overloading )来实现。

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

操作符函数定义的语法如下:

operatorFunctionDefinition
    	: functionModifierList? 'operator' 'func'
    	  overloadedOperators typeParameters?
          functionParameters (':' type)?
    	  (genericConstraints)? ('=' expression | block)? 
        ;

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

  • 定义操作符函数时需要在func关键字前面添加operator修饰符;
  • overloadedOperators是操作符函数名,它必须是一个操作符,且只有固定的操作符可以被重载。哪些操作符可重载将在[可以被重载的操作符]章节中详细介绍;
  • 操作符函数的参数个数和操作符要求的操作数个数必须相同;
  • 操作符函数只能定义在 class、interface、struct、enum 和 extend 中;
  • 操作符函数具有实例成员函数的语义,所以禁止使用 static 修饰符;
  • 操作符函数不能为泛型函数。

另外,需要注意:

  • 被重载后的操作符不改变它们固有的优先级和结合性(各操作符的优先级和结合律详见第 4 章表达式)。
  • 一元操作符是作为前缀操作符使用还是后缀操作符使用,与此操作符默认的用法保持一致。由于仓颉编程语言中不存在既可以作为前缀使用又可以作为后缀使用的操作符,所以不会产生歧义。
  • 操作符函数的调用方式遵循操作符固有的使用方式(根据操作数的数量和类型决定调用哪个操作符函数)。

如果要在一个类型 Type 之上重载某个操作符 opSymbol,需要在类型上定义名为opSymbol的操作符函数。在类型上定义操作符函数有两种方式:

  1. 使用 extend 的方式为其添加操作符函数,从而实现操作符在这些类型上的重载。对于无法直接包含函数定义的类型(是指除 structclassenuminterface 之外其他的类型),只能采用这种方式;

  2. 对于可以直接包含函数定义的类型 (包括 class、interface、enum 和 struct ),可以直接在其内部定义操作符函数的方式实现操作符的重载。

定义操作符函数

因为操作符函数实现的是特定类型之上的操作符,所以定义操作符函数与定义普通实例成员函数的差别在于对参数类型的约定:

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

    例如,假设opSymbol1是一元操作符,那么操作符函数opSymbol1定义为:

operator func opSymbol1(): returnType1 {
    functionBody1
}
  1. 对于二元操作符,操作符函数只有一个参数 right,对返回值的类型没有要求。

    例如,假设 opSymbol2 是二元操作符,那么操作符函数 opSymbol2 可以定义为:

operator func opSymbol2(right: anyType): returnType2 {
    functionBody2
}

同样地,对于定义了操作符函数的类型TypeName,就可以像使用普通的一元或二元操作符一样在 TypeName 类型的实例上使用这些操作符(保持各操作符固有的使用方式)。另外,因为操作符函数是实例函数,所以在TypeName 类型的实例 A 上使用重载操作符 opSymbol,其实是函数调用 A.opSymbol(arguments) 的语法糖(根据操作符的类型,参数的个数和类型,调用不同的操作符函数)。

下面举例说明如何在class类型中定义操作符函数,进而实现在class类型上重载特定的操作符。

假设我们希望在一个名为Point(包含两个Int32类型的成员变量xy)的class类型上实现一元负号(-)和二元加法(+)两个操作。其中,-实现对一个Point实例中两个成员变量xy取负值,然后返回一个新的Point对象,+实现对两个Point实例中两个成员变量xy分别求和,然后返回一个新的Point对象。

首先,定义名为Pointclass,并在其中分别定义函数名为-+的操作符函数,如下所示:

class Point {
    var x: Int32 = 0
    var y: Int32 = 0
    init (a: Int32, b: Int32) {
        x = a
        y = b
    }
    operator func -(): Point {
        return Point(-x, -y)
    }
    operator func +(right: Point): Point {
        return Point(x + right.x, y + right.y)
    }
}

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

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

操作符函数的作用域以及调用时的搜索策略

本节介绍操作符函数的作用域(名字和作用域请参考第 3 章)以及调用操作符函数时的搜索策略。

操作符函数的作用域

操作符函数和相同位置定义或声明的普通函数具备相同的作用域级别。

调用操作符函数时的搜索策略

这里介绍调用操作符函数(即使用操作符)时的搜索策略,因为一元操作符函数的搜索是二元操作符函数搜索的一个子情况,所以这里只介绍二元操作符(记为op)的搜索策略(一元操作符遵循一样的策略):

第一步,确定左操作数lOperand和右操作数rOperand的类型(假设分别为lTyperType);

第二步,在调用表达式lOperand op rOperand的当前作用域内,搜索和 lType 关联的所有名字为op,右操作数类型为rType的操作符函数。如果有且仅有一个这样的操作符函数,则将表达式调用转换成此操作符函数的调用;如果没有找到这样的函数,则继续执行第 3 步;

第三步,在更低优先级的作用域内重复第 2 步。如果在最低优先级的作用域内仍然没有找到匹配的操作符函数,则终止搜索,并产生一个编译错误(“函数未定义”错误)。

可以被重载的操作符

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

OperatorDescription
()Function call
[]Indexing
!NOT: unary
-Negative: unary
**Power: binary
*Multiply: binary
/Divide: binary
%Remainder: binary
+Add: binary
-Subtract: binary
<<Bitwise left shift: binary
>>Bitwise right shift: binary
<Less than: binary
<=Less than or equal: binary
>Greater than: binary
>=Greater than or equal: binary
==Equal: binary
!=Not equal: binary
&Bitwise AND: binary
^Bitwise XOR: binary
``

需要注意的是:

  1. 除了上表中列出的操作符,其他操作符(完整的操作符列表见 1.4 节)不支持被重载;

  2. 一旦在某个类型上重载了除关系操作符(<<=>>===!=)之外的其他二元操作符,并且操作符函数的返回类型与左操作数的类型一致或是其子类型,那么自然也就可以在此类型上使用对应的复合赋值符号。当操作符函数的返回类型与左操作数的类型不一致且不是其子类型时,在使用对应的复合赋值符号时将报类型不匹配错误;

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

  4. 函数调用操作符(())重载函数对输入参数和返回值类型没有要求。

  5. 对于类型 T, 如果 T 已经默认支持了上述若干可重载操作符,那么通过扩展的方式再次为其实现同签名的操作符函数时将报重定义错误。例如,为数值类型重载其已支持的同签名算术操作符、位操作符或关系操作符等操作符时,为 Rune 重载同签名的关系操作符时,为 Bool 类型重载同签名的逻辑操作符、判等或不等操作符时,等等这些情况,均会报重定义错。

存在以下特殊场景:

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

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

// Scenario for `this` or `super`.
open class A {
    init(x: Int64) {
        this() // error, missing argument for call with parameter list: (Int64)
    }
    operator func ()(): Unit {}
}

class B <: A {
    init() {
        super() // error, missing argument for call with parameter list: (Int64)
    }
}

// Scenario for enum constructor.
enum E {
    Y | X | X(Int64)
    operator func ()(a: Int64){a}
    operator func ()(a: Float64){a}
}

main() {
    let e = X(1) // ok, X(1) is to call the constructor X(Int64).
    X(1.0)   // ok, X(1.0) is to call the operator () overloading function.
    let e1 = X
    e1(1) // ok, e1(1) is to call the operator () overloading function.
    Y(1) // oK, Y(1) is to call the operator () overloading function.
}

索引操作符重载

索引操作符([])分为取值 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
}

特别的,数值类型、Bool、Unit、Nothing、Rune、String、Range、Function、Tuple 类型不支持重载索引操作符赋值形式。