加载中...
仓颉文档阅读-语言规约V: 函数(I)

仓颉文档阅读-语言规约V: 函数(I)

周一 9月 29 2025
8642 字 · 39 分钟

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

函数

函数是一段完成特定任务的独立代码片段, 可以通过函数名字来标识, 这个名字可以被用来调用函数

仓颉编程语言中函数是一等公民, 函数可以赋值给变量, 或作为参数传递, 或作为返回值返回

函数嘛, C/C++中很熟悉了

但是现代语言中应该新增了很多特性, 逐步了解

函数定义

仓颉编程语言中, 函数的定义遵循以下语法规则:

PLAINTEXT
functionDefinition
    : functionModifierList? 'func'
    identifier 
    typeParameters?
    functionParameters
    (':' type)?
    (genericConstraints)?
    block
    ;

可以总结为如下:

  • 通过关键字func来定义一个函数

  • func前可以用函数修饰符进行修饰

  • func后需要带函数名

  • 可选的类型形参列表, 类型形参列表由<>括起, 多个类型形参之间用,分隔

  • 函数的参数由()括起, 多个参数用,分隔, 并且需要给定每个参数的参数类型

  • 可缺省的函数返回类型, 由:type表示

  • 函数定义时必须有函数体, 函数体是一个块(不包含函数形参)

以下示例是一个完整的函数定义具备的所有要素, 它没有使用访问修饰符 public 修饰表示其在包内部可访问, 函数名为 foo, 有一个 Int64 类型的参数a, 有返回类型Int64, 有函数体

CANGJIE
func foo(a: Int64): Int64 { a }

函数名不能被进行赋值, 即函数名不能以表达式左值的形式在程序中出现

如以下示例中的操作是禁止的

CANGJIE
func f(a: Int64): Int64 { a }
// f = {a: Int64 => a + 1}        // compile-time error

仓颉中, 函数的定义直观表示是这样的:

CANGJIE
修饰符 func 函数名<类型参数列表(可选)>(函数参数列表): 返回值类型 {
    函数体
}
// 类型参数列表格式为: <T1, T2, T3>
// 函数参数列表可为空, 不为空格式为: (param1: Type, param2: Type, ...)

函数修饰符

全局函数的修饰符

全局函数可以被所有访问修饰符修饰, 默认的可访问性为 internal

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

全局函数是属于包的, 相关的修饰, 应该在包管理相关内容中有介绍

局部函数的修饰符

局部函数无可用修饰符

仓颉允许在函数内定义函数, 不过不能用修饰符

成员函数的修饰符

类成员函数可用修饰符有: public, protected, private, internal, static, open, override, redef详见[类的成员]以及包和模块管理章节[访问修饰符]

接口成员函数可用修饰符有: static, mut, 详见[接口成员]

struct 成员函数可用修饰符有: mut, public, private, internal, static, 详见[struct 类型]

enum 成员函数可用修饰符有: public, private, internal, static, 详见[enum 类型]

如果不提供可访问修饰符, 接口成员函数以外的成员函数可以在当前包及子包内被访问, 接口成员函数默认是 public 语义

structenum类型在语言规约之前的文档中有提及, 但是并无修饰符的具体介绍, 所以应该还是在类相关内容中

参数

函数定义时参数列表中参数的顺序: 非命名参数, 命名参数(包括: 不带默认值命名参数和带默认值的参数)

参数列表的语法定义如下:

PLAINTEXT
functionParameters
    : ('(' (unnamedParameterList (',' namedParameterList)? )? ')')
    | ('(' (namedParameterList)? ')')
    ;

nondefaultParameterList
    : unnamedParameter (',' unnamedParameter)* (',' namedParameter)*
    | namedParameter (',' namedParameter)*
    ;

namedParameterList
    : (namedParameter | defaultParameter) (',' (namedParameter | defaultParameter))*
    ;

namedParameter
    : identifier '!' ':' type 
    ;

defaultParameter
    : identifier '!' ':' type '=' expression
    ;

unnamedParameterList
    : unnamedParameter (',' unnamedParameter)*
    ;

unnamedParameter
    : (identifier | '_') ':' type
    ;

仓颉中, 函数的参数类型有两种: 非命名参数 和 命名参数

命名参数可以存在默认值, 对应 C++函数参数中的缺省值

对于非命名参数, 可以使用一个 _ 代替一个函数体中不会使用到的参数

