4.1 函数
函数是Rust程序的基本构造单位,也是程序执行的基本语法结构。Rust中函数不仅承载了诸如高阶函数、参数模式匹配等函数式编程的特性,还包含了为结构体及其实例实现方法面向对象范式的特性。
除Rust核心库和标准库提供的函数外,执行一个特定任务的代码也可以被封装成函数,使代码更具可读性和复用性。本节会介绍普通函数的定义与使用、函数与方法的区别以及高阶函数的定义与使用。
4.1.1 定义函数
Rust中函数使用fn关键字定义,由函数签名和函数体组合而成。函数签名由函数名、参数和返回值类型组成,主要作用是防止定义两个相同签名的函数。函数名建议使用snake case规范风格,所有字母都是小写并使用下划线分隔单词。参数用于将外部变量或者值传递到函数内部使用,但参数不是必需的,可以置空。同样,返回值类型也不是必需的,如果函数需要返回某个值,应在函数签名中指定返回值类型。函数体被包含于一对大括号之内,是函数要执行的具体代码。
函数需要调用才会被执行,让函数运行起来的过程叫作函数调用。在fn1函数中调用fn2函数,fn1函数就叫作函数调用者。函数定义时指定的参数叫作形参,调用函数时传递给函数的变量或者值叫作实参。当一个函数拥有参数(形参),在调用该函数时必须为这些参数提供具体的值(实参),并且函数调用时传递的实参数量和类型必须与形参的数量和类型一致。
先来看一个简单的函数定义与调用的示例,如代码清单4-1所示。
代码清单4-1 函数定义与调用
1 fn add(x: i32, y: i32) -> i32 { 2 x + y 3 } 4 5 fn main() { 6 let x = 5; 7 let y = { 8 let x = 2; 9 x + 1 10 }; 11 12 let sum = add(x, y); 13 println!("{} + {} = {}", x, y, sum); 14 } 15 16 // 5 + 3 = 8
(1)main函数
main函数是程序的入口点。对于二进制可执行文件来说,main函数是必不可少的。对于库函数来说,main函数不是必需的。
(2)函数体
函数体由一系列语句和一个可选的结尾表达式构成。代码清单4-1中,main函数的函数体内声明了两个变量x和y,调用add函数执行加法计算,并将计算结果作为返回值与变量sum绑定。第7~10行代码赋值语句的右侧是一个代码块,它的返回值是3,这个值与变量y绑定。需要注意的是,第9行代码中“x+1”的结尾没有分号,代表这是一个表达式而非语句,将会自动返回表达式的值。表达式的结尾如果加上分号,它就变成了语句,而语句没有返回值。
(3)函数参数
函数参数是一种特殊变量,它是函数签名的一部分。Rust要求函数参数必须明确指定数据类型,但不能指定默认值。函数参数分为可变和不可变参数,默认是不可变参数。当需要可变操作时,可以使用mut关键字。函数如果有多个参数,可以使用逗号分隔。
代码清单4-1中,第1行代码的add函数有两个i32类型的形参x和y,第12行代码调用add函数时向其传递两个实参,实参x的值是5,实参y的值是3。
(4)返回值
如果函数需要返回值给调用者,在函数定义时就要明确返回值的类型,这可以在函数签名中使用“→”加上数据类型来定义。函数只能有唯一的返回值,如果需要返回多个值,可以使用元组类型。Rust中每个函数都有返回值,即使是没有显式返回值的函数,也会隐式地返回一个单元值()。
大部分情况下,函数隐式地返回函数体最后一个表达式的值。add函数体中最后一个表达式“x+y”的值将默认视为函数的返回值,即第2行代码“x+y”等同于“return x+y;”。但是,对于流程控制结构中的循环或条件判断分支,如果需要提前退出函数并返回指定的值,必须显式地使用return语句来返回。
不管是显式还是隐式,函数体中返回值的类型必须和函数签名中返回值的类型一致,否则将会导致程序错误。
4.1.2 方法和函数
方法来自面向对象的编程范式,它表示某个类型实例的行为。方法与函数类似,使用fn关键字定义,可以有参数、返回值和函数体。2.3.3节介绍过关于结构体的知识,这里以结构体为例介绍结构体的方法和关联函数。
结构体的方法必须在结构体的上下文中定义,也就是定义在impl块中。需要注意的是,定义在impl块中的不一定是方法,有可能是关联函数。方法要求第一个参数必须是self,它代表调用该方法的结构体实例。在方法中使用&self能够读取实例中的数据,使用&mut self能够向实例中写入数据。使用方法替代函数的最大好处在于组织性,应该将结构体实例所有的行为都一起放入impl块中。
关联函数是指在impl块中定义,但又不以self作为参数的函数。它与结构体相关联,但不直接作用于结构体实例,常用作返回一个结构体实例的构造函数。
代码清单4-2中,结构体Student的impl块中定义了get_name、set_name、get_score、set_score这4个方法和new关联函数。方法的第一个参数都是self,在方法内部可以使用“self.字段名”语法来访问结构体的字段,在方法外部可使用“实例名.方法名”语法调用方法。调用关联函数可使用“结构体名::关联函数名”语法。在调用结构体方法时,第一个参数self不需要传递实参,这个参数的传递是由Rust编译器完成的。因为在impl Student上下文,Rust知道self的类型是结构体Student,即&self等价于student: &Student。
代码清单4-2 结构体方法和关联函数
1 #[derive(Debug, PartialEq)] 2 pub struct Student { 3 name: &'static str, 4 score: i32, 5 } 6 7 impl Student { 8 pub fn new(name: &'static str, score: i32) -> Self { 9 Student { name, score } 10 } 11 12 pub fn get_name(&self) -> &str { 13 self.name 14 } 15 16 pub fn set_name(&mut self, name: &'static str) { 17 self.name = name; 18 } 19 20 pub fn get_score(&self) -> i32 { 21 self.score 22 } 23 24 pub fn set_score(&mut self, score: i32) { 25 self.score = score; 26 } 27 } 28 29 fn main() { 30 let mut student: Student = Student::new("zhangsan", 59); 31 println!("name: {}, score: {}", student.get_name(), student.get_score()); 32 33 student.set_score(60); 34 println!("{:?}", student); 35 } 36 37 // name: zhangsan, score: 59 38 // Student { name: "zhangsan", score: 60 }
4.1.3 高阶函数
高阶函数是指以函数为参数或返回值的函数,是函数式编程语言最基础的特性。函数是一种类型,函数类型的变量可以像其他类型的变量一样使用,既可以被直接调用执行,也可以作为其他函数的参数或返回值。实现这一切的基础是函数指针,函数指针类型使用fn()来指定。
1. 函数指针
函数指针是指向函数的指针,其值是函数的地址。代码清单4-3中,第6行代码声明了一个函数指针fn_ptr,在声明中必须显式指定函数指针类型fn()。需要注意的是,等号右侧使用的是函数名hello,而不是调用函数hello。第7行代码打印出fn_ptr的指针地址,证明其是一个函数指针。
代码清单4-3 函数指针
1 fn hello() { 2 println!("hello function pointer!"); 3 } 4 5 fn main() { 6 let fn_ptr: fn() = hello; 7 println!("{:p}", fn_ptr); 8 9 let other_fn = hello; 10 // println!("{:p}", other_fn); 11 12 fn_ptr(); 13 other_fn(); 14 } 15 16 // 0x1000f1020 17 // hello function pointer! 18 // hello function pointer!
第9行代码变量other_fn声明中没有指定函数指针类型fn(),如果取消第10行的注释,编译代码会得到如下错误提示。
error[E0277]: the trait bound `fn() {hello}: std::fmt::Pointer` is not satisfied --> src/main.rs:10:22 | 10 | println!("{:p}", other_fn); | ^^^^^^^^ the trait `std::fmt::Pointer` is not implemented for `fn() {hello}` | = note: required by `std::fmt::Pointer::fmt`
根据错误信息可知,other_fn的类型实际上是fn() {hello},这是函数hello本身的类型,而非函数指针类型。但是,从第12、13行代码可以看到,不管是函数指针类型,还是函数hello本身的类型,都可以直接进行调用。
2. 函数作参数
函数作为参数时,为了提升代码可读性,可以使用type关键字为函数指针类型定义别名。
代码清单4-4中,第1行代码使用type关键字为函数指针类型fn(i32, i32) -> i32定义了别名MathOp。第2行代码中math函数的第一个参数op的类型是MathOp,因此math是高阶函数。main函数中调用math函数,并传入add或subtract函数作为实参,它们会自动转换成函数指针类型。
代码清单4-4 函数作参数
1 type MathOp = fn(i32, i32) -> i32; 2 fn math(op: MathOp, x: i32, y: i32) -> i32 { 3 println!("{:p}", op); 4 op(x, y) 5 } 6 7 fn add(x: i32, y: i32) -> i32 { 8 x + y 9 } 10 11 fn subtract(x: i32, y: i32) -> i32 { 12 x - y 13 } 14 15 fn main() { 16 let (x, y) = (8, 3); 17 println!("add operation result: {}", math(add, x, y)); 18 println!("subtraction operation result: {}", math(subtract, x, y)); 19 } 20 21 // 0x104860fb0 22 // add operation result: 11 23 // 0x104860ff0 24 // subtraction operation result: 5
3. 函数作返回值
函数作为返回值时,也可以使用type关键字为函数指针类型定义别名。代码清单4-5中,第2行代码math_op函数的返回值是函数指针类型,函数体中使用match模式匹配,如果字符串字面量是“add”则返回add函数,否则返回subtract函数。这里的add和subtract都是函数名。
代码清单4-5 函数作返回值
1 type MathOp = fn(i32, i32) -> i32; 2 fn math_op(op: &str) -> MathOp { 3 match op { 4 "add" => add, 5 _ => subtract, 6 } 7 } 8 9 fn add(x: i32, y: i32) -> i32 { 10 x + y 11 } 12 13 fn subtract(x: i32, y: i32) -> i32 { 14 x - y 15 } 16 17 fn main() { 18 let (x, y) = (8, 3); 19 20 let mut op = math_op("add"); 21 println!("operation result: {}", op(x, y)); 22 23 op = math_op("divide"); 24 println!("operation result: {}", op(x, y)); 25 } 26 27 // operation result: 11 28 // operation result: 5