- 文档
- 编程语言
【免费下载链接】cangjie_docs
仓颉编程语言开发者文档。
本文围绕仓颉编程语言(Cangjie)的 Lambda 表达式展开,系统讲解其{ ... => ... }语法结构、参数与返回类型的推断规则、=>的省略边界以及调用方式,并结合仓库中的闭包、函数类型与集合 API 文档,剖析 Lambda 表达式在真实开发场景中的落地用法。读完后,你将能够独立书写类型推断清晰的 Lambda 表达式,理解它与尾随 lambda、闭包之间的协作关系,并将其熟练用于数组初始化、集合构造和 FFI 回调等实战场景。
Lambda 表达式的定义与基本语法
Lambda 表达式是一种匿名函数(即没有函数名的函数),其核心设计目的是在程序中快速定义简短的函数逻辑,无需显式声明函数名称。这一概念起源于数学中的 λ 演算(lambda calculus),后被引入多种编程语言(如 C++、Python、C# 等),用于简化代码并提升灵活性。仓颉编程语言中也引入了 Lambda 表达式,并为其设计了与函数类型体系无缝衔接的语义。
Lambda 表达式的语法为如下形式:
{ p1: T1, ..., pn: Tn => expressions | declarations }其中各部分的含义如下:
- 参数列表(
=>之前):多个参数之间使用,分隔,每个参数名和参数类型之间使用:分隔。=>之前也可以没有参数,构成无参 Lambda 表达式; - 表达式体(
=>之后):是一组表达式或声明序列,其书写规则与普通函数体一致,可以包含多条语句并以最后一条表达式的值作为返回结果; - 参数名的作用域:与函数参数相同,仅限定在 Lambda 表达式的函数体部分,其作用域级别可视为与 Lambda 表达式体内定义的变量等同。
下面给出最基本的两个示例:带参 Lambda 与无参 Lambda。
let f1 = {a: Int64, b: Int64 => a + b} var display = { => // 无参 Lambda 表达式 println("Hello") println("World") }可以看到,即使参数列表为空,=>也必须保留(详见下一节)。无参 Lambda 的函数体可以是任意声明序列,例如连续的两条println调用。
=>不可省略的规则与尾随 Lambda 例外
一条容易踩坑的规则是:Lambda 表达式不管有没有参数,都不可以省略=>,唯一的例外是它作为函数的尾随 lambda 使用。
var display = {=> println("Hello")} func f2(lam: () -> Unit) {} let f2Res = f2 {println("World")} // 允许省略 =>上面示例中,f2 {println("World")}之所以能省略=>和空参数列表,是因为它符合尾随 lambda 的语法糖条件:函数的最后一个形参是函数类型,且调用时对应的实参是 lambda,此时 lambda 可以放在函数调用的尾部、圆括号外面。这一机制使函数调用看起来像是语言内置的语法一样,增强了语言的可扩展性。
需要注意二者的边界:
- 在赋值给变量、直接书写 Lambda 字面量的场合,
{...}内必须显式写出=>,无参时写作{=> ...}; - 只有在函数调用的实参位置且形参为函数类型时,才能以尾随 lambda 的形式省略
=>(当函数调用有且只有一个 lambda 实参时,还可以连同()一起省略)。
因此,{=> println("Hello")}与f2 {println("World")}分别代表了两种不同的书写语境,不能互相套用。
Lambda 表达式的参数类型推断
Lambda 表达式中参数的类型标注可以缺省。当参数类型省略时,编译器会尝试进行类型推断,当编译器无法推断出类型时会编译报错。官方文档明确了两种可以成功推断的情形:
- Lambda 表达式赋值给变量时,其参数类型根据变量类型推断;
- Lambda 表达式作为函数调用表达式的实参使用时,其参数类型根据函数的形参类型推断。
先看"赋值给变量"的场景,变量类型(Int64, Int64) -> Int64为两个参数提供了上下文:
// 参数类型根据变量 sum1 的类型推断 var sum1: (Int64, Int64) -> Int64 = {a, b => a + b} var sum2: (Int64, Int64) -> Int64 = {a: Int64, b => a + b}这里也展示了混合书写方式:可以只给部分参数标注类型({a: Int64, b => a + b}),其余参数仍依赖推断。两种写法完全等价,风格上通常建议全部省略或全部标注,保持统一。
再看"作为函数实参"的场景,形参类型(Int64) -> Int64决定了 Lambda 参数的推断结果:
func f(a1: (Int64) -> Int64): Int64 { a1(1) } main(): Int64 { // Lambda 的参数类型根据函数 f 的形参类型推断 f({a2 => a2 + 10}) }调用f({a2 => a2 + 10})时,编译器沿着调用点逆向推导:a1的形参类型要求实参是(Int64) -> Int64,于是 Lambda 参数a2的类型被推断为Int64。如果 Lambda 体中的运算与该推断冲突(例如a2被用于字符串拼接),编译期就会报错。
Lambda 表达式的返回类型推断
Lambda 表达式不支持显式声明返回类型,其返回类型总是从上下文中推断出来,若无法推断则报错。推断分为两大类情形。
上下文明确指定返回类型
若上下文中存在一个明确的目标类型,Lambda 的返回类型直接取该目标类型。具体又有三个子场景:
1. 赋值给变量时,返回类型根据变量类型推断:
let f: () -> Unit = {=> println(10)}变量f的类型() -> Unit指定了返回类型Unit,Lambda 体println(10)的求值结果恰好也是Unit,类型匹配通过。
2. 作为参数使用时,返回类型根据使用处所在函数调用的形参类型推断:
func f(a1: (Int64) -> Int64): Int64 { a1(1) } main(): Int64 { f({a2: Int64 => a2 + 10}) }实参位置要求的函数类型是(Int64) -> Int64,因此 Lambda 的返回类型被确定为Int64,Lambda 体a2 + 10的类型与之吻合。
3. 作为返回值使用时,返回类型根据使用处所在函数的返回类型推断:
func f(): (Int64) -> Int64 { {a: Int64 => a} }外层函数f声明了返回类型(Int64) -> Int64,于是作为函数体末尾表达式的 Lambda 就按该类型进行约束检查——这是"返回一个函数类型"的典型写法。
上下文未明确类型时的体推断
若上下文中类型未明确,与推导普通函数的返回值类型类似,编译器会根据 Lambda 表达式体中所有return xxx表达式里xxx的类型,以及 Lambda 表达式体本身的类型,共同推导出 Lambda 的返回类型。
=>右侧的内容与普通函数体的规则一样,返回类型取体表达式的类型:
let sum1 = {a: Int64, b: Int64 => a + b}这里a + b的类型是Int64,因此sum1的类型被推断为(Int64, Int64) -> Int64。
=>的右侧为空,返回类型为Unit:
let f = {=>}空体的 Lambda 返回Unit,等价于一个无参、无实际动作的占位函数。
参数推断与返回推断的对比
把前面两节放在一起看,可以总结出推断规则的对称结构:
| 使用场景 | 参数类型来源 | 返回类型来源 |
|---|---|---|
| 赋值给变量 | 变量类型 | 变量类型 |
| 作为函数实参 | 调用处的形参类型 | 调用处的形参类型 |
| 作为函数返回值 | 所在函数的返回类型 | 所在函数的返回类型 |
| 无明确上下文 | 无法推断时编译报错 | 根据体中return表达式及体类型推断 |
掌握这张表后,书写 Lambda 时可以反向运用:凡是希望编译器推断成功的位置,就确保上下文(变量声明、形参、返回类型)中给出完整的函数类型;凡是希望保持简洁的位置,就依赖体表达式自身的类型。
Lambda 表达式的调用
立即调用
Lambda 表达式支持立即调用(immediate invocation),即 Lambda 字面量后面直接跟调用参数:
let r1 = {a: Int64, b: Int64 => a + b}(1, 2) // r1 = 3 let r2 = {=> 123}() // r2 = 123{a: Int64, b: Int64 => a + b}(1, 2):定义并立即调用双参 Lambda,结果为3;{=> 123}():无参 Lambda 的空参数调用,结果为123。
立即调用适合定义一次性的、不希望污染命名空间的简短逻辑,例如临时的转换、包装或求值。
赋值给变量后调用
Lambda 表达式也可以赋值给一个变量,之后使用变量名进行调用:
func f() { var g = {x: Int64 => println("x = ${x}")} g(2) }调用g(2)后输出x = 2。这种"先赋值、后调用"的方式是 Lambda 与仓颉函数类型体系衔接的关键:由于函数是一等公民,Lambda 可以被赋值给变量、作为参数传递、作为返回值返回,变量g在此处等价于一个类型为(Int64) -> Unit的函数值。
深入理解:Lambda 与闭包的变量捕获规则
当 Lambda 表达式访问了定义它的静态作用域之外的局部变量时,这个 Lambda 和捕获的变量一起就构成了一个闭包。理解闭包规则,是正确写出 Lambda 的必要补充,因为 Lambda 体中一旦访问外部局部变量,就触发了捕获语义:
- 变量捕获的边界:对全局变量和静态成员变量的访问不属于变量捕获;对本 Lambda 的形参、本 Lambda 内定义的局部变量的访问也不属于捕获;
- 可见性约束:被捕获的变量必须在闭包定义时可见,且必须已完成初始化,否则编译报错。例如在
let y = 88之前写{ => println(y) }会触发cannot capture 'y' which is not defined yet错误; var捕获的特殊限制:为了防止捕获了var声明变量的闭包逃逸(即函数执行完毕后仍可在函数外部被调用),这类闭包只能被调用,不能作为一等公民使用——不能赋值给变量,不能作为实参或返回值使用。这意味着:一个捕获了外部可变变量var的 Lambda,不能像普通 Lambda 那样被赋值给变量再传递;- 捕获具有传递性:如果一个函数
f调用了捕获var变量的函数g,且g捕获的var变量不在f内定义,那么f同样捕获了该变量,f也不能作为一等公民使用。
因此,前文"赋值给变量后调用"的用法中,被赋值的 Lambda 应当是不捕获外部可变变量的;若确实需要跨作用域修改共享状态,更稳妥的做法是捕获一个引用类型的实例,通过其可变成员变量实现共享修改(这是引用捕获而非按值捕获,不受逃逸限制)。
Lambda 在仓颉中的典型实战场景
结合仓库中的标准库文档,Lambda 表达式在以下几类场景中出镜频率最高,可以作为日常开发的参考模式。
1. 数组与集合的按规则初始化
仓颉的数组与集合构造函数支持传入一个"规则函数"来逐元素初始化,天然适合 Lambda。以 数组 为例:
let d = Array<Int64>(3, {i => i + 1})这里{i => i + 1}作为初始化函数,参数i的类型由Array构造函数的形参推断,返回类型由元素类型约束,正好演示了前文"作为实参时双向推断"的规则。
同样的模式贯穿 ArrayList、HashMap、HashSet 等集合的构造函数:
let e = ArrayList<String>(2, {x: Int64 => x.toString()}) let e = HashMap<Int64, Int64>(10, {x: Int64 => (x, x * x)}) let e = HashSet<Int64>(10, {x: Int64 => (x * x)})ArrayList<String>(2, ...):容量 2,每个元素由规则函数生成字符串;HashMap的规则函数返回一个(key, value)元组,Lambda 体用元组字面量一次性给出键值对;HashSet的规则函数返回单元素值。
这类"构造即初始化"的写法避免了先构造再逐个add的样板代码,是 Lambda 最常见的落地形态。
2. FFI 回调函数
在与 C 语言互操作的场景中(参见 仓颉与 C 互操作文档),需要把一个仓颉函数作为回调传递给 C 侧。由于回调往往只需要一个简短的处理逻辑,Lambda 是首选:
let f: CFunc<(Int32) -> Unit> = {i => println("handle ${i} in callback")}注意此处的推断语境是"赋值给变量":变量f声明为CFunc<(Int32) -> Unit>,Lambda 参数i的类型由该声明推断为Int32,返回类型被约束为Unit。
3. 函数组合:composition的本质就是 Lambda
函数调用语法糖 文档中定义了中缀操作符~>(composition),其展开形式本身就是一条 Lambda 表达式:f ~> g等价于{ x => g(f(x)) }。例如:
func f(x: Int64): Float64 { Float64(x) } let lambdaComp = {x: Int64 => x} ~> f // 等价于 { x: Int64 => f({x: Int64 => x}(x)) }从源码结构看,composition可以推断是编译器把 Lambda 字面量在语法层面提升为一等表达式的产物:单参函数与单参函数(或 Lambda)的组合,在展开后就是一个捕获了f、g的匿名函数。类似的,pipeline表达式e1 |> e2展开为let v = e1; e2(v),当调用目标含命名形参时,也可以显式包一层 Lambda 来适配:
func f(a!: Int64): Unit {} var x = 1 |> {x: Int64 => f(a: x)} // OK这说明 Lambda 不仅是"匿名函数",还是仓颉各类调用语法糖的底层展开形式,理解它有助于读懂语法糖展开后的行为。
小结
- 语法:Lambda 表达式写作
{ p1: T1, ..., pn: Tn => expressions | declarations },参数作用域与函数参数相同; =>规则:任何情况下都必须保留=>,唯一例外是作为函数调用的尾随 lambda;- 参数推断:来自变量类型或函数形参类型两个上下文,无法推断即编译报错;
- 返回类型:不可显式声明;上下文指定时取上下文类型,否则按体中
return表达式与体类型共同推断,空体为Unit; - 调用:支持立即调用,也支持赋值给变量后以变量名调用;
- 进阶边界:Lambda 访问外部局部变量即成为闭包,捕获
var的闭包受"只可调用、不可作为一等公民"的逃逸限制约束。
本文所有示例均摘自仓颉官方开发者文档仓库,完整背景可进一步阅读 Lambda 表达式(中文)、函数调用语法糖、闭包 与 函数类型 等文档,其中每个代码块都带有编译或运行标记,可直接用于对照验证。
- 文档
- 编程语言
【免费下载链接】cangjie_docs
仓颉编程语言开发者文档。
相关推荐
仓颉语言函数调用语法糖详解:尾随 Lambda、Pipeline/Composition 流表达式与变长参数
仓颉语言函数调用语法糖详解:尾随 Lambda、Pipeline/Composition 流表达式与变长参数 本篇基于仓颉编程语言(Cangjie)开发者文档中
文档编程语言《仓颉语言实战》仓颉Lambda表达式、闭包与管道语法糖:函数高级用法完全指南
《仓颉语言实战》仓颉Lambda表达式、闭包与管道语法糖:函数高级用法完全指南 《仓颉语言实战》是张磊编著、清华大学出版社出版的仓颉语言入门教程,本书示例代码库
示例工程Carbon 语言 `if` 表达式设计详解:语法、求值语义与公共类型推断机制
Carbon 语言 if 表达式设计详解:语法、求值语义与公共类型推断机制 导读 if 表达式( if expression)是 Carbon 语言中把条件分支
编程语言编译器标准库
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考