16、Rust 函数

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

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

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

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

划分代码到函数中

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

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

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

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

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

函数定义

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

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

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

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

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

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

定义函数的语法

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

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

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

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

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

// 函数定义
fn fn_hello(){
   println!("hello from function fn_hello ");
}

函数调用

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

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

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

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

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

函数调用的语法格式

function_name(val1,val2,valN)

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

fn main(){
   //调用函数
   fn_hello();
}

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

范例

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

fn main(){
   // 调用函数
   fn_hello();
}

// 定义函数
fn fn_hello(){
   println!("hello from function fn_hello ");
}

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

hello from function fn_hello

函数返回值

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

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

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

函数返回值语法格式

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

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

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

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

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

1. 有 return 语句

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

   // 返回一个值
   return value;
}

2. 没有 return 语句则使用最后一条语句的结果作为返回值

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

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

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

范例

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

fn main(){
   println!("pi value is {}",get_pi());
   println!("pi value is {}",get_pi2());
}

fn get_pi()->f64 {
   22.0/7.0
}

fn get_pi2()->f64 {
   return 22.0/7.0;
}

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

pi value is 3.142857142857143
pi value is 3.142857142857143

范例 2

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

fn main(){
   println!("pi value is {}",get_pi());
}

fn get_pi()->f64 {
   22.0/7.0;
}

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

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

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

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

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

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

fn main(){
   let pi = get_pi();
   println!("pi value is {}",pi);
}

fn get_pi()->f64 {
   22.0/7.0
}

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

pi value is 3.142857142857143

函数参数

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

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

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

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

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

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

传值调用

传值调用 就是简单的把传递的变量的值传递给函数的 形参,从某些方面说了,就是把函数参数也赋值为传递的值。

因为是赋值,所以函数参数和传递的变量其实是各自保存了相同的值,互不影响。因此函数内部修改函数参数的值并不会影响外部变量的值。

范例

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

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

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

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

fn main(){
   let no:i32 = 5;
   mutate_no_to_zero(no);
   println!("The value of no is:{}",no);
}

fn mutate_no_to_zero(mut param_no: i32) {
   param_no = param_no*0;
   println!("param_no value is :{}",param_no);
}

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

param_no value is :0
The value of no is:5

引用调用

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

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

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

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

范例

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

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

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

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

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

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

The value of no is 0.

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

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

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

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

fn main(){
   let name:String = String::from("TutorialsPoint");
   display(name); 
   println!("after function name is: {}",name);
}

fn display(param_name:String){
   println!("param_name value is :{}",param_name);
}

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

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

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

fn main(){
   let name:String = String::from("TutorialsPoint");
   display(name);
}

fn display(param_name:String){
   println!("param_name value is :{}",param_name);
}

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

param_name value is :TutorialsPoint

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