CANGJIE
func bar(a: Int64 , b!: Float64 = 1.0, s!: String) {}                    // OK
func bar2(a: Int64 = 1, b!: Float64 = 1.0, s: String = "Hello") {}    // Error
func foo(a: Int64, b: Float64)(s: String = "Hello") {}                // Error

func f1(a: Int64, _: Int64): Int64 {
    return a + 1
}

func f2(_: String): Unit {
    print("Hello Cangjie")
}

函数参数均为不可变变量, 即均有 let 修饰, 在函数定义内不能对其进行赋值

CANGJIE
func foo(a: Int64, b: Float64) {
    a = 1       // Error: 参数'a'是不可变的, 不能被赋值
    b = 1.0     // Error: 参数'b'是不可变的, 不能被赋值
}

函数的参数类型不受下述”是否是命名形参”、“是否有默认值”的影响;

且讨论参数类型时, 一个类型与它的 alias 被视为相同的类型

函数定义例子中有两个错误示例:

  1. 非命名参数不允许设置初始值
  2. 命名参数之后, 不允许再出现非命名参数

仓颉中函数的参数均为不可变类型, 是不允许赋值的

命名形参

函数定义时通过 在形参名后添加 ! 来定义命名形参

PLAINTEXT
namedParameter
    : identifier '!' ':' type
    ;
  • 函数定义时, 命名形参后不允许有非命名形参

    CANGJIE
    func add1(a: Int32, b!: Int32): Int32 { a + b } // ok
    
    func add2(a!: Int32, b: Int32): Int32 { a + b } // error
  • 当形参被定义成命名形参后, 调用这个函数时, 则必须在实参值前使用参数名:前缀来指定这个实参对应的形参, 否则编译报错

    CANGJIE
    func add(a: Int32, b!: Int32): Int32 { a + b }
    
    add(1, b: 2) // ok
    add(1, 2)    // error
  • 如果抽象函数或 open 修饰的函数有命名形参, 那么实现函数或 override 修饰的函数也需要保持同样的命名形参

    CANGJIE
    open class A {
        public open func f(a!: Int32): Int32 {
            return a + 1
        }
    }
    class B <: A {
        public override func f(a!: Int32): Int32 { // ok
            return a + 1
        }
    }
    class C <: A {
        public override func f(b!: Int32): Int32 { // error
            return b + 1
        }
    }

仓颉中, 如果函数存在命名参数, 则必须通过参数名: 值来进行传参

参数的默认值

函数的参数可以有默认值, 通过=来为参数定义默认值

当默认值被定义后, 调用这个函数可以忽略这个参数, 函数体会使用默认值

为了便于理解, 有默认值的参数称为可选参数

函数定义时, 可选参数是一种命名形参, 可选参数名后必须添加!, 否则编译报错;

PLAINTEXT
defaultParameter
    : identifier '!' ':' type '=' expression
    ;  

如下示例, 我们定义了一个add函数, 它的参数类型列表(Int32, Int32)

函数定义时, b具有默认值1

因此, 当我们调用add函数, 并且只传递一个值3时, 3会被赋值给a, 从而返回结果4

如果传入两个值32, 那么b的值为2

CANGJIE
func add(a: Int32, b!: Int32 = 1): Int32 { a + b }

add(3)            // invoke add(3, 1), return 4
add(3, b: 2)    // return 5

