十一、Rust 函数 fn
函数是一组一起执行一个任务的语句块。
函数是一段可读,可维护和可重用代码的多条语句。
每个 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 中返回的值或最后一条语句的执行结果 必须和函数定义时的返回数据类型一样,不然会编译会出错
具有返回值的函数的完整定义语法如下
- 有 return 语句
function function_name() -> return_type {
// 其它代码
// 返回一个值
return value;
}
- 没有 return 语句则使用最后一条语句的结果作为返回值
函数中最后用于返回值的语句不能有 分号 ; 结尾,否则就不会时返回值了。
function 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
`
后面的章节,我们会讨论如何解决这个问题,本章节这不是重点。