NOTE阅读文档版本:
语言规约 Cangjie-0.53.18-Spec
具体开发指南 Cangjie-LTS-1.0.3
在阅读 了解仓颉的语言规约时, 难免会涉及到一些仓颉的示例代码, 但 我们对仓颉并不熟悉, 所以可以用 仓颉在线体验 快速验证
有条件当然可以直接 配置Canjie-SDK
WARNING博主在此之前, 基本只接触过C/C++语言, 对大多现代语言都没有了解, 所以在阅读过程中遇到相似的概念, 难免会与C/C++中的相似概念作类比, 见谅
此样式内容, 表示文档原文内容
函数
函数是一段完成特定任务的独立代码片段, 可以通过函数名字来标识, 这个名字可以被用来调用函数
仓颉编程语言中函数是一等公民, 函数可以赋值给变量, 或作为参数传递, 或作为返回值返回
Lambda表达式
一个
Lambda表达式是一个匿名的函数
Lambda表达式的语法如下:lambdaExpression: '{' NL* lambdaParameters? '=>' NL* expressionOrDeclarations '}';lambdaParameters: lambdaParameter (',' lambdaParameter)* ','?;lambdaParameter: (identifier | '_') (':' type)?;
lambda表达式有两种形式, 一种是有形参的{a: Int64 => e1; e2 }另一种是无形参的
{ => e1; e2 }(e1 和 e2 是表达式或声明序列)let f1: (Int64, Int64)->Int64 = {a: Int64, b: Int64 => a + b}var f2: () -> Int32 = { => 123 }
现代语言中基本都存在lambda表达式
仓颉中的lambda表达式的书写并不复杂:
{ 形参列表 => 表达式序列 }仓颉的lambda表达式, 语法规定 形参列表不用()包裹, 执行的语句序列也不用{}包裹
对于有形参的
lambda表达式, 可以使用一个_代替一个形参,_代表在lambda的函数体中不会使用 到的参数let f2 = { n: Int64, _: Int64 => return n * n }let f3: (Int32, Int32) -> Int32 = { n, _ => return n * n }let f4: (String) -> String = { _ => return "Hello" }
Lambda表达式中不支持声明返回类型若上下文明确指定了
lambda表达式的返回类型, 则其返回类型为上下文指定的类型如果指定的返回类型是
Unit, 则仓颉编译器还会在lambda表达式的函数体中所有可能返回的地方插入return (), 使其总是返回Unit类型指定了
Unit返回类型的示例如下:func column(c: (Data) -> Unit) {...}func row(r: (Data) -> Unit) {...}func build(): Unit {column { _ =>row { _ =>buildDetail()buildCalendar()} // OK. 由于插入了'return', 所以类型正确width(750)height(700)backgroundColor("#ff41444b")} // OK. 由于插入了'return', 所以类型正确}若不存在这样的上下文, 则
=>右侧的表达式的类型会被视为lambda表达式的返回类型同函数一样, 若无法推导出返回类型, 则会编译报错
仓颉中的lambda表达式不支持声明返回值类型
如果lambda表达式的上下文中指定了返回值类型, 那么编译器也会在lambda表达式的可能返回的位置插入return表达式, 这一点和函数是一样的
如果不存在相关的上下文, 那么就将=>右侧的表达式类型当作lambda表达式的返回类型, 这里的右侧 是指lambda表达式会执行的完整的表达式序列的类型
Lambda表达式中参数的类型标注可缺省, 编译器会尝试从上下文进行类型推断, 当编译器无法推断出类型时会编译报错var sum1: (Int32, Int32) -> Int32 = {a, b => a + b}var sum2: (Int32, Int32) -> Int32 = {a: Int32, b => a + b}var display = { => print("Hello") }var a = { => return 1 }
=>右侧的内容与普通函数体的规则一样, 同样可以省略return若
=>的右侧为空, 返回值为()sum1 = {a, b => a + b}sum2 = {a, b => return a + b} // 与前面的一行一致
Lambda表达式支持原地调用, 例如:let r1 = {a: Int64, b: Int64 => a + b}(1, 2) // r1 = 3let r2 = { => 123 }() // r2 = 123
仓颉中lambda表达式的原地调用, 类似将整个lambda表达式当作函数名, 直接加上(实参列表)就可以调用
闭包
闭包指的是自包含的函数或
lambda闭包可以从定义它的静态作用域中捕获变量, 即使对闭包调用不在定义的作用域, 仍可访问其捕获的变量
变量捕获发生在闭包定义时
自包含, 表示自己内部包含有 部分或全部 执行所需的数据, 不需要完全依赖调用时传入的参数执行
当一个函数或lambda被定义时, 如果你在函数体或lambda执行体中引用了 在定义位置所在静态作用域中 能够访问到的变量, 这些变量会被自动捕获到闭包中
被捕获的变量, 会被包含到函数或lambda中, 此时形成闭包
其实可以类比C++中的lambda表达式的手动捕获可见变量功能, 不过C++需要手动捕获
不是所有闭包内的变量访问都称为捕获, 以下情形的变量访问不是捕获:
对定义在本函数或本
lambda内的局部变量的访问;对本函数或本
lambda的形参的访问;全局变量和静态成员变量在函数或
lambda中的访问;实例成员变量在实例成员函数中的访问
由于实例成员函数将
this作为参数传入, 在实例成员函数内通过this访问所有实例成员变量关于变量的捕获, 可以分为以下两种情形:
捕获
let声明的变量:在闭包中不能修改这些变量的值
如果捕获的变量是引用类型, 可修改其可变实例成员变量的值
仅捕获了
let声明的局部变量的函数或lambda可以作为一等公民使用, 即可以赋值给变量, 可以作为实参或返回值使用, 可以作为表达式使用捕获
var声明的变量:捕获了可变变量的函数或
lambda只能被调用, 不能作为一等公民使用, 包括不能赋值给变量, 不能作为实参或返回值使用, 不能作为表达式使用需要注意的是, 捕获具有传递性, 如果一个函数
f调用了捕获var变量的函数g, 且存在g捕获的var变量不在函数f内定义, 那么函数f同样捕获了var变量, 此时,f也不能作为一等公民使用以下示例中,
h仅捕获了let声明的变量y,h可以作为一等公民使用func f(){let y = 2func h() {print(y) // OK, 捕获了不可变变量}let d = h // OK, h 可以被赋值到变量return h // OK, h 可以被当做返回值}以下示例中,
g捕获了var声明的变量x,g不可以作为一等公民使用, 仅能被调用func f() {var x = 1func g() {print(x) // OK, 捕获了一个可变变量}let b = g // Error, g 不能被赋值到变量g // Error, g 不能用作表达式g() // OK, g 可以被调用// Lambda 捕获了一个可变变量, 不能赋值给变量let e = { => print("${x}") } // Errorlet i = { => x * x }() // OK, Lambda 捕获了一个可变变量, 可以被调用return g // Error, g 不能用作返回值}以下示例中,
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}访问了
var修饰的全局变量、静态成员变量、实例成员变量的函数或lambda仍可作为一等公民使用class C {static var a: Int32 = 0static func foo() {a++ // OKreturn a}}var globalV1 = 0func countGlobalV1() {globalV1++C.a = 99let g = C.foo // OK}func g(){let f = countGlobalV1 // OKf()}
对全局变量、静态变量的访问不被看作捕获
从文档介绍来看, 如果暂时先忽略捕获的传递性, 那么 只要函数或lambda捕获了var变量, 那么此闭包就只能调用, 而不能被当作一等公民使用
捕获的变量必须满足以下规则:
被捕获的变量必须在闭包定义时可见, 否则编译报错
变量被捕获时必须已经完成初始化, 否则编译报错
如果函数外有变量, 同时函数内有同名的局部变量, 函数内的闭包因局部变量的作用域未开始而捕获了函数外的变量时, 为避免用户误用, 报 warning
// 1. 捕获的变量必须在闭包之前定义let x = 4func f() {print("${x}") // Print 4.let x = 99func f1() {print("${x}")}let f2 = { =>print("${x}")}f1() // Print 99f2() // Print 99}// 2. 变量在被捕获之前必须被初始化let x = 4func f() {print("${x}") // Print 4let x: Int64func f1() {print("${x}") // Error: x 还没有初始化}x = 99f1()}// 3. 如果在代码块中存在局部变量, 则 闭包捕获外部作用域中同名变量 时将警告let x = 4func f() {print("${x}") // Print 4func f1() {print("${x}") // warning}let f2 = { =>print("${x}") // warning}let x = 99f1() // print 4f2() // print 4}
函数重载
函数重载定义
在仓颉编程语言中, 一个作用域中可见的同一个函数名对应的函数定义 不构成重定义时 便构成重载
函数存在重载时, 在进行函数调用时 需要根据函数调用表达式中的实参的类型和上下文信息 明确是哪一个函数定义被使用
被重载的多个函数必须遵循以下规则:
函数名必须相同, 且满足以下情况之一:
通过
func关键字引入的函数名在一个
class或struct内定义的构造函数, 包括主构造函数和init构造函数参数类型必须不同, 即满足以下情况之一:
函数参数数量不同
函数参数数量相同, 但是对应位置的参数类型不同
必须在同一个作用域中可见
注意, 参数类型不受”是否是命名形参”、“是否有默认值”影响; 且讨论参数类型时, 一个类型与它的
alias被视为相同的类型例如下面例子中的四个函数的参数类型是完全相同的:
type Boolean = Boolfunc f(a : Bool) {}func f(a! : Bool) {}func f(a! : Bool = false) {}func f(a! : Boolean) {}
仓颉的函数重载基本与C++中的函数重载定义一致
-
函数名相同
-
参数类型或参数数量存在一定差异
示例一: 定义源文件顶层的 2 个函数的参数类型不同, f构成了重载
// f 构成重载func f() {...}func f(a: Int32) {...}示例二: 接口I、类C1、C2中的函数f1构成了重载
interface I {func f1() {...}}open class C1 {func f1(a: Int32) {...}}class C2 <: C1 & I {// f1 构成重载func f1(a: Int32, b: String) {...}}示例三: 类型内的构造函数之间构成重载
class C {var name: String = "abc"// 构造函数构成重载init(){print(name)}init(name: String){this.name = name}}示例四: 如下示例, 函数参数数量相同, 相同位置的类型相同, 仅参数类型中包含的类型变元的约束不同, 不构成重载
interface I1{}interface I2{}func f<T>(a: T) where T <: I1 {}func f<T>(a: T) where T <: I2 {} // Error, 不构成重载
mut函数
mut函数是一种特殊的实例成员函数在
struct类型的mut成员函数中, 可以通过this修改成员变量, 而在struct类型的非mut成员函数中, 不可以通过this修改成员变量
仓颉中, strcut的普通成员函数是不能通过this修改成员变量的
仓颉中, 可以用来mut修饰成员函数, 使成员函数可以通过this修改成员变量
定义
mut函数使用mut关键字修饰, 只允许在interface、struct和struct扩展中定义, 并且只能作用于实例成员函数(不支持静态成员函数)struct A {mut func f(): Unit {} // okmut static func g(): Unit {} // errormut operator func +(rhs: A): A { // okreturn A()}}extend A {mut func h(): Unit {} // ok}class B {mut func f(): Unit {} // error}interface I {mut func f(): Unit // ok}
mut只能用到interface、struct和struct扩展的非静态成员函数中, 无法用在class中
在
mut函数中可以对struct实例的字段进行赋值, 这些赋值会修改实例并立刻生效与实例成员函数相同,
this不是必须的, 可以由编译器推断struct Foo {var i = 0mut func f() {this.i += 1 // oki += 1 // ok}}main() {var a = Foo()print(a.i) // 0a.f()print(a.i) // 2a.f()print(a.i) // 4return 0}
mut函数中的this具有额外的限制:
不能被捕获(意味着当前实例的字段也不能被捕获)
不能作为表达式
struct Foo {var i = 0mut func f(): Foo {let f1 = { => this } // errorlet f2 = { => this.i = 2 } // errorlet f3 = { => this.i } // errorlet f4 = { => i } // errorreturn this // error}}
mut函数的作用是 mut函数可以通过this修改成员变量
因为, 仓颉中, struct的普通成员函数是无法修改成员变量的
不过, 仓颉中的mut函数内的this不能当作表达式, 即不能被返回、赋值给其他变量等
至于原因, 应该是为了防止出现this暴露, 导致其他地方能够通过this访问成员变量?
毕竟struct是值类型的
接口中的mut函数
struct类型在实现interface的函数时 必须保持一样的mut修饰
struct以外的类型实现interface的函数时禁止使用mut修饰interface I {mut func f1(): Unitfunc f2(): Unit}struct A <: I {public mut func f1(): Unit {} // okpublic func f2(): Unit {} // ok}struct B <: I {public func f1(): Unit {} // errorpublic mut func f2(): Unit {} // error}class C <: I {public func f1(): Unit {} // okpublic func f2(): Unit {} // ok}需要注意的是, 当
struct的实例赋值给interface类型时 是拷贝语义, 因此interface的mut函数并不能修改原本struct实例的值interface I {mut func f(): Unit}struct Foo <: I {var v = 0public mut func f(): Unit {v += 1}}main() {var a = Foo()var b: I = ab.f()print(a.v) // 0return 0}
继承自interface的struct类型, struct实例赋值给interface类型, 是拷贝语义
访问规则
如果一个变量使用
let声明, 并且类型可能是struct(包含静态类型是struct类型, 或者类型变元可能是struct类型), 那么这个变量不能访问该类型使用mut修饰的函数其它情况均允许访问
interface I {mut func f(): Unit}struct Foo <: I {var i = 0public mut func f(): Unit {i += 1}}class Bar <: I {var i = 0public func f(): Unit {i += 1}}main() {let a = Foo()a.f() // errorvar b = Foo()b.f() // oklet c: I = Foo()c.f() // okreturn 0}func g1<T>(v: T): Unit where T <: I {v.f() // error}func g2<T>(v: T): Unit where T <: Bar & I {v.f() // ok}
仓颉中, let修饰的变量, 只要静态类型可能是strcut, 就禁止调用mut函数
如果一个变量的类型可能是
struct(包含静态类型是struct类型, 或者类型变元可能是struct类型), 那么这个变量不能 将该类型使用mut修饰的函数被作为 高阶函数使用, 只能调用这些mut函数interface I {mut func f(): Unit}struct Foo <: I {var i = 0public mut func f(): Unit {i += 1}}class Bar <: I {var i = 0public func f(): Unit {i += 1}}main() {var a = Foo()var fn = a.f // errorvar b: I = Foo()fn = b.f // okreturn 0}func g1<T>(v: T): Unit where T <: I {let fn = v.f // error}func g2<T>(v: T): Unit where T <: Bar & I {let fn = v.f // ok}
仓颉中, struct类型实例的mut函数, 不能被当作高阶函数使用
但文档中给出的例子, 并不是mut函数被当作高阶函数使用, 而是尝试当作第一公民使用
非
mut的实例成员函数(包括lambda表达式)不能访问this的mut函数, 反之可以struct Foo {var i = 0mut func f(): Unit {i += 1g() // ok}func g(): Unit {f() // error}}interface I {mut func f(): Unit {g() // ok}func g(): Unit {f() // error}}
仓颉中, 非mut函数不能访问mut函数
仓颉中, struct的普通成员函数是不能修改成员变量的, 但是是可以访问成员变量的
这表示, struct的this默认是不可变的
根本的原因是, 仓颉中的struct被设计为值类型, 如果struct实例被let修饰, 因为是值类型实例, 那么这个实例的成员应该是默认禁止被修改的
如果this是可变的, 那么即使是被let修饰的实例, 通过普通的成员函数就可能会出现, 普通成员函数能够修改成员变量的情况
这不符合struct的设计哲学