7 方法impl

impl也可以叫做“实现”(implementation)。在面向对象语言中,方法一般是和类或者对象绑定的,rust在概念上来说也大差不差,但是方法的定义却并不在类中,而是在 impl 代码块中定义。

7.1 静态方法

首先是静态方法(这种定义在 impl 中且没有 self 的函数也叫做关联函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct Point {
x: f64,
y: f64,
}

impl Point {
// 静态方法不需要self
// 静态方法不需要被实例调用,并且是直接绑定到类型上
// 这类方法一般用作构造器(constructor)
// 比如初始化一个(0,0)的点
fn origin() -> Point {
Point { x: 0.0, y: 0.0 }
}
// 根据参数初始化一个(x,y)的点
// 在rust这一是个约定俗成的规则,使用 new 来作为构造器的名称
// 并且,出于设计上的考虑,rust 特地没有用 new 作为关键字
fn new(x: f64, y: f64) -> Point {
Point { x: x, y: y }
}
}

现在你应该可以理解,rust的对象定义和方法定义是分离的。impl Point {} 表示为 Point 实现(implementation)方法。我们为Point实现了两个静态方法,下面来看看如何使用它:

1
2
3
4
fn main() {
let p = Point::new(1.0,2.0); // 通过双冒号 `::` (命名空间限定符)使用静态方法 `new`
println!("{:?}",p)
}

7.2 实例方法

实例方法定义

接下来我们为实例实现一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
impl Point {
// 省略..
fn sum(self) {
// self 代指该实例对象
println!("{}", self.x + self.y)
}
}

fn main() {
let p = Point::new(1.0,2.0);
p.sum(); // 通过点`.`操作符: `实例.方法` 来调用
println!("{:?}",p);
}

一般情况下,每个函数参数都需要标注类型,但是self比较特殊,它等价于self:Self。注意大小写。

实例方法与所有权

如果你写过python,那么对这种self肯定非常熟悉。但是,rust往往会给你惊喜:猜这段代码会不会报错?是的,它毫不留情地报错了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
|
26 | let p = Point::new(1.0,2.0);
| - move occurs because `p` has type `Point`, which does not implement the `Copy` trait
27 | p.sum();
| ----- `p` moved due to this method call
28 | println!("{:?}",p);
| ^ value borrowed here after move
|
note: this function takes ownership of the receiver `self`, which moves `p`
--> src/main.rs:20:12
|
20 | fn sum(self) {
| ^^^^
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

*/

嗯,仔细看,就会发现还是老问题:所有权。在sum的函数签名中,我们直接使用selffn sum(self),它其实相当于fn sum(self : Self)的简写,大写的Self是当前对象的类型。

实例方法具有所有权的概念,也就是说,只要你没有为当前的self(也就是Point实例)实现Copy特征,那么就会发生所有权转移,在最后进行打印时,p已经失去了所有权而导致报错。

当然,解决方法也很简单,之前的章节也不止一次提到了,使用引用:

1
fn sum(self : &Self) // 也可以简写为 `&self`

这种引用当然是不可变的,要想改变实例,需要再加上mut

1
fn sum(self : &mut Self)

方法名与字段名相同

rust允许方法名跟结构体的字段名相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#[derive(Debug)]
struct Point {
x: f64,
y: f64,
}

impl Point {
fn new(x: f64, y: f64) -> Point {
Point { x: x, y: y }
}

fn x(self : &mut Self) {
println!("{}", self.x+self.y)
}
}

fn main() {
let mut p = Point::new(1.0,2.0);
p.x();
println!("{:?}",p);
println!("{:?}",p.x);
}

当你使用p.x()时,编译器知道你在调用方法,当你使用p.x,它也知道你是在访问其中的字段。

多个impl

rust允许你为同一个类型定义多个impl块,这样可以更加灵活:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Point {
x: f64,
y: f64,
}

impl Point {
fn new(x: f64, y: f64) -> Point {
Point { x: x, y: y }
}
}

impl Point {
fn x(self : &mut Self) {
println!("{}", self.x + self.y)
}
}

除了结构体,我们还可以为枚举、特征实现方法。有关特征,马上就会讲到了。

8 泛型 Generic

如果你接触过C++或java的泛型,对泛型的概念应该不会陌生。泛型的应用广泛,并且可以极大地减少代码的重复。泛型最简单和常用的用法是用于类型参数。类型参数就像函数的形参一样,作为类型的代号指代一部分类型。一般情况下,类型参数是用尖括号和大驼峰命名的名称:<Aaa, Bbb, ...>指定的,作为其他语言约定俗成的规则,rust里也一般沿用<T>作为类型参数。要使用泛型,需要提前声明泛型的类型参数。

特征泛型会在下一章介绍完特征后再介绍,见特征泛型

8.1 函数泛型

比如,定义一个名为 foo 的泛型函数,它可接受类型为 T 的任何参数 arg

1
fn foo<T>(arg: T) { ... }

在使用类型 T 前,在函数名后面指定泛型类型参数 <T>,那么 T 就变成了泛型。

同理,在结构体中使用泛型:

1
2
3
4
struct A<T> {
a : T,
b : T,
}

需要注意的是,同一个泛型类型只能指代一种具体类型,比如:

1
2
3
4
5
6
7
8
fn main() {
let foo = A{a: 1, b : String::from("hello")};
}
/*
|
8 | let foo = A{a: 1, b : String::from("hello")};
| ^^^^^^^^^^^^^^^^^^^^^ expected integer, found struct `String`
*/

结构体A中的ab都是T类型,当初始化a时,类型推断确定T为整数类型,那么b的类型也应该是整数类型。

要想让ab都是泛型类型,且指代的具体类型不同,那就需要声明不同的泛型类型。

1
2
3
4
struct A<T,U> {
a : T,
b : U,
}

8.2 枚举泛型

枚举一节中,介绍了Option,它的定义是这样的:

1
2
3
4
enum Option<T> {
Some(T),
None,
}

这里的T就是泛型。Some(T)表示它可以接收任意类型的值。

另外一个常用的枚举使用泛型的例子就是Result

1
2
3
4
enum Result<T, E> {
Ok(T),
Err(E),
}

Option一样,主要用于函数返回值,当函数正确返回时,则返回Ok(T)T 是函数具体的返回值类型;当发生错误时,则返回 Err(E)E是错误类型。

8.3 方法泛型

在方法上也可以使用泛型:

1
2
3
4
5
6
7
8
9
10
struct Point<T> {
x: T,
y: T,
}

impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}

使用泛型参数前,依然需要提前声明:impl<T>,只有提前声明了,我们才能在Point<T>中使用它,这样rust就知道 Point 的尖括号中的类型是泛型而不是具体类型。需要注意的是,这里的 Point<T> 不再是泛型声明,而是一个完整的结构体类型,因为我们定义的结构体就是 Point<T> 而不再是 Point

除了结构体中的泛型参数,我们还能在该结构体的方法中定义额外的泛型参数,就跟泛型函数一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 上的泛型参数,它们并不冲突。

8.4 使用具体类型实现方法

对于 Point<T> 类型,你不仅能定义基于 T 的方法,还能针对特定的具体类型,进行方法定义:

1
2
3
4
5
impl Point<f32> {
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}

这段代码意味着 Point<f32> 类型会有一个方法 distance_from_origin,而其他 T 不是 f32 类型的 Point<T> 实例则没有定义此方法。这个方法计算点实例与坐标(0.0, 0.0) 之间的距离,并使用了只能用于浮点型的数学运算符。

这样我们就能针对特定的泛型类型实现某个特定的方法,对于其它泛型类型则没有定义该方法。

8.5 const泛型

对于数组,[i32; 2][i32; 3] 是不同的数组类型:

1
2
3
4
5
6
7
8
9
10
fn display_array(arr: [i32; 3]) {
println!("{:?}", arr);
}
fn main() {
let arr: [i32; 3] = [1, 2, 3];
display_array(arr);

let arr: [i32;2] = [1,2];
display_array(arr);
}

无法使用一个函数来接收这两个不同的类型。当然,你可以通过引用来解决:

1
2
3
4
5
6
7
8
9
10
fn display_array(arr: &[i32]) {
println!("{:?}", arr);
}
fn main() {
let arr: [i32; 3] = [1, 2, 3];
display_array(&arr);

let arr: [i32;2] = [1,2];
display_array(&arr);
}

只要使用数组切片,然后传入 arr 的不可变引用即可。但是如果在某些场景下引用不适宜用或者干脆不能用呢,rust在后续引入的const泛型可以解决这个问题:

1
2
3
4
5
6
7
8
9
10
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 这个泛型参数,它是一个基于值的泛型参数,因为它用来替代的是数组的长度。

N 就是const泛型,定义的语法是 const N: usize,表示 const 泛型 N ,它基于的值类型是 usize