阅读文档版本:
具体开发指南 Cangjie-LTS-1.0.3↗
在阅读 了解仓颉的语言规约时, 难免会涉及到一些仓颉的示例代码, 但 我们对仓颉并不熟悉, 所以可以用仓颉在线体验↗快速验证
有条件当然可以直接配置Canjie-SDK↗
博主在此之前, 基本就只接触过C/C++语言, 对大多现代语言都没有了解, 所以在阅读过程中遇到相似的概念, 难免会与C/C++中的相似概念作类比, 见谅
此样式内容, 表示文档原文内容
函数
函数是一段完成特定任务的独立代码片段, 可以通过函数名字来标识, 这个名字可以被用来调用函数
仓颉编程语言中函数是一等公民, 函数可以赋值给变量, 或作为参数传递, 或作为返回值返回
函数嘛, C/C++中很熟悉了
但是现代语言中应该新增了很多特性, 逐步了解
函数定义
仓颉编程语言中, 函数的定义遵循以下语法规则:
functionDefinition : functionModifierList? 'func' identifier typeParameters? functionParameters (':' type)? (genericConstraints)? block ;
可以总结为如下:
通过关键字
func
来定义一个函数
func
前可以用函数修饰符进行修饰
func
后需要带函数名可选的类型形参列表, 类型形参列表由
<>
括起, 多个类型形参之间用,
分隔函数的参数由
()
括起, 多个参数用,
分隔, 并且需要给定每个参数的参数类型可缺省的函数返回类型, 由
:type
表示函数定义时必须有函数体, 函数体是一个块(不包含函数形参)
以下示例是一个完整的函数定义具备的所有要素, 它没有使用访问修饰符
public
修饰表示其在包内部可访问, 函数名为foo
, 有一个Int64
类型的参数a
, 有返回类型Int64
, 有函数体
func foo(a: Int64): Int64 { a }
函数名不能被进行赋值, 即函数名不能以表达式左值的形式在程序中出现
如以下示例中的操作是禁止的
func f(a: Int64): Int64 { a } // f = {a: Int64 => a + 1} // compile-time error
仓颉中, 函数的定义直观表示是这样的:
修饰符 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
语义
struct
和enum
类型在语言规约之前的文档中有提及, 但是并无修饰符的具体介绍, 所以应该还是在类相关内容中
参数
函数定义时参数列表中参数的顺序: 非命名参数, 命名参数(包括: 不带默认值命名参数和带默认值的参数)
参数列表的语法定义如下:
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++函数参数中的缺省值
对于非命名参数, 可以使用一个
_
代替一个函数体中不会使用到的参数
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
修饰, 在函数定义内不能对其进行赋值
func foo(a: Int64, b: Float64) { a = 1 // Error: 参数'a'是不可变的, 不能被赋值 b = 1.0 // Error: 参数'b'是不可变的, 不能被赋值 }
函数的参数类型不受下述”是否是命名形参”、“是否有默认值”的影响;
且讨论参数类型时, 一个类型与它的
alias
被视为相同的类型
函数定义例子中有两个错误示例:
- 非命名参数不允许设置初始值
- 命名参数之后, 不允许再出现非命名参数
仓颉中函数的参数均为不可变类型, 是不允许赋值的
命名形参
函数定义时通过 在形参名后添加
!
来定义命名形参
namedParameter : identifier '!' ':' type ;
函数定义时, 命名形参后不允许有非命名形参
func add1(a: Int32, b!: Int32): Int32 { a + b } // ok func add2(a!: Int32, b: Int32): Int32 { a + b } // error
当形参被定义成命名形参后, 调用这个函数时, 则必须在实参值前使用
参数名:
前缀来指定这个实参对应的形参, 否则编译报错
func add(a: Int32, b!: Int32): Int32 { a + b } add(1, b: 2) // ok add(1, 2) // error
如果抽象函数或
open
修饰的函数有命名形参, 那么实现函数或override
修饰的函数也需要保持同样的命名形参
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 } }
仓颉中, 如果函数存在命名参数, 则必须通过参数名: 值
来进行传参
参数的默认值
函数的参数可以有默认值, 通过
=
来为参数定义默认值当默认值被定义后, 调用这个函数可以忽略这个参数, 函数体会使用默认值
为了便于理解, 有默认值的参数称为可选参数
函数定义时, 可选参数是一种命名形参, 可选参数名后必须添加
!
, 否则编译报错;
defaultParameter : identifier '!' ':' type '=' expression ;
如下示例, 我们定义了一个
add
函数, 它的参数类型列表(Int32, Int32)
函数定义时,
b
具有默认值1
因此, 当我们调用
add
函数, 并且只传递一个值3
时,3
会被赋值给a
, 从而返回结果4
如果传入两个值
3
和2
, 那么b
的值为2
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 表达式)不允许有可选参数
函数参数默认值中引入的名字从静态作用域中获得, 即引入的名字为函数定义时可访问到的名字
函数参数默认值中引入的名字在函数定义时可访问即可, 无需和函数本身的可访问性一致
函数参数和其默认值不属于该函数的函数体
参数默认值在函数调用时求值, 而非在函数定义时求值
规定, 函数调用时参数求值顺序是按照定义时顺序从左到右, 函数参数的默认值可以引用定义在该参数之前的形参
函数调用时, 通过函数名调用可以使用默认值, 通过变量名调用不支持使用默认值
// 编译时错误 // 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) }
/* 在默认值中引入的名称, 不需要具有与函数相同或更严格的可访问性 */ 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}") } }
/* 调用函数时, 函数声明中的名称可以使用默认值 使用变量名调用函数时, 参数不能是可选的, 即不支持使用默认值 */ 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
的函数体中:
func f() { func g(x! :Int64 = return) { // Error: return 必须在函数体内使用 0 } 1 }
这个意思是, 仓颉中函数体 就只是{}
的内容, 参数列表什么的都是不算的
函数体
函数体由一个
block
组成
局部变量
在仓颉编程语言中, 允许在函数体内定义变量, 将其称为局部变量
变量可以用
var
修饰为可变变量或let
修饰为不可变变量
func foo(): Unit { var a = 1 let b = 1 }
熟悉C/C++, 对局部变量再熟悉不过了
嵌套函数
在仓颉编程语言中, 允许在函数体内定义函数, 将其称为嵌套函数
嵌套函数中可以捕获外部函数中定义的变量或其他嵌套函数
嵌套函数支持递归
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)
func returnAB(a: Int32, b: Int32): (Int32, Int32) { (a, b) }
函数类型作为返回类型: 可以使用函数类型作为另一个函数的返回类型
如下示例, 在
:
后紧跟的是add
函数的类型(Int32, Int32) -> Int32
func returnAdd(a: Int32, b: Int32): (Int32, Int32) -> Int32 { return {a, b => a + b} // 返回一个 lambda 表达式 }
函数作为仓颉语言中的一等公民, 是可以作为返回值的, 返回值类型也就有函数类型
函数返回元组, 实现多个返回值返回
如果指定函数的返回类型, 则在函数定义的参数列表后使用
:Type
指定此时要求函数体的类型、函数体中所有
return e
表达式中e
的类型是标注的类型(Type
)的子类型, 否则编译报错
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
示例如下:
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
的最小公共父类型如果不存在最小公共父类型, 则产生一个编译错误
示例如下:
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
修饰的语义
仓颉中, 如果函数定义未指定返回值类型, 那么编译器将自动推导返回类型
推导过程为:
- 记录块中 最后一个表达式 或 声明的类型:
T0
- 记录所有
return e
表达式中,e
的类型为T1, T2, T3, ...
- 找
T0, T1, T2, T3, ...
的最小公共父类型, 作为函数的返回类型
仓颉中, 函数的返回值不能修改, 具有let
修饰的语义
函数声明
仓颉编程语言中, 函数声明和函数定义的区别是, 前者没有函数体
函数声明的语法如下:
functionDeclaration : functionModifierList? 'func' identifier typeParameters? functionParameters (':' type)? genericConstraints? ;
函数声明可以出现在抽象类, 接口中
仓颉中的函数声明与C/C++中的函数声明有些不同
C/C++中的函数声明, 只是声明一下函数的符号, 此函数是否已经被实现是不确定的
但仓颉中的函数声明, 好像也只是证明一下函数的符号, 且只能出现在抽象类或接口中, 这意味着 仓颉中的函数声明, 表示这个函数一定没有被实现
所以在之后的继承或接口实现中, 必须要实现声明的函数
仓颉中函数的声明 与 函数定义的区别, 就是没有函数体
函数的重定义
对于非泛型函数, 在同一个作用域中, 参数类型完全相同的同名函数被视为重定义, 将产生一个编译错误
以下几种情况要格外注意:
同名函数即使返回类型不同也构成重定义
同名的泛型与非泛型函数永不构成重定义
在继承时, 对于子类中的一个与父类同名且参数类型完全相同的函数, 若其返回类型是父类中的版本的子类型, 则构成覆盖, 也不构成重定义
这是因为子类与父类作用域不同
对于两个泛型函数, 如果重命名一个函数的泛型形参后, 其非泛型部分与另一个函数的非泛型部分构成重定义, 则这两个泛型函数构成重定义
举例如下:
下面这两个泛型函数构成重定义, 因为存在一种
[T1 |-> T2]
的重命名(作用到第一个函数上), 使得两个函数的非泛型部分构成重定义
func f<T1>(a: T1) {} func f<T2>(b: T2) {}
下面这两个泛型函数不构成重定义, 因为找不到上述的一种重命名
func f<X, Y>(a:X, b:Y) {} func f<Y, X>(a:X, b:Y) {}
下面的这两个泛型函数构成重定义, 因为存在一种
[X |-> Y, Y |-> X]
的重命名使得两个函数的非泛型部分构成重定义
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) {}
, 两个泛型函数类型不也是完全一致吗?
是完全一致, 但是这是非法的泛型定义, 因为多个泛型类型参数名字一样, 所以多类型参数的泛型, 不存在单独类型参数的映射, 因为泛型的类型参数名字不能存在重复
函数类型
函数类型由函数的参数类型和返回类型组成, 其中参数类型与返回类型之间用
->
分隔
functionType: : '(' (type (, type)*)? ')' '->' type
以下是一些示例:
示例 1: 没有参数、返回类型为
Unit
func hello(): Unit { print("Hello!") } // function type: () -> Unit
示例 2: 参数类型为
Int32
, 返回类型为Unit
func display(a: Int32): Unit { print("${a}") } // function type: (Int32) -> Unit
示例 3: 两个参数类型均为
Int32
, 返回类型为Int32
func add(a: Int32, b: Int32): Int32 { a + b } // function type: (Int32, Int32) -> Int32
示例 4: 参数类型为
(Int32, Int32) -> Int32
,Int32
和Int32
, 返回类型为Unit
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
func returnAdd(a: Int32, b: Int32): (Int32, Int32) -> Int32 { {a, b => a + b} } // function type: (Int32, Int32) -> (Int32, Int32) -> Int32
示例 6: 两个参数类型均为
Int32
, 返回为一个元组类型为:(Int32, Int32)
func returnAB(a: Int32, b: Int32): (Int32, Int32) { (a, b) } // function type: (Int32, Int32) -> (Int32, Int32)
仓颉中的每个函数都拥有自己的类型: (参数类型列表) -> 返回值类型
函数调用
有关函数调用表达式的语法, 请参考函数调用表达式↗
命名实参
命名实参是指在函数调用时, 在实参值前使用
形参名 :
前缀来指定这个实参对应的形参只有在函数定义时使用
!
定义的命名形参, 才可以在调用时使用命名实参的语法函数调用时, 所有的命名形参均必须使用命名实参来传参, 否则报错
在函数调用表达式中, 命名实参后不允许出现非命名实参
使用命名实参指定实参值时, 可以不需要和形参列表的顺序保持一致
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
因为函数定义时, 命名形参之后不允许出现非命名形参, 所以后面所有的命名形参都可以不按照参数列表顺序进行传参
函数调用类型检查
本节介绍的是, 给定一个调用表达式, 对调用表达式所需要进行的类型检查
如果被调用的函数涉及重载, 则需要根据函数重载的规则进行类型检查和重载决议
如果函数调用表达式中指定了类型参数, 只有类型实参的个数与类型形参的个数相同才可能通过类型检查, 即假设函数调用表达式为:
f<T1, ..., Tm>(A1, ..., An)
, 其中给定了m
个类型实参, 则函数类型形参数量须为m
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
根据调用表达式中的实参 和 调用表达式所在的类型检查上下文中 指定的返回类型
R
对函数进行类型检查假设函数定义为:
**如果调用表达式带了类型实参:
fi<T1, ..., Tp>(A1, ..., Ak)
, 那么对于函数fi
的类型检查规则如下: **
类型实参约束检查: 类型实参
<T1, ..., Tp>
需要满足函数fi
的类型约束参数类型检查: 将类型实参代入函数
fi
的形参后, 满足实参类型(A1, ..., Ak)
是类型实参代入形参后类型的子类型返回类型检查: 如果调用表达式的上下文对其有明确类型要求
R
, 则需要根据返回类型进行类型检查, 将类型实参代入函数fi
的返回类型Ri
后, 满足类型实参代入后的返回类型是R
的子类型**如果调用表达式不带类型实参:
f(A1, ..., An)
, 那么对于函数fi
的类型检查规则如下: **
如果**
fi
是非泛型函数**, 则按如下规则进行类型检查:
参数类型检查: 实参类型
(A1, ..., Ak)
是形参类型的子类型返回类型检查: 如果调用表达式的上下文对其有明确类型要求
R
, 则检查函数fi
的返回类型Ri
是R
的子类型
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
如果
fi
是泛型函数, 则按如下规则进行类型检查:
参数类型检查: 存在代换使得实参类型
(A1, ..., Ak)
是形参类型代换后的类型的子类型返回类型检查: 如果调用表达式的上下文对其有明确类型要求
R
, 则需要根据返回类型进行类型检查, 将 a) 中的代换代入函数fi
的返回类型Ri
后, 满足代换后的返回类型是R
的子类型需要注意的是:
- 如果函数有缺省值, 在类型检查时会补齐缺省值之后再进行类型检查;
- 如果函数有命名参数, 命名参数的顺序可能会和形参顺序不一致, 在类型检查时, 命名实参要与名字匹配的命名形参对应
总结几点就是:
泛型函数, 传入的类型实参数量 需要与 目标泛型函数的类型形参数量 保持一致
泛型函数, 如果显式传入类型实参, 则会:
- 进行类型约束的检查, 即 判断是否满足 函数定义时 对实参类型指定的约束条件(
where 条件
) - 类型实参传入之后, 函数调用实参的类型要满足 是传入类型实参的子类型
- 根据传入类型实参, 检查最终的返回值类型 是否满足 函数定义的返回类型的子类型
- 进行类型约束的检查, 即 判断是否满足 函数定义时 对实参类型指定的约束条件(
如果不显式传入类型实参, 则有可能是非泛型函数
如果是非泛型函数, 则会:
- 检查函数调用实参类型 是否满足 函数定义的形参类型的子类型
- 检查返回值类型 是否满足 函数定义的返回类型的子类型
如果是泛型函数, 则会:
检查调用参数类型
根据函数调用传入的实参类型, 推导类型代换σ(上面的类型映射数学公式), 使得实参类型满足代换后的形参类型
即 传入的实参的类型, 属于函数形参类型的子类型
检查返回类型
还是根据推导和代换的形参类型, 去推导最后的返回值类型 是否满足 函数定义的返回类型的子类型
文本上比较绕, 但其实核心很简单: 根据实参类型推导、代换形参类型, 再根据形参类型 进行实际的参数检查、返回值类型检查等操作
尾随Lambda
当函数最后一个形参是函数类型, 并且函数调用对应的实参是
lambda
时, 我们可以使用尾随lambda
语法, 将lambda
放在函数调用的尾部, 括号外面
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
func f(fn: (Int64)->Int64) { fn(1) } f{ i => i * i }
如果尾随
lambda
不包含形参,=>
可以省略
func f(fn: ()->Int64) { fn() } f{ i * i }
需要注意的是, 尾随
lambda
语法只能用在具有函数名/变量名
的函数调用上, 并且尾随lambda
的lambda
表达式只会解释为函数名/变量名
对应的函数的参数这意味着以下两种调用例子是不合法的:
func f(): (()->Unit)->Unit { {a => } } f() {} // Error, lambda 表达式不是 f 的参数 func g(a: ()->Unit) {} if (true) { g } else { g } () {} // Error, 非法尾随 lambda 语法
必须先将以上的表达式赋值给变量, 使用变量名调用时才可以使用尾随
lambda
语法如下面的代码所示:
let f2 = f() f2 {} // ok let g2 = if (true) { g } else { g } g2() {} // ok
普通函数调用和构造函数调用都可以使用这个语法, 包含
this()
和super()
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
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
字面量
变长参数没有特殊的声明语法, 只要求函数声明处最后一个非命名参数是
Array
类型变长参数在函数调用时可以使用普通参数列表的形式逐个传入
Array
的多个元素非命名参数中, 只有最后一个位置的参数可以使用变长参数
命名参数不能使用这个语法糖
变长参数对全局函数、静态成员函数、实例成员函数、局部函数、构造函数、函数变量、
lambda
、函数调用操作符重载、索引操作符重载的调用都适用, 不支持其它操作符重载、compose
、pipeline
这几种调用方式变长参数的个数可以是 0 个或以上
变长参数只有在函数重载所有情况都不匹配的情况下, 才判断是否可以应用语法糖, 优先级最低
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
类型的数据
函数重载决议总是会优先考虑不使用变长参数就能匹配的函数, 只有在所有函数都不能匹配, 才尝试使用变长参数解析
当编译器无法决议时会报错
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
是全局函数它的作用域是全局作用域
func globalFunction() {}
嵌套函数
在函数体内部定义的函数成为嵌套函数, 它的作用域是局部的, 详见[作用域]
如下示例, 函数
nestedFunction
是嵌套函数, 它的作用域是从定义之后开始, 到globalFunction
函数体结束
func globalFunction() { func nestedFunction() {} }
成员函数
在类型定义中可以声明或定义成员函数
成员函数的作用域是整个类型及其扩展
interface Myinterface { func foo(): Unit static func bar(): Unit } class MyClass { func foo() {} static func bar() {} }
扩展成员函数
在扩展中可以声明额外的成员函数
它的作用域是被扩展类型的所有扩展, 同时受访问修饰符限制
extend MyType { func foo(): Unit {} }
如果你了解过任意一门有面向对象思想的编程语言, 这一部分应该没有什么特别的问题