函数
函数在 Cairo 代码中非常普遍。你已经见过语言中最重要的函数之一:
main
函数,它是很多程序的入口点。
你也见过 fn
关键字,它用来声明新函数。
Cairo代码使用 蛇形命名法( snake case ) 作为函数和变量名称的常规样式。 所有的字母都是小写的,并以下划线分隔单词。 这里有一个程序,包含一个函数定义的例子:
use debug::PrintTrait; fn another_function() { 'Another function.'.print(); } fn main() { 'Hello, world!'.print(); another_function(); }
在 Cairo 中,我们通过输入 fn
和函数名称以及一组括号来定义一个函数。大括号告诉编译器函数体的开始和结束位置。
可以使用函数名后跟圆括号来调用我们定义过的任意函数。
因为程序中已定义 another_function
函数,所以可以在 main
函数中调用它。
注意,源码中 another_function
定义在 main
函数 之前 ;当然我们也可以定义在其之后。
Cairo 不关心函数定义所在的位置,只要函数被调用时出现在调用之处可见的作用域内就行。
让我们用 Scarb 启动一个名为 functions 的新项目来进一步探索函数。
更进一步。把 another_function
的例子写入 src/lib.cairo 中并运行它。你应该看到以下输出:
$ scarb cairo-run
[DEBUG] Hello, world! (raw: 5735816763073854953388147237921)
[DEBUG] Another function. (raw: 22265147635379277118623944509513687592494)
这几行是按照它们在 main
函数中出现的顺序执行的。
首先打印 "Hello, world!" 信息,然后调用 another_function
,并打印其信息。
参数
我们可以定义为拥有 参数( parameters ) 的函数,参数是特殊变量,是函数签名的一部分。当函数拥有参数(形参)时,可以为这些参数提供具体的值(实参)。 技术上讲,这些具体值被称为 参数( arguments ),但是在日常交流中,人们倾向于不区分使用 parameters 和 arguments 来表示函数定义中的变量或调用函数时传入的具体值。
在这个版本的 another_function
中,我们添加了一个参数:
use debug::PrintTrait; fn main() { another_function(5); } fn another_function(x: felt252) { x.print(); }
尝试运行这个程序;你应该得到以下输出:
$ scarb cairo-run
[DEBUG] (raw: 5)
another_function
的声明有一个名为 x
的参数。
x
的类型被指定为 felt252
。当我们把 5
传给 another_function
时,.print()
函数在控制台中会输出 5
。
在函数签名中,你 必须 声明每个参数的类型。 这是 Cairo 设计中一个经过慎重考虑的决定: 要求在函数定义中提供类型注解,意味着编译器再也不需要你在代码的 其他地方注明类型来指出你的意图。而且,在知道函数需要什么类型后, 编译器就能够给出更有用的错误消息。
当定义多个参数时,用逗号分隔。 像这样:
use debug::PrintTrait; fn main() { another_function(5, 6); } fn another_function(x: felt252, y: felt252) { x.print(); y.print(); }
这个例子创建了一个名为 another_function
的函数,它有两个
参数。第一个参数被命名为 x
,类型是 felt252
。第二个参数被命名为 y
,也是 felt252
类型。然后,该函数打印了 x
的内容,然后打印 y
的内容。
让我们试着运行这段代码。用前面的示例替换当前在你的 functions
项目的 src/lib.cairo 文件中的程序,然后使用 scarb cairo-run
运行它:
$ scarb cairo-run
[DEBUG] (raw: 5)
[DEBUG] (raw: 6)
因为我们在调用函数时,将 5
作为 x
的值,将 6
作为 y
的值。
程序输出包含这些值。
命名参数
在 Cairo 中,命名参数允许您在调用函数时指定参数的名称。这使得函数调用更具可读性和自描述性。
如果你想使用命名参数,你需要指定参数的名称和你想传递给它的值。语法是 parameter_name: value
。如果你传递的变量与参数名称相同,你可以简写为 :parameter_name
,而不是 parameter_name: variable_name
。
下面是一个例子:
fn foo(x: u8, y: u8) {} fn main() { let first_arg = 3; let second_arg = 4; foo(x: first_arg, y: second_arg); let x = 1; let y = 2; foo(:x, :y) }
语句和表达式
函数体由一系列的语句和一个可选的结尾表达式构成。 目前为止,我们提到的函数还不包含结尾表达式, 不过你已经见过作为语句一部分的表达式。因为 Cairo 是一门基于表达式 (expression-based)的语言,这是一个需要理解的(不同于其他语言) 重要区别。其他语言并没有这样的区别,所以让我们看看语句与表达式有什么区别以及这些区别是如何影响函数体的。
- 语句(Statements)是执行一些操作但不返回值的指令。
- 表达式(Expressions)计算并产生一个值。让我们看一些例子。
实际上,我们已经使用过语句和表达式。
使用 let
关键字创建变量并绑定一个值是一个语句。在示例 2-1 中,let y = 6;
; 是一个语句。
fn main() { let y = 6; }
函数定义也是语句,上面整个例子本身就是一个语句。
语句不返回值。因此,不能把 let
语句赋值给另一个变量,比如下面的例子尝试做
的,会产生一个错误:
fn main() {
let x = (let y = 6);
}
当你运行这个程序时,你将得到的错误看起来是这样:
$ scarb cairo-run
error: Missing token TerminalRParen.
--> src/lib.cairo:2:14
let x = (let y = 6);
^
error: Missing token TerminalSemicolon.
--> src/lib.cairo:2:14
let x = (let y = 6);
^
error: Missing token TerminalSemicolon.
--> src/lib.cairo:2:14
let x = (let y = 6);
^
error: Skipped tokens. Expected: statement.
--> src/lib.cairo:2:14
let x = (let y = 6);
语句 let y = 6
没有返回一个值,所以没有任何东西让 x
与之绑定。这与其他语言
中的情况不同,比如说 C 和 Ruby,其中赋值会返回赋值的值。
在这些语言中,你可以写 x = y = 6
,让 x
和 y
都有值 6
;但在 Cairo 中不是这样的。
表达式会计算出一个值,并且你编写的大部分 Cairo 代码将是由表达式组成的。
考虑一个数学运算,比如 5 + 6
,这是一个表达式并计算出值 11
。
表达式可以是语句的一部分:在示例 2-1 中,语句 let y = 6;
中的 6
是一个表达式,它计算出的值是 6
。
函数调用是一个表达式。宏调用也是一个表达式。用大括号创建的一个新的块作用域同样也是一个表达式,例如:
use debug::PrintTrait; fn main() { let y = { let x = 3; x + 1 }; y.print(); }
这个表达式:
let y = {
let x = 3;
x + 1
};
是一个代码块,它的值是 4
。这个值作为 let
语句的一部分被绑定到 y
上。注意
x + 1
这一行在结尾没有分号,与你见过的大部分代码行不同。表达式的结尾没有
分号。如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值。
在接下来学习具有返回值的函数和表达式时要谨记这一点。
具有返回值的函数
函数可以向调用它的代码返回值。我们并不对返回值命名,但要在箭头(->
)后声明它
的类型。在 Cairo 中,函数的返回值等同于函数体最后一个表达式的值。使用
return
关键字和指定值,可从函数中提前返回;但大部分函数隐式的返回最后的表达式。
这是一个有返回值的函数的例子:
use debug::PrintTrait; fn five() -> u32 { 5 } fn main() { let x = five(); x.print(); }
在 five 函数中没有函数调用、宏、甚至没有 let
语句 —— 只有数字 5
。这在
Cairo 中是一个完全有效的函数。注意,也指定了函数返回值的类型,就是
-> u32
。尝试运行代码;输出应该看起来像这样:
$ scarb cairo-run
[DEBUG] (raw: 5)
five
函数的返回值是 5
,所以返回值类型是 u32
。让我们仔细检查一下这段代
码。有两个重要的部分:首先,let x = five();
这一行表明我们使用函数的返回值初
始化一个变量。因为 five
函数返回 5
,这一行与如下代码相同:
let x = 5;
第二,five
函数没有参数并定义了返回值的类型。不过函数体只有单单一个 5
也没
有分号,因为这是一个表达式,我们想要返回它的值。
让我们看看另一个例子:
use debug::PrintTrait; fn main() { let x = plus_one(5); x.print(); } fn plus_one(x: u32) -> u32 { x + 1 }
运行这段代码将打印 [DEBUG] (raw: 6)
。但是如果在包含 x + 1
的行尾放置一个
分号,把它从一个表达式变成一个语句,我们会看到一个错误:
use debug::PrintTrait; fn main() { let x = plus_one(5); x.print(); } fn plus_one(x: u32) -> u32 { x + 1; }
编译这段代码会产生一个错误,如下所示:
error: Unexpected return type. Expected: "core::integer::u32", found: "()".
主要的错误信息 Unexpected return type
揭示了该代码的核心问题。
函数plus_one
的定义说它将返回一个 u32
类型的值,但是语句并不会返回一个值,而是给出了一个 ()
unit 类型。
因此,没有返回值与函数的定义相矛盾,导致了错误发生。