十一、Rust 函数 fn

函数是一组一起执行一个任务的语句块。

函数是一段可读,可维护和可重用代码的多条语句。

每个 Rust 程序都至少有一个函数,即主函数 main()。

除了使用 Rust 核心和标准库提供的函数外,我们还可以自己定义自己的函数。

11.1 划分代码到函数中

我们可以把代码划分到不同的函数中,这样可以使得代码可读性更强,逻辑更简单。

虽然划分代码到不同的函数中没有一个统一的规范,但实践证明,在逻辑上,划分的标准是每个函数执行一个特定的任务的

函数声明就是告诉编译器一个函数的名称、变量、和返回值类型。这三个合在一起组成了函数的签名,函数签名的作用就是防止出现两个相同的函数。

函数定义,就是提供了函数的具体实现

术语 说明
函数定义 函数定义其实就是定义一个任务要以什么方式来完成
函数调用 函数只有被调用才会运行起来
函数返回值 函数在执行完成后可以返回一个值给它的调用者
函数参数 函数参数用于携带外部的值给函数内部的代码

11.2 函数定义

函数定义其实就是定义一个任务要以什么方式来完成。

因此,定义函数时首先想的并不是我要定义一个函数,而是我这个任务要怎么做,要定义哪些函数来完成。

函数也不会凭空出现的,在使用一个函数前,我们必须先定义它。

定义函数时必须以 fn 关键字开头,fn 关键字是 function 的缩写。

函数内部必须包含函数要执行的具体代码,我们把这些代码称之为 函数体

函数名称的命名规则和变量的命名规则一致。

11.2.1 定义函数的语法

定义函数的语法如下,定义函数时必须使用 fn 关键字开头,后面跟着要定义的函数名。

  1. fn function_name([param1:data_type1,param2..paramN]) {
  2. // 函数代码
  3. }

参数用于将值传递给函数内部的语句。函数定义时可以自由选择包含参数与否。

11.2.2 范例:定义一个简单的函数

下面的代码,我们定义了一个函数名为 fn_hello 的函数,用于输出一些信息

  1. // 函数定义
  2. fn fn_hello(){
  3. println!("hello from function fn_hello ");
  4. }

11.3 函数调用

为了运行一个函数首先必须调用它。函数不像普通的语句,写完了会自动执行,函数需要调用才会被执行。否则看起来就像是多余的没有用的代码。

让函数运行起来的过程我们称之为 函数调用。

如果函数定义了参数,那么在 函数调用 时必须传递指定类型的参数。

在一个函数 fn1 内部调用其它函数 fn2,那么这个 fn1 函数就称为 调用者函数,简称为 调用者。

调用者函数有点拗口,我们一般都称呼为 函数调用者。

11.3.1 函数调用的语法格式

  1. function_name(val1,val2,valN)

例如我们在 main() 函数内部调用函数 fn_hello()

  1. fn main(){
  2. //调用函数
  3. fn_hello();
  4. }

这时候,函数 main() 就是 调用者函数,也就是 调用者。

11.3.2 范例

下面的代码,我们定义了一个函数 fn_hello() 用于输出一些信息。 然后我们在 main() 函数对 fn_hello() 进行调用

  1. fn main(){
  2. // 调用函数
  3. fn_hello();
  4. }
  5. // 定义函数
  6. fn fn_hello(){
  7. println!("hello from function fn_hello ");
  8. }

编译运行以上 Rust 代码,输出结果如下

  1. hello from function fn_hello

11.4 函数返回值

函数可以返回值给它的调用者。我们将这些值称为 函数返回值。

也就是说,函数在代码执行完成后,除了将控制权还给调用者之外,还可以携带值给它的调用者。

如果一个函数需要返回值给它的调用者,那么我们在函数定义时就需要明确中函数能够返回什么类型的值。

11.4.1 函数返回值语法格式

Rust 语言的返回值定义语法与其它语言有所不同,它是通过在 小括号后面使用 箭头 ( -> ) 加上数据类型 来定义的。

同时在函数的代码中,可以使用 return 关键字指定要返回的值。

如果函数代码中没有使用 return 关键字,那么函数会默认使用最后一条语句的执行结果作为返回值。

因此,千万注意,return 中返回的值或最后一条语句的执行结果 必须和函数定义时的返回数据类型一样,不然会编译会出错

具有返回值的函数的完整定义语法如下

  1. 有 return 语句
  1. function function_name() -> return_type {
  2. // 其它代码
  3. // 返回一个值
  4. return value;
  5. }
  1. 没有 return 语句则使用最后一条语句的结果作为返回值

函数中最后用于返回值的语句不能有 分号 ; 结尾,否则就不会时返回值了。