了解过C++的话, 仓颉中存在默认值参数的使用, 也没有太大区别

  • 类或接口中被 open 关键字修饰的函数不允许有可选参数

  • 操作符函数不允许有可选参数

  • 匿名函数(lambda 表达式)不允许有可选参数

  • 函数参数默认值中引入的名字从静态作用域中获得, 即引入的名字为函数定义时可访问到的名字

  • 函数参数默认值中引入的名字在函数定义时可访问即可, 无需和函数本身的可访问性一致

  • 函数参数和其默认值不属于该函数的函数体

  • 参数默认值在函数调用时求值, 而非在函数定义时求值

  • 规定, 函数调用时参数求值顺序是按照定义时顺序从左到右, 函数参数的默认值可以引用定义在该参数之前的形参

  • 函数调用时, 通过函数名调用可以使用默认值, 通过变量名调用不支持使用默认值

    CANGJIE
    // 编译时错误
    // func f(a: Int32, b!: Int32 = 2, c: Int32): Int32 { ... }
                                                                                
    // OK.
    func f1(a: Int32, b: Int32, c!: Int32 = 3, d!: Int32 = 4): Int32 { 
        a + b + c + d
    }
                                                                                
    func test() {
        f1(1, 2)            // 10,  f1(1, 2, 3, 4)
        f1(1, 2, c: 5)        // 12,  f1(1, 2, 5, 4)
    }
    CANGJIE
    /* 在默认值中引入的名称, 不需要具有与函数相同或更严格的可访问性 */
    var x = 10
    var y = 10
    func g() {}
    public func f2(a!: Int64 = x * 2 + y, b!: ()->Unit = g) {}  // OK.
                                                                                
    class AAA {
        static private var x = 10
                                                                                
        func f(a!: Int64 = x) { // OK, public 方法可以使用私有静态字段
            print("${a}")
            x = x + 10
            print("${x}")
        }
    }
    CANGJIE
    /* 
    调用函数时, 函数声明中的名称可以使用默认值
    使用变量名调用函数时, 参数不能是可选的, 即不支持使用默认值
    */
    func f1(): (Int64) -> Unit { g1 }
                                                                                
    func g1(a!: Int64 = 42) {
        print("g1: ${a}")
    }
                                                                                
    let gg1 = f1()   
    let x = gg1()    // Error, 不能省略参数
    let gg3 = g1     
    let a = gg3()    // Error, 不能省略参数

文档中有两句比较不明确的介绍:

  • 函数参数默认值中引入的名字从静态作用域中获得, 即引入的名字为函数定义时可访问到的名字
  • 函数参数默认值中引入的名字在函数定义时可访问即可, 无需和函数本身的可访问性一致

这两句话的意思其实是:

函数参数的默认值表达式中所引用的名字(变量、函数等), 按照静态作用域规则函数定义处进行解析, 即 解析的是函数定义时可以看到的名字, 而不是调用时可能可以看到的名字

只要在该位置这些名字是可访问的(无论其访问权限是 private 还是 public), 就可以使用, 不要求默认值中引用的名字具有 与函数定义本身的访问级别(如 public) 相同或更宽松的访问权限

在例子中就是, public成员函数可以引用private成员变量作为参数的默认值

由于函数的形参和其默认值不属于该函数的函数体

所以下面例子中的 return 表达式缺少包围它的函数体

它既不属于外层函数 f(因为内层函数定义 g 已经开始), 也不在内层函数 f 的函数体中:

CANGJIE
func f() {
    func g(x! :Int64 = return) { // Error: return 必须在函数体内使用
        0
    }
    1
}

这个意思是, 仓颉中函数体 就只是{}的内容, 参数列表什么的都是不算的

函数体

函数体由一个 block 组成

局部变量

在仓颉编程语言中, 允许在函数体内定义变量, 将其称为局部变量

变量可以用var修饰为可变变量或let修饰为不可变变量

CANGJIE
func foo(): Unit {
    var a = 1
    let b = 1
}

熟悉C/C++, 对局部变量再熟悉不过了

嵌套函数

在仓颉编程语言中, 允许在函数体内定义函数, 将其称为嵌套函数

嵌套函数中可以捕获外部函数中定义的变量或其他嵌套函数

嵌套函数支持递归

CANGJIE
func foo(): Unit {
    func add(a: Int32, b: Int32) {
        a + b
    }

    let c = 1

    func nest(): Unit {
        print("${c}")        // 1
        var b = add(1, 2)    // b = 3
    }
}

仓颉中, 嵌套函数就是在函数体内定义函数

嵌套函数 可以捕获外部函数定义的变量或其他嵌套函数, 其实与顶层作用域定义函数相似

函数的返回类型

函数的返回类型有以下情况:

  • 任何类型

  • 返回值为元组的函数: 可以使用元组类型作为函数的返回类型, 将多个值作为一个复合返回值返回

    如以下例子, 它的返回是一个元组(a, b), 返回类型是(Int32, Int32)

    CANGJIE
    func returnAB(a: Int32, b: Int32): (Int32, Int32) { 
        (a, b) 
    }
  • 函数类型作为返回类型: 可以使用函数类型作为另一个函数的返回类型

    如下示例, 在:后紧跟的是add函数的类型(Int32, Int32) -> Int32

    CANGJIE
    func returnAdd(a: Int32, b: Int32): (Int32, Int32) -> Int32 {
        return {a, b => a + b}    // 返回一个 lambda 表达式
    }

函数作为仓颉语言中的一等公民, 是可以作为返回值的, 返回值类型也就有函数类型

