函数
基本语法
Rust的函数使用fn
关键字开头,函数可以有一系列的输入参数,还有一个返回类型。函数返回可以使用return
语句,可以使用表达式。下面是一个标准函数的示例,add
函数接受两个i32
的参数,然后计算它们的和并返回:
fn add(a: i32, b: i32) -> i32 {
a + b
}
println!("{}", add(1, 2)); // 输出:3
函数返回值如果不显示标明,默认是()
。函数返回值类型也可以是never类型!
,这一类函数叫做发散函数,代表这个函数不能够正常返回,例如:
fn diverges() -> ! {
panic!("This function never return!")
}
Rust编写的可执行程序的入口是fn main() -> ()
函数。一般情况下,一个进程开始执行的时候可以接受一系列的参数,退出的时候也可以返回一个错误码,所以很多编程语言会为main
函数设计参数和返回值类型,例如C语言中的int main(int argc, char **argv)
。但是,Rust的main
函数无参数也无返回值,其传递参数和返回状态码都通过单独的API来完成,示例如下:
fn main() {
for arg in std::env::args() {
println!("Arg: {}", arg);
}
}
可以通过std::env::args()
函数获取参数,通过exit()
函数的参数传递错误码,通过std::env::var()
读取环境变量。
函数递归与TCO
函数递归是我们常用的一种思维方式,Rust也支持函数递归调用。下面用经典的Fibonacci
数列来举例:
fn fib(index: u32) -> u64 {
match index {
1 | 2 => 1,
_ => fib(index - 1) + fib(index - 2),
}
}
println!("{}", fib(25)); // 输出:75025
谈起递归,我们都会想到尾递归优化的概念(TCO,Tail Call Optimization)。TCO可以把尾递归在编译阶段转换为迭代循环从而降低时间和空间开销,但Rust并不支持TCO,某个RFC的作者给出了以下理由:
- 可移植性问题,LLVM当时在某些指定架构上特别是MIPS和WebAssembly,不支持正确尾调用。
- LLVM中正确尾调用实际上可能会由于它们当时的实现方式而造成性能损失。
- TCO让调试变得更加困难,因为它重写了栈上的值。
方法
在一些编程语言中,函数和方法往往是对同一种东西的两种称呼,但在Rust中,它们是有明确区分的。方法和函数的语法完全相同:它们使用 fn
关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。但是,只有在结构体上下文中被定义的函数才能称为方法。
方法分为成员方法和静态方法,它们的区别在于第一个参数是否为self
。Rust中的Self
和self
都是关键字,其中Self
是类型名,self
是变量名,self
代表调用该方法的结构体实例,静态方法属于结构体类型所有,所以不需要self
。常见的Self
和self
的组合有: self: Self
(获得所有权)、self: &self
(仅仅读取)、self: &mut self
(做出修改),因为它们的使用频率很高,Rust也提供了相应的语法糖来简写:self
、&self
、&mut self
。
成员方法
可以使用impl
为结构体实现成员方法,如下面的代码所示。将函数移到impl
块中,并将第一个参数改成self
,就把函数变成了成员方法。和其他编程语言类似,调用成员方法只需在结构体示例后加.
即可。每个结构体可以拥有多个impl
块。
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rect = Rectangle { width: 30, height: 50 };
println!(
"The area of the rectangle is {} square pixels.",
rect.area()
);
}
静态方法
在impl
块中定义的不以self
作为参数的方法就是静态方法,见下面的代码。可以使用结构体名和::
运算符来调用静态方法,例如let sq = Rectangle::square(3);
来获得一个大小为3的正方形。
// 接上面
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle { width: size, height: size }
}
}
trait
基本语法
trait的功能类似于Java中的接口,trait可以翻译为“特性”,它可以为多种类型抽象出共同拥有的一些功能。一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法的话,这些类型就可以共享相同的行为了。trait 定义将方法签名组合起来,目的是定义一个实现某些目的所必需的行为的集合。这些方法既可以是成员方法,也可以是静态方法。trait可以用关键字trait
来声明,例如:
trait Shape {
fn area(&self) -> u32;
fn hello();
}
上面的代码声明了一个名为Shape
的trait,它有一个名为area
的方法,即,若某个类型拥有Shape
这个特性,那么它一定可以求面积。为某个类型实现trait使用impl...for
关键字,例如:
struct Rectangle {
width: u32,
height: u32,
}
trait Shape {
fn area(&self) -> u32;
fn hello();
}
impl Shape for Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn hello() {
println!("Just say hello!")
}
}
fn main() {
let rect = Rectangle {
width: 30,
height: 50,
};
println!(
"The area of the rectangle is {} square pixels.",
rect.area()
);
Rectangle::hello();
}
trait中的方法也可以有默认实现,那么在针对具体类型实现的时候,就可以不用重写。我们也可以利用trait为其他类型扩展方法,哪怕这个类型不是我们自己写的。例如,可以为内置类型i32
添加一个方法:
trait Double {
fn double(&self) -> i32;
}
impl Double for i32 {
fn double(&self) -> i32 {
*self * 2
}
}
fn main() {
let i: i32 = 5;
println!("{}", i.double()); // 输出:10
}
孤儿规则
使用trait为类型扩展方法也要受到一定的限制,在声明trait和impl trait的时候,Rust规定了一个一致性规则,也称为孤儿规则:impl块要么与trait的声明在同一个crate中,要么与类型的声明在同一个crate中。也就是说,如果trait和类型都来自于外部的crate,那么便不允许为这个类型实现该trait。这是因为,该类型没有实现该trait,这可能是该类型作者有意的设计,强行实现可能会导致bug。
trait和接口的区别
之前我们说trait和接口在功能上类似,但它们在使用中是有区别的。trait本身不是具体类型,也不是指针类型,它只是定义了针对类型的抽象的约束,不同的类型可以实现同一个trait,而这些类型可能具有不同的大小,因此trait在编译阶段没有固定大小,所以,Rust中不能直接使用trait作为实例变量、参数和返回值,这一点和接口的习惯用法是不同的。例如,下面的代码就是编译错误的:
trait Shape {
fn area(&self) -> u32;
}
// error[E0277]: the size for values of type `(dyn Shape + 'static)` cannot be known at compilation time
fn use_shape(arg: Shape) {}
trait继承
trait允许继承,例如:
trait Base{}
trait Derived: Base {}
这表示Derived
继承了Base
,它意味着,满足Derived
的类型,必然也满足Base
,所以,在针对一个具体类型impl Derived
的时候,编译器也会要求同时impl Base
,否则会报编译错误:
trait Base {}
trait Derived: Base {}
struct T;
// error[E0277]: the trait bound `T: Base` is not satisfied
impl Derived for T {}
fn main() {}
常见trait简介
标准库中有很多常见且很有用的trait,我们一起学习一下。
Display和Debug
Display
和Debug
的定义如下:
pub trait Display {
fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}
pub trait Debug {
fn fmt(&self, f: &mut Formatter<'_>) -> Result;
}
这两个trait主要用在类似println!
这样进行输出的地方。只有实现了Display
的类型,才能用{}
格式打印出来;只有实现了Debug
的类型,才能用{:?}
和{:#?}
格式打印出来。它们之间的更多区别如下:
Display
假定了这个类型可以用utf-8
格式的字符串表示,它是准备给最终用户看的,并不是所有的类型都应该实现这个trait。标准库中还有一个常用的trait叫作std::string::ToString
,对于所有实现Display
的类型, 都自动实现了这个ToString
trait,它包含了一个to_string(&self)->String
方法。Debug
主要是为了调试使用,建议所有的作为API的公开类型都应当实现这个trait,以方便调试。
PartialOrd/Ord/PartialEq/Eq
我们首先介绍一下全序和偏序的概念。对于集合X
中的元素a,b,c
:
- 如果
a < b
则一定有!(a > b)
,称为反对称性; - 如果
a < b
且b < c
则一定有a < c
,称为传递性; - 对于
X
中的所有元素,都存在a < b
或a > b
或者a == b
,三者必居其一,称为完全性。
如果集合中的元素只具备上述前两条特征,则称X
是偏序;同时具备以上所有特征,则称X
是全序。
Rust设计了两个trait来对全序和偏序进行描述,PartialOrd
代表偏序,Ord
代表全序。只有满足全序的类型才可以进行排序,像浮点数这样的偏序类型就无法排序。同理,PartialEq
用来描述只能部分元素进行相等比较,Eq
表示全部元素都可以进行相等比较。这样的设计可以让我们在更早的阶段发现错误。
Sized
这个trait表示类型是否有大小,它定义在std::marker
模块中,它没有任何的成员方法,它与普通trait不同,编译器对它有特殊处理,用户也不能针对自己的类型实现这个trait。一个类型是否满足Sized
约束完全是由编译器推导的,用户无权指定。
Default
Rust中没有C++中构造函数的概念,因为相比普通函数,构造函数本身并没有提供额外的抽象能力,反倒增加了语法上的负担,因此,Rust推荐使用普通的静态函数作为类型的构造器,例如String::new()
。对于那种无参数无错误处理的简单情况,标准库提供了Default
来做统一抽象,其定义如下:
pub trait Default: Sized {
fn default() -> Self;
}
属性与derive
Rust中有一种语法,属性(attribute),基本格式类似于#[xxx]
。属性可以用来注释声明,类似于Java中的注解。有一种非常实用的属性derive
,可以帮助我们自动impl
某些trait,因为实现某些trait
的时候,逻辑是非常机械化的,例如Debug
。示例如下:
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect = Rectangle {
width: 10,
height: 20,
};
println!("{:?}", rect); // 输出:Rectangle { width: 10, height: 20 }
}
目前,Rust支持的可以自动derive
的trait有以下这些:
Debug Clone Copy Hash RustcEncodable RustcDecodable PartialEq Eq
ParialOrd Ord Default FromPrimitive Send Sync
参考文献
- 《Rust编程之道》张汉东
- 《深入浅出Rust》范长春