function function_name() -> return_type { // 其它代码

value // 没有分号则表示返回值 }

11.4.2 范例1

下面的代码,我们定义了两个相同功能的 get_pi() 和 get_pi2() 函数,一个使用 return 语句返回值,另一个则使用没有分号的最后一条语句作为返回值。

  1. fn main(){
  2. println!("pi value is {}",get_pi());
  3. println!("pi value is {}",get_pi2());
  4. }
  5. fn get_pi()->f64 {
  6. 22.0/7.0
  7. }
  8. fn get_pi2()->f64 {
  9. return 22.0/7.0;
  10. }

编译运行以上 Rust 代码,输出结果如下

  1. pi value is 3.142857142857143
  2. pi value is 3.142857142857143

11.4.3 范例 2

我们修改下上面的代码,在没有 return 的那个函数的最后一条语句添加一个分号,看看执行结果如何

  1. fn main(){
  2. println!("pi value is {}",get_pi());
  3. }
  4. fn get_pi()->f64 {
  5. 22.0/7.0;
  6. }

编译运行上面的代码,会报错,错误信息如下

  1. error[E0308]: mismatched types
  2. --> src/main.rs:5:14
  3. |
  4. 5 | fn get_pi()->f64 {
  5. | ------ ^^^ expected f64, found ()
  6. | |
  7. | this function's body doesn't return
  8. 6 | 22.0/7.0;
  9. | - help: consider removing this semicolon
  10. |
  11. = note: expected type `f64`
  12. found type `()`

从错误信息中可以看出,函数定义了返回值,但我们却没有返回值。也就是说,函数的返回值必须没有 分号 结尾。

11.4.4 范例3: 函数返回值接收变量

我们还可以将函数的返回值赋值给一个变量。

例如下面的代码,我们定义了变量 pi 来接收函数的返回值

  1. fn main(){
  2. let pi = get_pi();
  3. println!("pi value is {}",pi);
  4. }
  5. fn get_pi()->f64 {
  6. 22.0/7.0
  7. }

编译运行以上 Rust 代码,输出结果如下

  1. pi value is 3.142857142857143

11.5 函数参数

函数参数 是一种将外部变量和值带给函数内部代码的一种机制。函数参数是函数签名的一部分。

函数签名的最大作用,就是防止定义两个相同的签名的函数。

当一个函数定义了函数参数,那么在调用该函数的之后就可以把变量/值传递给函数。

我们把函数定义时指定的参数名叫做 形参。同时把调用函数时传递给函数的变量/值叫做 实参

除非特别指定,函数调用时传递的 实参 数量和类型必须与 形参 数量和类型必须相同。 否则会编译错误。

函数参数有两种传值方法,一种是把值直接传递给函数,另一种是把值在内存上的保存位置传递给函数。

11.5.1 传值调用

传值调用 就是简单的把传递的变量的值传递给函数的 形参,从某些方面说了,就是把函数参数也赋值为传递的值。 因为是赋值,所以函数参数和传递的变量其实是各自保存了相同的值,互不影响。因此函数内部修改函数参数的值并不会影响外部变量的值。

11.5.2 范例

我们定义了一个函数 mutate_no_to_zero(),它接受一个参数并将参数重新赋值为 0 。

我们还定义了一个变量 no 并初始化它的值为 5。然后将该变量传递给函数 mutate_no_to_zero()。

虽然我们在函数中将变量的值改成了 0,但当调用完成后,我们的 no 变量的值仍然是 5。

这是因为传值调用传递的是变量的一个副本,也就是重新创建了一个变量。

  1. fn main(){
  2. let no:i32 = 5;
  3. mutate_no_to_zero(no);
  4. println!("The value of no is:{}",no);
  5. }
  6. fn mutate_no_to_zero(mut param_no: i32) {
  7. param_no = param_no*0;
  8. println!("param_no value is :{}",param_no);
  9. }

编译运行以上 Rust 代码,输出结果如下

  1. param_no value is :0
  2. The value of no is:5

11.5.3 引用调用

值传递变量导致重新创建一个变量。但引用传递则不会,引用传递把当前变量的内存位置传递给函数。

对于引用传递来说,传递的变量和函数参数都共同指向了同一个内存位置。

引用传递需要函数定义时在参数类型的前面加上 & 符号,语法格式如下

  1. fn function_name(parameter: &data_type) {
  2. // 函数的具体代码
  3. }

11.5.4 范例

我们对刚刚传值调用的代码做一些修改。

我们定义了一个函数 mutate_no_to_zero(),它接受一个可变引用作为参数,并把传递的引用变量重新赋值为 0 。

同时定义了一个 可变变量 no 并初始化它的值为 5。然后将该变量的一个引用传递给函数 mutate_no_to_zero()。

当函数执行完成后,可变变量 no 的值就变成 0 了。

  1. fn main() {
  2. let mut no:i32 = 5;
  3. mutate_no_to_zero(&mut no);
  4. println!("The value of no is:{}",no);
  5. }
  6. fn mutate_no_to_zero(param_no:&mut i32){
  7. *param_no = 0; //解引用操作
  8. }

编译运行以上 Rust 代码,输出结果如下

  1. The value of no is 0.

上面的代码中,星号 ( ) 用于访问变量 param_no 指向的内存位置上存储的变量的值。这种操作也称为 解引用。 因此 星号() 也称为 解引用操作符。

11.6 传递复合类型给函数做参数

对于复合类型,比如字符串,如果按照普通的方法传递给函数后,那么该变量将不可再访问

例如下面的代码编译会报错

  1. fn main(){
  2. let name:String = String::from("TutorialsPoint");
  3. display(name);
  4. println!("after function name is: {}",name);
  5. }
  6. fn display(param_name:String){
  7. println!("param_name value is :{}",param_name);
  8. }

编译上面的代码会出错,错误信息如下

  1. error[E0382]: borrow of moved value: `name`
  2. --> src/main.rs:4:42
  3. |
  4. 2 | let name:String = String::from("TutorialsPoint");
  5. | ---- move occurs because `name` has type `std::string::String`, which does not implement the `Copy` trait
  6. 3 | display(name);
  7. | ---- value moved here
  8. 4 | println!("after function name is: {}",name);
  9. | ^^^^ value borrowed here after move

修复的方法之一就是去掉后面的 println!() 语句

  1. fn main(){
  2. let name:String = String::from("TutorialsPoint");
  3. display(name);
  4. }
  5. fn display(param_name:String){
  6. println!("param_name value is :{}",param_name);
  7. }

编译运行以上 Rust 代码,输出结果如下

  1. param_name value is :TutorialsPoint
  2. `

后面的章节,我们会讨论如何解决这个问题,本章节这不是重点。