NOTE阅读文档版本:
语言规约 Cangjie-0.53.18-Spec
具体开发指南 Cangjie-LTS-1.0.4
在阅读 了解仓颉的语言规约时, 难免会涉及到一些仓颉的示例代码, 但 我们对仓颉并不熟悉, 所以可以用 仓颉在线体验 快速验证
有条件当然可以直接 配置 Canjie-SDK
WARNING博主在此之前, 基本只接触过 C/C++语言, 对大多现代语言都没有了解, 所以在阅读过程中遇到相似的概念, 难免会与 C/C++中的相似概念作类比, 见谅
且, 本系列是文档阅读, 而不是仓颉的零基础教学, 所以如果要跟着阅读的话最好有一门编程语言的开发经验
WARNING在阅读仓颉编程语言的开发指南之前, 已经大概阅读了一遍 仓颉编程语言的语言规约, 已经对仓颉编程语言有了一个大概的了解
所以在阅读开发指南时, 不会对类似: 类、函数、结构体、接口等解释起来较为复杂名称 做出解释
此样式内容, 表示文档原文内容
函数
嵌套函数
定义在源文件顶层的函数被称为全局函数
定义在函数体内的函数被称为嵌套函数
示例, 函数
foo内定义了一个嵌套函数nestAdd, 可以在foo内调用该嵌套函数nestAdd, 也可以将嵌套函数nestAdd作为返回值返回, 在foo外对其进行调用:func foo() {func nestAdd(a: Int64, b: Int64) {a + b + 3}println(nestAdd(1, 2)) // 6return nestAdd}main() {let f = foo()let x = f(1, 2)println("result: ${x}")}程序会输出:
6result: 6
仓颉函数, 除了在函数外(源文件顶层或结构体、类内)定义, 即 全局函数 或 成员函数
还可以在函数体内定义, 这样在函数体内定义的函数被称为 嵌套函数
嵌套函数可以直接在函数中被调用, 还可以作为函数的返回值返回
Lambda表达式
Lambda表达式定义
Lambda表达式是一种匿名函数(即没有函数名的函数), 其核心设计目的是在程序中快速定义简短的函数逻辑, 无需显式声明函数名称这一概念起源于数学中的
λ演算(lambda calculus), 后被引入多种编程语言(如C++、Python、C#等), 用于简化代码并提升灵活性仓颉编程语言中也引入了
Lambda表达式, 具体使用介绍将在本小节展开介绍
Lambda表达式的语法为如下形式:{ p1: T1, ..., pn: Tn => expressions | declarations }其中:
=>之前为参数列表, 多个参数之间使用,分隔, 每个参数名和参数类型之间使用:分隔
=>之前也可以没有参数
=>之后为Lambda表达式体, 是一组表达式或声明序列
Lambda表达式的参数名的作用域与函数的相同, 为Lambda表达式的函数体部分, 其作用域级别可视为与Lambda表达式的函数体内定义的变量等同let f1 = { a: Int64, b: Int64 => a + b }var display = { => // 无参数 lambda 表达式println("Hello")println("World")}
lambda表达式是一种匿名函数语法, C++中也存在
仓颉的lambda表达式语法为:
{ 参数列表 => 一组声明或表达式序列 }要强调的是, 参数列表不能用()包裹, 同时 一组声明或表达式序列 也不能被{}包裹
lambda参数列表, 就像函数的参数列表一样, 只不过不用()包裹
一组声明或表达式序列, 在lambda表达式中 不用{}包裹 也是被看作一个整体的, 所以可以随意按合法语法换行
lambda表达式的参数, 也可以在lambda表达式函数体内使用
Lambda表达式不管有没有参数, 都不可以省略=>, 除非其作为尾随lambda例如:
var display = { => println("Hello") }func f2(lam: () -> Unit) {}let f2Res = f2 { println("World") } // OK, 省略 =>
仓颉lambda表达式的=>不允许被省略, 除非是尾随lambda
不过, 也只有在lambda参数为空时, 尾随lambda的=>可以省略
TIP尾随
lambda
lambda表达式作为函数的最后一个参数, 可以以**尾随lambda**形式传参即, 当函数的最后一个形参是函数类型时, 调用时最后一个参数要传入
lambda表达式时, 可以使用尾随lambda的形式尾随
lambda形式, 不是将lambda表达式传入函数的参数列表中, 而是在函数调用时将lambda表达式声明在函数调用之后:举个例子:
func function(param: Int64, lam: (Int64, String) -> Unit) {lam(param, "Cangjie")}main() {// 正常传参function(10, {param1: Int64, param2: String => println("result: ${param1 * param2.size}")})// 尾随lambdafunction(10) {param1: Int64, param2: String => println("result: ${param1 * param2.size}")}}这两种调用方式都是可以的, 一个
lambda表达式正常作为实参传入, 一个以尾随lambda的形式传入
Lambda表达式中参数的类型标注可缺省以下情形中, 若参数类型省略, 编译器会尝试进行类型推断, 当编译器无法推断出类型时会编译报错:
Lambda表达式 赋值给变量 时, 其参数类型根据变量类型推断
Lambda表达式 作为 函数调用表达式的实参 使用时, 其参数类型根据函数的形参类型推断// 参数类型由变量 sum1 的类型推断得出var sum1: (Int64, Int64) -> Int64 = { a, b => a + b }var sum2: (Int64, Int64) -> Int64 = { a: Int64, b => a + b }func f(a1: (Int64) -> Int64): Int64 {a1(1)}main(): Int64 {// lambda 的参数类型是从函数 f 的类型推断出来的f({ a2 => a2 + 10 })}
lambda表达式的参数的类型是可以省略的
但, 编译器要能根据上下文推断出来, 否则报错
比如, 给变量赋值时, 根据变量声明类型进行推断; 作为实参传入参数形参时, 根据形参类型进行推断
Lambda表达式中不支持声明返回类型, 其返回类型总是从上下文中推断出来, 若无法推断则报错
若上下文明确指定了
Lambda表达式的返回类型, 则其返回类型为上下文指定的类型
Lambda表达式赋值给变量时, 其返回类型根据 变量类型 推断返回类型:let f: () -> Unit = { ... }
Lambda表达式作为参数使用时, 其返回类型根据 使用处所在的函数调用的形参类型 推断:func f(a1: (Int64) -> Int64): Int64 {a1(1)}main(): Int64 {f({ a2: Int64 => a2 + 10 })}
Lambda表达式作为返回值使用时, 其返回类型根据 使用处所在函数的返回类型 推断:func f(): (Int64) -> Int64 {{ a: Int64 => a }}若上下文中类型未明确, 与推导函数的返回值类型类似, 编译器会根据
Lambda表达式体中所有return表达式return xxx中xxx的类型, 以及Lambda表达式体的类型, 来共同推导出Lambda表达式的返回类型
=>右侧的内容与普通函数体的规则一样, 返回类型为Int64:let sum1 = { a: Int64, b: Int64 => a + b }
=>的右侧为空, 返回类型为Unit:let f = { => }
仓颉ladmbda的参数类型 是可缺省的
但 返回类型是不可指定的, 只能根据上下文进行推导:
-
若 使用处的上下文 有指定返回值类型, 那么 就根据指定的类型进行推导
-
若 使用处的上下文 没有指定返回值类型, 那么 就根据
lambda表达式体中 所有return expr表达式中的expr类型 以及lambda表达式体的类型 共同进行推导
这部分在具体使用时应该可以有更熟悉、深入的了解
Lambda表达式调用
Lambda表达式支持立即调用, 例如:let r1 = { a: Int64, b: Int64 => a + b }(1, 2) // r1 = 3let r2 = { => 123 }() // r2 = 123
Lambda表达式也可以赋值给一个变量, 使用变量名进行调用, 例如:func f() {var g = { x: Int64 => println("x = ${x}") }g(2)}
其实调用在上面了解定义时, 就已经见过了
闭包 **
一个函数或
lambda从定义它的静态作用域中捕获了变量, 函数或lambda和捕获的变量一起被称为一个闭包, 这样即使脱离了闭包定义所在的作用域, 闭包也能正常运行
闭包, 我个人理解就是:
-
闭包形成:
如果一个函数/
lambda, 捕获了定义时 函数/lambda外部的非全局或静态变量时, 此函数和其捕获的变量, 形成一个闭包闭包形成之后, 闭包封闭、包装, 将捕获的变量”包装”到函数/
lambda内部, 不再直接依赖外部变量但, 这里的”包装”形式 针对不同的类型也是有区别的:
var或let变量有区别, 值类型和引用类型变量也有区别 -
闭包调用
闭包可以直接像函数一样调用, 非特殊通常情况下也可以作为一等公民使用
而且, 调用时能够访问到捕获的变量, 即使 闭包调用时的作用域 已经不在 被捕获变量 定义时的有效作用域
函数或
lambda的定义中对于以下几种变量的访问, 称为变量捕获:
函数的参数缺省值中 访问了 本函数之外定义的局部变量
函数或
lambda内 访问了 本函数或本lambda之外定义的局部变量
class/struct内定义的不是成员函数的函数或lambda访问了实例成员变量或this以下情形的变量访问不是变量捕获:
对定义在本函数或本
lambda内的局部变量的访问对本函数或本
lambda的形参的访问对全局变量和静态成员变量的访问
对实例成员变量在实例成员函数或属性中的访问
由于实例成员函数或属性将
this作为参数传入, 在实例成员函数或属性内通过this访问所有实例成员变量变量的捕获发生在闭包定义时, 因此变量捕获有以下规则:
被捕获的变量必须在闭包定义时可见, 否则编译报错
被捕获的变量必须在闭包定义时已经完成初始化, 否则编译报错
仓颉中, 捕获变量动作发生在闭包定义时
且仓颉闭包中使用到的外部变量, 会被自动捕获
并不是 所有变量在闭包访问都属于变量捕获
变量捕获只针对 非本函数/本lambda的局部变量, 以及成员变量或this
静态变量或全局变量的访问, 不属于变量捕获, 对本身形参的访问就更不属于了
具体下面有所展示
示例 1: 闭包
add, 捕获了let声明的局部变量num, 之后通过返回值返回到num定义的作用域之外, 调用add时仍可正常访问numfunc returnAddNum(): (Int64) -> Int64 {let num: Int64 = 10func add(a: Int64) {return a + num}add}main() {let f = returnAddNum()println(f(10))}程序输出的结果为:
20
此例中, 嵌套函数add捕获了 外层函数中let声明的局部变量num, 形成闭包
变量num被”包装”到add中, add作为一等公民返回
示例 2: 捕获的变量必须在闭包定义时可见
func f() {let x = 99func f1() {println(x)}let f2 = { =>println(y) // Error, 无法捕获尚未定义的'y'}let y = 88f1() // Print 99f2()}示例 3: 捕获的变量必须在闭包定义前完成初始化
func f() {let x: Int64func f1() {println(x) // Error, x 还未初始化}x = 99f1()}
仓颉捕获变量, 只能捕获可见的变量, 且只能捕获已经初始化的变量, 否则编译错误
如果捕获的变量是引用类型, 可修改其可变实例成员变量的值
class C {public var num: Int64 = 0}func returnIncrementer(): () -> Unit {let c: C = C()func incrementer() {c.num++}incrementer}main() {let f = returnIncrementer()f() // c.num 增加 1}
仓颉中, 闭包捕获引用类型变量, 可以通过捕获的变量修改原实例的成员变量
为了防止捕获了
var声明变量的闭包逃逸, 这类闭包只能被调用, 不能作为一等公民使用, 包括不能赋值给变量, 不能作为实参或返回值使用, 不能直接将闭包的名字作为表达式使用func f() {var x = 1let y = 2func g() {println(x) // OK, 捕获一个可变变量}let b = g // Error, g 不能赋值给变量g // Error, g 不能用作表达式g() // OK, g 可以被调用g // Error, g 不能用作返回值}
仓颉规定, var声明的变量被捕获之后, 闭包只能被调用, 不能作为一等公民使用
主要是为了防止引用类型变量的闭包逃逸
值类型变量被捕获之后, 无论是let还是var, 闭包中拥有的是变量的副本, 不存在逃逸
但引用类型变量被捕获则不同, 引用类型变量被捕获, 捕获的就是此引用变量本身 不是实际实例
如果是let引用类型变量, 则变量被捕获前后 闭包内外引用指向是不会更变的, 这就表示 闭包的捕获期望是不会改变的
即, 可以保证 闭包无论何时、何处被调用, 闭包捕获的变量 一定闭包定义时 捕获到的变量实例
但, 如果是var引用类型变量, 则变量被捕获之后, 因为var可变, 就表示此引用变量的实际指向是可以修改的
即, 闭包内外是可以随时修改引用变量的指向的
这就会导致一个问题, 闭包被调用时, 内部访问被捕获的变量 可能已经不是 闭包定义时捕获的变量了
这就会导致闭包调用非预期的问题
而且, 闭包捕获var引用类型变量, 那么闭包内部也是可以 改变此变量的指向的
如果此时, 闭包能够作为一等公民使用, 那么在外部使用时, 就可能导致各种不安全的行为
下面这段代码, 可以很直接的展示 闭包捕获var引用类型变量 的特点:
class JustTest { var num = 0
init(num!: Int64 = 0) { this.num = num }}
func returnAddNum() { var test = JustTest(num: 1) println(test.num) // print: 1
func add() { println(test.num) // print: 1 / 3 test = JustTest(num: 2) println(test.num) // print: 2 } add() println(test.num) // print: 2
test = JustTest(num: 3) println(test.num) // print: 3 add() println(test.num) // print: 2}
main() { let f = returnAddNum()}这段代码执行之后, 将会输出:
11223322结果表明, 如果闭包捕获var引用类型变量, 那么 闭包内外修改变量指向, 都会互相影响, 因为这就是同一个引用类型变量
所以, 对捕获了var变量(包括值类型和引用类型)的闭包, 仓颉规定禁止将其当作一等公民使用
需要注意的是, 捕获具有传递性
如果一个函数
f调用了捕获var变量的函数g, 且g捕获的var变量不在函数f内定义, 那么函数f同样捕获了var变量, 此时,f也不能作为一等公民使用以下示例中,
g捕获了var声明的变量x,f调用了g, 且g捕获的x不在f内定义,f同样不能作为一等公民使用:func h(){var x = 1func g() { x } // 捕获一个可变变量func f() {g() // 调用 g}return f // Error}以下示例中,
g捕获了var声明的变量x,f调用了g但
g捕获的x在f内定义,f没有捕获其他var声明的变量, 因此,f仍作为一等公民使用:func h(){func f() {var x = 1func g() { x } // 捕获一个可变变量g()}return f // Ok}
仓颉中, 变量的捕获具有传递性, 即 如果g()捕获了a, 且在f()内调用了g(), 则视为f()也捕获了a
此时, 如果a是var变量, 那么g()和f()都禁止作为一等公民使用
静态成员变量和全局变量的访问, 不属于变量捕获, 因此访问了
var修饰的全局变量、静态成员变量的函数或lambda仍可作为一等公民使用class C {static public var a: Int32 = 0static public func foo() {a++ // OKreturn a}}var globalV1 = 0func countGlobalV1() {globalV1++C.a = 99let g = C.foo // OK}func g(){let f = countGlobalV1 // OKf()}