函数返回元组, 实现多个返回值返回

如果指定函数的返回类型, 则在函数定义的参数列表后使用 :Type 指定

此时要求函数体的类型、函数体中所有 return e 表达式中 e 的类型是标注的类型(Type)的子类型, 否则编译报错

CANGJIE
class A {}
class B <: A {}
// Return is not written.
func add(a: Int32, b: Int32): B {
    var c = a + b
    if (c > 100) {
        return B()
    }
    else {
        return A() // 编译错误, 因为 A 不是 B 的子类型
    }
}

特别地, 指定返回类型为 Unit 时(如 func f(x:Bool):Unit { x }), 则编译器会在函数体中所有可能返回的地方自动插入表达式 return (), 使得函数的返回类型总是为 Unit

示例如下:

CANGJIE
func Column(c: (Data) -> Unit): Unit {
    2
}     // return () 会被自动插入

func Column(c: (Data) -> Unit) {
    2
}    // 返回值类型为 Int64

仓颉的函数还比较有意思, 如果函数返回值类型是Unit, 编译器会在函数体有可能返回的地方 自动添加return ()

所以此时, 可以不用显式书写return ()

如果不指定函数的返回类型 , 则编译器会根据函数体的类型以及函数体中的所有 return 表达式来共同推导出函数的返回类型

此过程不是完备的, 如遇到(互)递归函数而无法推导它们的返回类型时, 编译报错

(注意: 不能为没有函数体的函数推导其返回类型)

函数的返回类型推导规则如下:

函数体是表达式和声明的序列, 我们将序列的最后一项的类型记为 T0(若块的最后一项是表达式, 则为表达式的类型; 若最后一项为声明, 则 T0 = Unit), 再将函数体中所有 return e (包括所有子表达式中的 return e)表达式中 e 的类型记为 T1 ... Tn, 则函数的返回类型是 T0, T1, ..., Tn的最小公共父类型

如果不存在最小公共父类型, 则产生一个编译错误

示例如下:

PLAINTEXT
open class Base {}
class Child <: Base {}

func f(a: Rune) {
    if (false) {
        return Base()
    }
    return Child()
}
  • 函数体的类型是块的最后一项的类型, 即 return Child() 的类型, 其类型为 Nothing
  • 第一个 return e 表达式 return Base()e 的类型是 Base
  • 第二个 return e 表达式 return Child()e 的类型为 Child
  • 由于 Nothing, Base, Child 三者的最小公共父类型是 Base, 所以该函数的返回类型为 Base

注: 函数的返回值具有 let 修饰的语义

仓颉中, 如果函数定义未指定返回值类型, 那么编译器将自动推导返回类型

推导过程为:

  1. 记录块中 最后一个表达式 或 声明的类型: T0
  2. 记录所有 return e 表达式中, e的类型为T1, T2, T3, ...
  3. T0, T1, T2, T3, ...的最小公共父类型, 作为函数的返回类型

仓颉中, 函数的返回值不能修改, 具有let修饰的语义

函数声明

仓颉编程语言中, 函数声明和函数定义的区别是, 前者没有函数体

函数声明的语法如下:

PLAINTEXT
functionDeclaration
    : functionModifierList? 'func'
    identifier 
    typeParameters?
    functionParameters
    (':' type)?
    genericConstraints?
    ;

函数声明可以出现在抽象类, 接口中

仓颉中的函数声明与C/C++中的函数声明有些不同

C/C++中的函数声明, 只是声明一下函数的符号, 此函数是否已经被实现是不确定的

但仓颉中的函数声明, 好像也只是证明一下函数的符号, 且只能出现在抽象类或接口中, 这意味着 仓颉中的函数声明, 表示这个函数一定没有被实现

所以在之后的继承或接口实现中, 必须要实现声明的函数

仓颉中函数的声明 与 函数定义的区别, 就是没有函数体

函数的重定义

对于非泛型函数, 在同一个作用域中, 参数类型完全相同的同名函数被视为重定义, 将产生一个编译错误

以下几种情况要格外注意:

  • 同名函数即使返回类型不同也构成重定义

  • 同名的泛型与非泛型函数永不构成重定义

  • 在继承时, 对于子类中的一个与父类同名且参数类型完全相同的函数, 若其返回类型是父类中的版本的子类型, 则构成覆盖, 也不构成重定义

    这是因为子类与父类作用域不同

