Rust中,方法往往和对象成对出现:object.method()
。Rust 的方法往往跟结构体、枚举、特征(Trait)一起使用
Rust 使用 impl
来定义方法,例如以下代码:
struct Circle {
x: f64,
y: f64,
radius: f64,
}
impl Circle {
// new是Circle的关联函数,因为它的第一个参数不是self,且new并不是关键字
// 这种方法往往用于初始化当前结构体的实例
fn new(x: f64, y: f64, radius: f64) -> Circle {
Circle {
x: x,
y: y,
radius: radius,
}
}
// Circle的方法,&self表示借用当前的Circle结构体
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
}
注:Rust 的对象定义和方法定义是分离的,这种数据和使用分离的方式,会给予使用者极高的灵活度。
在一个 impl
块内,Self
指代被实现方法的结构体类型,self
指代此类型的实例,即为哪个结构体实现方法,那么 self
就是指代哪个结构体的实例。self
依然有所有权的概念:
self
表示 Rectangle
的所有权转移到该方法中,这种形式用的较少&self
表示该方法对 Rectangle
的不可变借用&mut self
表示可变借用self
的使用就跟函数参数一样,要严格遵守 Rust 的所有权规则。使用方法代替函数有以下好处:
self
对应的类型在 Rust 中,允许方法名跟结构体的字段名相同,往往适用于实现 getter
访问器:
pub struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
pub fn new(width: u32, height: u32) -> Self {
Rectangle { width, height }
}
pub fn width(&self) -> u32 {
return self.width;
}
}
fn main() {
let rect1 = Rectangle::new(30, 50);
println!("{}", rect1.width());
}
用这种方式,我们可以把 Rectangle
的字段设置为私有属性,只需把它的 new
和 width
方法设置为公开可见,那么用户就可以创建一个矩形,同时通过访问器 rect1.width()
方法来获取矩形的宽度,因为 width
字段是私有的,当用户访问 rect1.width
字段时,就会报错。注意在此例中,Self
指代的就是被实现方法的结构体 Rectangle
。
在 C/C++ 语言中,有两个不同的运算符来调用方法:.
直接在对象上调用方法,而 ->
在一个对象的指针上调用方法,这时需要先解引用指针。换句话说,如果 object
是一个指针,那么 object->something()
和 (*object).something()
是一样的。
Rust 并没有一个与 ->
等效的运算符;相反,Rust 有一个叫 自动引用和解引用的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。他是这样工作的:当使用 object.something()
调用方法时,Rust 会自动为 object
添加 &
、&mut
或 *
以便使 object
与方法签名匹配。也就是说,这些代码是等价的:
p1.distance(&p2);
(&p1).distance(&p2);
第一行看起来简洁的多。这种自动引用的行为之所以有效,是因为方法有一个明确的接收者———— self
的类型。在给出接收者和方法名的前提下,Rust 可以明确地计算出方法是仅仅读取(&self
),做出修改(&mut self
)或者是获取所有权(self
)。事实上,Rust 对方法接收者的隐式借用让所有权在实践中更友好。
思考一个问题,如何为一个结构体定义一个构造器方法?也就是接受几个参数,然后构造并返回该结构体的实例。其实答案在开头的代码片段中就给出了,很简单,参数中不包含 self
即可。
这种定义在 impl
中且没有 self
的函数被称之为关联函数: 因为它没有 self
,不能用 f.read()
的形式调用,因此它是一个函数而不是方法,它又在 impl
中,与结构体紧密关联,因此称为关联函数。
在之前的代码中,我们已经多次使用过关联函数,例如 String::from
,用于创建一个动态字符串。
impl Rectangle {
fn new(w: u32, h: u32) -> Rectangle {
Rectangle { width: w, height: h }
}
}
Rust 中有一个约定俗成的规则,使用
new
来作为构造器的名称,出于设计上的考虑,Rust 特地没有用new
作为关键字
因为是函数,所以不能用 .
的方式来调用,我们需要用 ::
来调用,例如 let sq = Rectangle::new(3, 3);
。这个方法位于结构体的命名空间中:::
语法用于关联函数和模块创建的命名空间。
Rust 允许我们为一个结构体定义多个 impl
块,目的是提供更多的灵活性和代码组织性,例如当方法多了后,可以把相关的方法组织在同一个 impl
块中,那么就可以形成多个 impl
块,各自完成一块儿目标。
除了结构体,还可以为枚举、特征(trait)实现方法,如:
#![allow(unused)]
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(&self) {
// 在这里定义方法体
}
}
fn main() {
let m = Message::Write(String::from("hello"));
m.call();
}
使用泛型参数,有一个先决条件,必需在使用前对其进行声明:
fn largest<T>(list: &[T]) -> T {}
可以这样理解这个函数定义:函数 largest
有泛型类型 T
,它有个参数 list
,其类型是元素为 T
的数组切片,最后,该函数返回值的类型也是 T
。
结构体中的字段类型也可以用泛型来定义,下面代码定义了一个坐标点 Point
,它可以存放任何类型的坐标值:
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
这里有两点需要特别的注意:
Point
,接着就可以在结构体的字段类型中使用 T
来替代具体的类型struct Point { x: T, y: U, }
Option
枚举类型
enum Option<T> {
Some(T),
None,
}
Option
是一个拥有泛型 T
的枚举类型,它第一个成员是 Some(T)
,存放了一个类型为 T
的值。可以在任何一个需要返回值的函数中,去使用 Option
枚举类型来做为返回值,用于返回一个任意类型的值 Some(T)
,或者没有值 None
。
Result
枚举类型
enum Result<T, E> {
Ok(T),
Err(E),
}
主要用于函数返回值,如果函数正常运行,则最后返回一个 Ok(T)
,T
是函数具体的返回值类型,如果函数异常运行,则返回一个 Err(E)
,E
是错误类型。例如打开一个文件:如果成功打开文件,则返回 Ok(std::fs::File)
,因此 T
对应的是 std::fs::File
类型;而当打开文件时出现问题时,返回 Err(std::io::Error)
,E
对应的就是 std::io::Error
类型。
在结构体的方法中定义额外的泛型参数,就跟泛型函数一样:
struct Point<T, U> {
x: T,
y: U,
}
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}
fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c'};
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
这个例子中,T,U
是定义在结构体 Point
上的泛型参数,V,W
是单独定义在方法 mixup
上的泛型参数,可以理解为,一个是结构体泛型,一个是函数泛型。
对于 Point
类型,你不仅能定义基于 T
的方法,还能针对特定的具体类型,进行方法定义:
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
这段代码意味着 Point
类型会有一个方法 distance_from_origin
,而其他 T
不是 f32
类型的 Point
实例则没有定义此方法。这个方法计算点实例与坐标(0.0, 0.0)
之间的距离,并使用了只能用于浮点型的数学运算符。
这样我们就能针对特定的泛型类型实现某个特定的方法,对于其它泛型类型则没有定义该方法
const 泛型,针对值的泛型,可以用于处理数组长度的问题:
fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
println!("{:?}", arr);
}
fn main() {
let arr: [i32; 3] = [1, 2, 3];
display_array(arr);
let arr: [i32; 2] = [1, 2];
display_array(arr);
}
如上所示,我们定义了一个类型为 [T; N]
的数组,其中 T
是一个基于类型的泛型参数,重点在于 N
这个泛型参数,它是一个基于值的泛型参数!因为它用来替代的是数组的长度。
注意的是需要对
T
加一个限制std::fmt::Debug
,该限制表明T
可以用在println!("{:?}", arr)
中,因为{:?}
形式的格式化输出需要arr
实现该特征。
N
就是 const 泛型,定义的语法是 const N: usize
,表示 const 泛型 N
,它基于的值类型是 usize
。
Rust 通过在编译时进行泛型代码的 单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。
编译器所做的工作正好与我们创建泛型函数的步骤相反,编译器寻找所有泛型代码被调用的位置并针对具体类型生成代码。
特征类似于其他语言中的接口,定义了一组可以被共享的行为,只要实现了特征,你就能使用这组行为。
因为特征只定义行为看起来是什么样的,因此我们需要为类型实现具体的特征,定义行为具体是怎么样的。
首先来为 Post
和 Weibo
实现 Summary
特征:
//特征定义,也可以在特征中定义具有默认实现的方法
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct Post {
pub title: String, // 标题
pub author: String, // 作者
pub content: String, // 内容
}
//特征实现
impl Summary for Post {
fn summarize(&self) -> String {
format!("文章{}, 作者是{}", self.title, self.author)
}
}
pub struct Weibo {
pub username: String,
pub content: String
}
impl Summary for Weibo {
fn summarize(&self) -> String {
format!("{}发表了微博{}", self.username, self.content)
}
}
实现特征的语法与为结构体、枚举实现方法很像:impl Summary for Post
,读作“为 Post
类型实现 Summary
特征”,然后在 impl
的花括号中实现该特征的具体方法。
关于特征实现与定义的位置,有一条非常重要的原则:如果你想要为类型
A
实现特征T
,那么A
或者T
至少有一个是在当前作用域中定义的!
定义一个函数,使用特征作为函数参数:
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
impl Summary
,它的意思是 实现了Summary
特征 的 item
参数。
你可以使用任何实现了 Summary
特征的类型作为该函数的参数,同时在函数体内,还可以调用该特征的方法,例如 summarize
方法。具体的说,可以传递 Post
或 Weibo
的实例来作为参数,而其它类如 String
或者 i32
的类型则不能用做该函数的参数,因为它们没有实现 Summary
特征。
虽然 impl Trait
这种语法非常好理解,但是实际上它只是一个语法糖:
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
真正的完整书写形式如上所述,形如 T: Summary
被称为特征约束。
在简单的场景下 impl Trait
这种语法糖就足够使用,但是对于复杂的场景,特征约束可以让我们拥有更大的灵活性和语法表现能力,例如一个函数接受两个 impl Summary
的参数:
pub fn notify(item1: &impl Summary, item2: &impl Summary) {}
如果函数两个参数是不同的类型,那么上面的方法很好,只要这两个类型都实现了 Summary
特征即可。但是如果我们想要强制函数的两个参数是同一类型呢?上面的语法就无法做到这种限制,此时我们只能使特征约束来实现:
pub fn notify<T: Summary>(item1: &T, item2: &T) {}
泛型类型 T
说明了 item1
和 item2
必须拥有同样的类型,同时 T: Summary
说明了 T
必须实现 Summary
特征。
pub fn notify<T: Summary + Display>(item: &T) {}
通过这两个特征,就可以使用 item.summarize
方法,以及通过 println!("{}", item)
来格式化输出 item
。
当特征约束变得很多时,函数的签名将变得很复杂:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {}
严格来说,上面的例子还是不够复杂,但是我们还是能对其做一些形式上的改进,通过 where
:
fn some_function<T, U>(t: &T, u: &U) -> i32
where T: Display + Clone,
U: Clone + Debug
{}
可以通过 impl Trait
来说明一个函数返回了一个类型,该类型实现了某个特征:
fn returns_summarizable() -> impl Summary {
Weibo {
username: String::from("sunface"),
content: String::from(
"m1 max太厉害了,电脑再也不会卡",
)
}
}
因为 Weibo
实现了 Summary
,因此这里可以用它来作为返回值。要注意的是,虽然我们知道这里是一个 Weibo
类型,但是对于 returns_summarizable
的调用者而言,他只知道返回了一个实现了 Summary
特征的对象,但是并不知道返回了一个 Weibo
类型。
当函数返回的真实类型非常复杂,不知道该怎么声明时(毕竟 Rust 要求你必须标出所有的类型),此时就可以用 impl Trait
的方式简单返回。例如,闭包和迭代器就是很复杂,只有编译器才知道那玩意的真实类型,如果让你写出来它们的具体类型,估计内心有一万只草泥马奔腾,好在你可以用 impl Iterator
来告诉调用者,返回了一个迭代器,因为所有迭代器都会实现 Iterator
特征。
+
操作在 Rust 中除了数值类型的加法,String
也可以做加法,因为 Rust 为该类型实现了 std::ops::Add
特征,同理,如果我们为自定义类型实现了该特征,那就可以自己实现 Point1 + Point2
的操作:
use std::ops::Add;
// 为Point结构体派生Debug特征,用于格式化输出
#[derive(Debug)]
struct Point<T: Add<T, Output = T>> { //限制类型T必须实现了Add特征,否则无法进行+操作。
x: T,
y: T,
}
impl<T: Add<T, Output = T>> Add for Point<T> {
type Output = Point<T>;
fn add(self, p: Point<T>) -> Point<T> {
Point{
x: self.x + p.x,
y: self.y + p.y,
}
}
}
fn add<T: Add<T, Output=T>>(a:T, b:T) -> T {
a + b
}
fn main() {
let p1 = Point{x: 1.1f32, y: 1.1f32};
let p2 = Point{x: 2.1f32, y: 2.1f32};
println!("{:?}", add(p1, p2));
let p3 = Point{x: 1i32, y: 1i32};
let p4 = Point{x: 2i32, y: 2i32};
println!("{:?}", add(p3, p4));
}
在开发过程中,往往只要使用 #[derive(Debug)]
对我们的自定义类型进行标注,即可实现打印输出的功能:
#[derive(Debug)]
struct Point{
x: i32,
y: i32
}
fn main() {
let p = Point{x:3,y:3};
println!("{:?}",p);
}
但是在实际项目中,往往需要对我们的自定义类型进行自定义的格式化输出,以让用户更好的阅读理解我们的类型,此时就要为自定义类型实现 std::fmt::Display
特征:
#![allow(dead_code)]
use std::fmt;
use std::fmt::{Display};
#[derive(Debug,PartialEq)]
enum FileState {
Open,
Closed,
}
#[derive(Debug)]
struct File {
name: String,
data: Vec<u8>,
state: FileState,
}
impl Display for FileState {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
FileState::Open => write!(f, "OPEN"),
FileState::Closed => write!(f, "CLOSED"),
}
}
}
impl Display for File {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "<{} ({})>",
self.name, self.state)
}
}
impl File {
fn new(name: &str) -> File {
File {
name: String::from(name),
data: Vec::new(),
state: FileState::Closed,
}
}
}
fn main() {
let f6 = File::new("f6.txt");
//...
println!("{:?}", f6);
println!("{}", f6);
}
可以通过 &
引用或者 Box
智能指针的方式来创建特征对象。
trait Draw {
fn draw(&self) -> String;
}
impl Draw for u8 {
fn draw(&self) -> String {
format!("u8: {}", *self)
}
}
impl Draw for f64 {
fn draw(&self) -> String {
format!("f64: {}", *self)
}
}
// 若 T 实现了 Draw 特征, 则调用该函数时传入的 Box 可以被隐式转换成函数参数签名中的 Box
fn draw1(x: Box<dyn Draw>) {
// 由于实现了 Deref 特征,Box 智能指针会自动解引用为它所包裹的值,然后调用该值对应的类型上定义的 `draw` 方法
x.draw();
}
fn draw2(x: &dyn Draw) {
x.draw();
}
fn main() {
let x = 1.1f64;
// do_something(&x);
let y = 8u8;
// x 和 y 的类型 T 都实现了 `Draw` 特征,因为 Box 可以在函数调用时隐式地被转换为特征对象 Box
// 基于 x 的值创建一个 Box 类型的智能指针,指针指向的数据被放置在了堆上
draw1(Box::new(x));
// 基于 y 的值创建一个 Box 类型的智能指针
draw1(Box::new(y));
draw2(&x);
draw2(&y);
}
上面代码,有几个非常重要的点:
draw1
函数的参数是 Box
形式的特征对象,该特征对象是通过 Box::new(x)
的方式创建的draw2
函数的参数是 &dyn Draw
形式的特征对象,该特征对象是通过 &x
的方式创建的dyn
关键字只用在特征对象的类型声明上,在创建时无需使用 dyn
因此,可以使用特征对象来代表泛型或具体的类型。
泛型是在编译期完成处理的:编译器会为每一个泛型参数对应的具体类型生成一份代码,这种方式是静态分发(static dispatch),因为是在编译期完成的,对于运行期性能完全没有任何影响。
与静态分发相对应的是动态分发(dynamic dispatch),在这种情况下,直到运行时,才能确定需要调用什么方法。之前代码中的关键字 dyn
正是在强调这一“动态”的特点。当使用特征对象时,Rust 必须使用动态分发。
特征对象特点:
&dyn Draw
、Box
(引用大小是固定的,占用两个指针大小:ptr-指向实现了特征的具体类型的实例/vptr-指向虚表vtable)在Rust中,一般
Self
代表类型,self
指代当前实例对象。
不是所有特征都能拥有特征对象,只有对象安全的特征才行。当一个特征的所有方法都有如下属性时,它的对象才是安全的:
Self