跳转至

11-函数

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

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

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

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

11.1 划分代码到函数中

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

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

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

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

11.2 函数定义

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

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

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

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

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

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

11.2.1 定义函数的语法

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

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

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

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

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

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

11.3 函数调用

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

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

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

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

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

11.3.1 函数调用的语法格式

function_name(val1,val2,valN)

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

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

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

11.3.2 范例

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

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

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

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

hello from function fn_hello

11.4 函数返回值

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

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

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

11.4.1 函数返回值语法格式

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

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

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

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

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

  1. 有 return 语句
fn function_name() -> return_type {
    // 其它代码

    // 返回一个值
    return value;
}
  1. 没有 return 语句则使用最后一条语句的结果作为返回值

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

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

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

11.4.2 范例1

下面的代码,我们定义了两个相同功能的 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

11.4.3 范例 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 `()`

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

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

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

例如下面的代码,我们定义了变量 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

11.5 函数参数

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

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

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

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

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

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

11.5.1 传值调用

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

11.5.2 范例

我们定义了一个函数 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

11.5.3 引用调用

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

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

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

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

11.5.4 范例

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

我们定义了一个函数 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 指向的内存位置上存储的变量的值。这种操作也称为 解引用。 因此 星号(*) 也称为 解引用操作符。

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

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

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

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 

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