对于两个泛型函数, 如果重命名一个函数的泛型形参后, 其非泛型部分与另一个函数的非泛型部分构成重定义, 则这两个泛型函数构成重定义

举例如下:

  1. 下面这两个泛型函数构成重定义, 因为存在一种 [T1 |-> T2] 的重命名(作用到第一个函数上), 使得两个函数的非泛型部分构成重定义

    CANGJIE
    func f<T1>(a: T1) {}
    func f<T2>(b: T2) {}
  2. 下面这两个泛型函数不构成重定义, 因为找不到上述的一种重命名

    CANGJIE
    func f<X, Y>(a:X, b:Y) {}
    func f<Y, X>(a:X, b:Y) {}
  3. 下面的这两个泛型函数构成重定义, 因为存在一种 [X |-> Y, Y |-> X] 的重命名使得两个函数的非泛型部分构成重定义

    CANGJIE
    func f<X, Y>(a:X, b:Y) {}
    func f<Y, X>(a:Y, b:X) {}

[T1 |-> T2]是泛型类型参数重命名映射的一种表示, 意思是 将T1映射为T2

文档中的例子, func f<T1>(a: T1) {}func f<T2>(b: T2) {}构成重定义, 因为当[T1 |-> T2]时, 两个泛型函数类型完全一致

但为什么第二个例子不存在这样的情况呢?

对于func f<X, Y>(a:X, b:Y) {}func f<Y, X>(a:X, b:Y) {}, 如果 [X |-> Y], 得到func f<Y, Y>(a:Y, b:Y) {}func f<Y, Y>(a:Y, b:Y) {}, 两个泛型函数类型不也是完全一致吗?

是完全一致, 但是这是非法的泛型定义, 因为多个泛型类型参数名字一样, 所以多类型参数的泛型, 不存在单独类型参数的映射, 因为泛型的类型参数名字不能存在重复

函数类型

函数类型由函数的参数类型和返回类型组成, 其中参数类型与返回类型之间用->分隔

PLAINTEXT
functionType:
    : '(' (type (, type)*)? ')' '->' type

以下是一些示例:

  • 示例 1: 没有参数、返回类型为Unit

    CANGJIE
    func hello(): Unit { print("Hello!") }
    
    // function type: () -> Unit
  • 示例 2: 参数类型为Int32, 返回类型为Unit

    CANGJIE
    func display(a: Int32): Unit { print("${a}") }
    
    // function type: (Int32) -> Unit
  • 示例 3: 两个参数类型均为Int32, 返回类型为Int32

    CANGJIE
    func add(a: Int32, b: Int32): Int32 { a + b }
    
    // function type: (Int32, Int32) -> Int32
  • 示例 4: 参数类型为(Int32, Int32) -> Int32, Int32Int32, 返回类型为Unit

    CANGJIE
    func printAdd(add: (Int32, Int32) -> Int32, a: Int32, b: Int32): Unit {
        print("${add(a, b)}")
    }
    // function type: ((Int32, Int32) -> Int32, Int32, Int32) -> Unit
  • 示例 5: 两个参数类型均为Int32, 返回类型为函数类型(Int32, Int32) -> Int32

    CANGJIE
    func returnAdd(a: Int32, b: Int32): (Int32, Int32) -> Int32 {
      {a, b => a + b}
    }
    
    // function type: (Int32, Int32) -> (Int32, Int32) -> Int32
  • 示例 6: 两个参数类型均为Int32, 返回为一个元组类型为: (Int32, Int32)

    CANGJIE
    func returnAB(a: Int32, b: Int32): (Int32, Int32) { (a, b) }
                                                                        
    // function type: (Int32, Int32) -> (Int32, Int32)

仓颉中的每个函数都拥有自己的类型: (参数类型列表) -> 返回值类型

函数调用

有关函数调用表达式的语法, 请参考函数调用表达式

命名实参

命名实参是指在函数调用时, 在实参值前使用 形参名 : 前缀来指定这个实参对应的形参

只有在函数定义时使用!定义的命名形参, 才可以在调用时使用命名实参的语法

函数调用时, 所有的命名形参均必须使用命名实参来传参, 否则报错

在函数调用表达式中, 命名实参后不允许出现非命名实参

使用命名实参指定实参值时, 可以不需要和形参列表的顺序保持一致

CANGJIE
func add(a!: Int32, b!: Int32): Int32 {
    a + b
}

var sum1 = add(1, 2)        // error
var sum2 = add(a: 1, b: 2)  // OK, 3
var sum3 = add(b: 2, a: 1)  // OK, 3

因为函数定义时, 命名形参之后不允许出现非命名形参, 所以后面所有的命名形参都可以不按照参数列表顺序进行传参

函数调用类型检查

本节介绍的是, 给定一个调用表达式, 对调用表达式所需要进行的类型检查

如果被调用的函数涉及重载, 则需要根据函数重载的规则进行类型检查和重载决议

  1. 如果函数调用表达式中指定了类型参数, 只有类型实参的个数与类型形参的个数相同才可能通过类型检查, 即假设函数调用表达式为: f<T1, ..., Tm>(A1, ..., An), 其中给定了 m 个类型实参, 则函数类型形参数量须为 m

    CANGJIE
    open class Base {}
    class Sub <: Base {}
    
    func f<X, Y>(a: X, b: Y) {}    // f1
    func f<X>(a: Base, b: X) {}    // f2
    
    f<Base>(Base(), Sub())        // f2 may pass the type checking
  2. 根据调用表达式中的实参 和 调用表达式所在的类型检查上下文中 指定的返回类型 R 对函数进行类型检查

    假设函数定义为:

    fi<Ti1,...,Tip>(Ai1,...,Aik):Ri where Ci1,...,Ciqif_i<T_{i1}, ..., T_{ip}>(A_{i1}, ..., A_{ik}):R_i\ where\ C_{i1}, ..., C_{iq_i}
    1. **如果调用表达式带了类型实参: fi<T1, ..., Tp>(A1, ..., Ak), 那么对于函数fi的类型检查规则如下: **

      1. 类型实参约束检查: 类型实参 <T1, ..., Tp> 需要满足函数 fi 的类型约束

        σ=[T1Ti1,...,TpTip]Δσ  solves  Ci1,...,Ciqiσ=[T_1↦T_{i1},...,T_p↦T_{ip}]\\ Δ⊢σ\ \ solves\ \ C_{i1},...,C{iq_i}
      2. 参数类型检查: 将类型实参代入函数 fi 的形参后, 满足实参类型 (A1, ..., Ak) 是类型实参代入形参后类型的子类型

        σ=[T1Ti1,...,TpTip]Δ(A1,...,Ak)<:σ(Ai1,...,Aik)σ=[T_1↦T_{i1},...,T_p↦T_{ip}]\\ Δ⊢(A_1,...,A_k)<:σ(A_{i1},...,A_{ik})
      3. 返回类型检查: 如果调用表达式的上下文对其有明确类型要求R, 则需要根据返回类型进行类型检查, 将类型实参代入函数 fi 的返回类型 Ri 后, 满足类型实参代入后的返回类型是 R 的子类型

        σ=[T1Ti1,...,TpTip]ΔσRi<:Rσ=[T_1↦T_{i1},...,T_p↦T_{ip}]\\ Δ⊢σR_i<:R
    2. **如果调用表达式不带类型实参: f(A1, ..., An), 那么对于函数fi的类型检查规则如下: **

      1. 如果**fi是非泛型函数**, 则按如下规则进行类型检查:

        1. 参数类型检查: 实参类型 (A1, ..., Ak) 是形参类型的子类型

          Δ(A1,...,Ak)<:(Ai1,...,Aik)Δ⊢(A_1,...,A_k)<:(A_{i1},...,A_{ik})
        2. 返回类型检查: 如果调用表达式的上下文对其有明确类型要求 R, 则检查函数 fi 的返回类型 RiR 的子类型

          ΔRi<:RΔ⊢R_i<:R
          CANGJIE
          open class Base {}
          class Sub <: Base {}
          func f(a: Sub) {1}        // f1
          func f(a: Base) {Base()}    // f2
          let x: Base = f(Sub())    // f2 can pass the type checking
      2. 如果 fi是泛型函数, 则按如下规则进行类型检查:

        1. 参数类型检查: 存在代换使得实参类型(A1, ..., Ak)是形参类型代换后的类型的子类型

          σ=[T1Ti1,...,TpTip]Δ(A1,...,Ak)<:σ(Ai1,...,Aik)σ=[T_1↦T_{i1},...,T_p↦T_{ip}]\\ Δ⊢(A_1,...,A_k)<:σ(A_{i1},...,A_{ik})
        2. 返回类型检查: 如果调用表达式的上下文对其有明确类型要求R, 则需要根据返回类型进行类型检查, 将 a) 中的代换代入函数fi的返回类型Ri后, 满足代换后的返回类型是R的子类型

          σ=[T1Ti1,...,TpTip]ΔσRi<:Rσ=[T_1↦T_{i1},...,T_p↦T_{ip}]\\ Δ⊢σR_i<:R

需要注意的是:

  1. 如果函数有缺省值, 在类型检查时会补齐缺省值之后再进行类型检查;
  2. 如果函数有命名参数, 命名参数的顺序可能会和形参顺序不一致, 在类型检查时, 命名实参要与名字匹配的命名形参对应

总结几点就是:

  1. 泛型函数, 传入的类型实参数量 需要与 目标泛型函数的类型形参数量 保持一致

  2. 泛型函数, 如果显式传入类型实参, 则会:

    1. 进行类型约束的检查, 即 判断是否满足 函数定义时 对实参类型指定的约束条件(where 条件)
    2. 类型实参传入之后, 函数调用实参的类型要满足 是传入类型实参的子类型
    3. 根据传入类型实参, 检查最终的返回值类型 是否满足 函数定义的返回类型的子类型
  3. 如果不显式传入类型实参, 则有可能是非泛型函数

    1. 如果是非泛型函数, 则会:

      1. 检查函数调用实参类型 是否满足 函数定义的形参类型的子类型
      2. 检查返回值类型 是否满足 函数定义的返回类型的子类型
    2. 如果是泛型函数, 则会:

      1. 检查调用参数类型

        根据函数调用传入的实参类型, 推导类型代换σ(上面的类型映射数学公式), 使得实参类型满足代换后的形参类型

        传入的实参的类型, 属于函数形参类型的子类型

      2. 检查返回类型

        还是根据推导和代换的形参类型, 去推导最后的返回值类型 是否满足 函数定义的返回类型的子类型

文本上比较绕, 但其实核心很简单: 根据实参类型推导、代换形参类型, 再根据形参类型 进行实际的参数检查、返回值类型检查等操作

尾随Lambda

当函数最后一个形参是函数类型, 并且函数调用对应的实参是 lambda 时, 我们可以使用尾随 lambda 语法, 将 lambda 放在函数调用的尾部, 括号外面

CANGJIE
func f(a: Int64, fn: (Int64)->Int64) {
    fn(a)
}
f(1, { i => i * i })       // 普通函数调用
f(1) { i => i * i }        // 尾随 lambda

func g(a!: Int64, fn!: (Int64)->Int64) {
    fn(a)
}
g(a: 1, fn: { i => i * i })   // 普通函数调用
g(a: 1) { i => i * i }        // 尾随 lambda

当函数调用有且只有一个 lambda 实参时, 我们还可以省略 (), 只写 lambda

CANGJIE
func f(fn: (Int64)->Int64) {
    fn(1)
}

f{ i => i * i }

如果尾随 lambda不包含形参, => 可以省略

CANGJIE
func f(fn: ()->Int64) {
    fn()
}

f{ i * i }

需要注意的是, 尾随 lambda 语法只能用在具有 函数名/变量名 的函数调用上, 并且尾随 lambdalambda 表达式只会解释为 函数名/变量名 对应的函数的参数

这意味着以下两种调用例子是不合法的:

CANGJIE
func f(): (()->Unit)->Unit {
    {a => }
}

f() {}                                // Error, lambda 表达式不是 f 的参数

func g(a: ()->Unit) {}

if (true) { g } else { g } () {}      // Error, 非法尾随 lambda 语法

必须先将以上的表达式赋值给变量, 使用变量名调用时才可以使用尾随 lambda 语法

如下面的代码所示:

CANGJIE
let f2 = f()
f2 {}          // ok

let g2 = if (true) { g } else { g }
g2() {}        // ok

普通函数调用和构造函数调用都可以使用这个语法, 包含 this()super()

CANGJIE
this(1, { i => i * i } )
this(1) { i => i * i }

super(1, { i => i * i } )
super(1) { i => i * i }

仓颉中, 如果函数的最后一个形参类型为函数类型, 就可以使用尾随Lambda语法

即, 实参可以正常传入lambda表达式, 也可以正常函数调用之后(不传入lambda), 跟上一个{lambda}来实现尾随lambda

CANGJIE
func f(param1: Int64, param2: Int64, lam: (Int64, Int64)->Int64) {
    lam(param1, param2)
}

f(1, 2, { p1, p2 => p1 * p2 })    // 普通函数调用
f(1, 2) { p1, p2 => p1 * p2}      // 尾随 lambda

这是一个在C/C++中从未见过的lambda表达式传参方法

变长参数

变长参数是一种特殊的函数调用语法糖, 当形参最后一个非命名参数是 Array 类型时, 实参中对应位置可以直接传入参数序列代替 Array 字面量

  1. 变长参数没有特殊的声明语法, 只要求函数声明处最后一个非命名参数是 Array 类型

  2. 变长参数在函数调用时可以使用普通参数列表的形式逐个传入 Array 的多个元素

  3. 非命名参数中, 只有最后一个位置的参数可以使用变长参数

    命名参数不能使用这个语法糖

  4. 变长参数对全局函数、静态成员函数、实例成员函数、局部函数、构造函数、函数变量、lambda、函数调用操作符重载、索引操作符重载的调用都适用, 不支持其它操作符重载、composepipeline 这几种调用方式

  5. 变长参数的个数可以是 0 个或以上

  6. 变长参数只有在函数重载所有情况都不匹配的情况下, 才判断是否可以应用语法糖, 优先级最低

CANGJIE
func f1(arr: Array<Int64>) {}
func f2(a: Int64, arr: Array<Int64>) {}
func f3(arr: Array<Int64>, a: Int64) {}
func f4(arr1!: Array<Int64>, a!: Int64, arr2!: Array<Int64>) {}

func g() {
    let li = [1, 2, 3]
    f1(li)
    f1(1, 2, 3)                           // 使用变长参数
    f1()                                  // 使用变长参数
    f2(4, li)
    f2(4, 1, 2, 3)                        // 使用变长参数

    f3(1, 2, 3)                           // Error, Array 不是最后一个参数
    f4(arr1: 1,2,3, a: 2, arr2: 1,2,3)    // Error, 命名参数不能使用变长参数
}

仓颉中, 只要函数定义的最后一个参数是Array类型的, 那么就可以当作变长参数使用

当然也可以直接传入Array类型的数据

函数重载决议总是会优先考虑不使用变长参数就能匹配的函数, 只有在所有函数都不能匹配, 才尝试使用变长参数解析

当编译器无法决议时会报错

CANGJIE
open class A {
    func f(v: Int64): Unit {          // f1
    }
}

class B <: A {
    func f(v: Array<Int64>): Unit {   // f2
    }
}

func p1() {
    let x = B()
    x.f(1)                            // call the f1
}

func g<T>(arg: T): Unit {             // g1
}

func g(arg: Array<Int64>): Unit {     // g2
}

func p2() {
    g(1)                              // call the g1
}

func h(arg: Any): Unit {              // h1
}
func h(arg: Array<Int64>): Unit {     // h2
}

func p3() {
    h(1)                              // call the h1
}

如果使用变长参数的函数存在重载, 那么编译器会优先决议使用不使用变长参数的版本

函数作用域

在仓颉编程语言中, 函数可以在源程序顶层定义或者在函数内部定义:

  • 全局函数

    源程序顶层定义函数称为全局函数, 它的作用域是全局的

    如下示例, 函数 globalFunction 是全局函数

    它的作用域是全局作用域

    CANGJIE
    func globalFunction() {}
  • 嵌套函数

    函数体内部定义的函数成为嵌套函数, 它的作用域是局部的, 详见[作用域]

    如下示例, 函数 nestedFunction 是嵌套函数, 它的作用域是从定义之后开始, 到 globalFunction 函数体结束

    CANGJIE
    func globalFunction() {
        func nestedFunction() {}
    }
  • 成员函数

    类型定义中可以声明或定义成员函数

    成员函数的作用域是整个类型及其扩展

    CANGJIE
    interface Myinterface {
        func foo(): Unit
        static func bar(): Unit
    }
    
    class MyClass {
        func foo() {}
        static func bar() {}
    }
  • 扩展成员函数

    扩展中可以声明额外的成员函数

    它的作用域是被扩展类型的所有扩展, 同时受访问修饰符限制

    CANGJIE
    extend MyType {
        func foo(): Unit {}
    }

如果你了解过任意一门有面向对象思想的编程语言, 这一部分应该没有什么特别的问题


Thanks for reading!

仓颉文档阅读-语言规约V: 函数(I)

周一 9月 29 2025
8642 字 · 39 分钟