rust入坑小记-04-方法与泛型
7 方法impl
impl也可以叫做“实现”(implementation)。在面向对象语言中,方法一般是和类或者对象绑定的,rust在概念上来说也大差不差,但是方法的定义却并不在类中,而是在 impl 代码块中定义。
7.1 静态方法
首先是静态方法(这种定义在 impl 中且没有 self 的函数也叫做关联函数):
1 | struct Point { |
现在你应该可以理解,rust的对象定义和方法定义是分离的。impl Point {} 表示为 Point 实现(implementation)方法。我们为Point实现了两个静态方法,下面来看看如何使用它:
1 | fn main() { |
7.2 实例方法
实例方法定义
接下来我们为实例实现一个方法:
1 | impl Point { |
一般情况下,每个函数参数都需要标注类型,但是self比较特殊,它等价于self:Self。注意大小写。
实例方法与所有权
如果你写过python,那么对这种self肯定非常熟悉。但是,rust往往会给你惊喜:猜这段代码会不会报错?是的,它毫不留情地报错了:
1 | /* |
嗯,仔细看,就会发现还是老问题:所有权。在sum的函数签名中,我们直接使用self,fn 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 |
|
当你使用p.x()时,编译器知道你在调用方法,当你使用p.x,它也知道你是在访问其中的字段。
多个impl
rust允许你为同一个类型定义多个impl块,这样可以更加灵活:
1 | struct Point { |
除了结构体,我们还可以为枚举、特征实现方法。有关特征,马上就会讲到了。
8 泛型 Generic
如果你接触过C++或java的泛型,对泛型的概念应该不会陌生。泛型的应用广泛,并且可以极大地减少代码的重复。泛型最简单和常用的用法是用于类型参数。类型参数就像函数的形参一样,作为类型的代号指代一部分类型。一般情况下,类型参数是用尖括号和大驼峰命名的名称:<Aaa, Bbb, ...>指定的,作为其他语言约定俗成的规则,rust里也一般沿用<T>作为类型参数。要使用泛型,需要提前声明泛型的类型参数。
特征泛型会在下一章介绍完特征后再介绍,见特征泛型。
8.1 函数泛型
比如,定义一个名为 foo 的泛型函数,它可接受类型为 T 的任何参数 arg:
1 | fn foo<T>(arg: T) { ... } |
在使用类型 T 前,在函数名后面指定泛型类型参数 <T>,那么 T 就变成了泛型。
同理,在结构体中使用泛型:
1 | struct A<T> { |
需要注意的是,同一个泛型类型只能指代一种具体类型,比如:
1 | fn main() { |
结构体A中的a、b都是T类型,当初始化a时,类型推断确定T为整数类型,那么b的类型也应该是整数类型。
要想让a、b都是泛型类型,且指代的具体类型不同,那就需要声明不同的泛型类型。
1 | struct A<T,U> { |
8.2 枚举泛型
在枚举一节中,介绍了Option,它的定义是这样的:
1 | enum Option<T> { |
这里的T就是泛型。Some(T)表示它可以接收任意类型的值。
另外一个常用的枚举使用泛型的例子就是Result:
1 | enum Result<T, E> { |
和Option一样,主要用于函数返回值,当函数正确返回时,则返回Ok(T),T 是函数具体的返回值类型;当发生错误时,则返回 Err(E),E是错误类型。
8.3 方法泛型
在方法上也可以使用泛型:
1 | struct Point<T> { |
使用泛型参数前,依然需要提前声明:impl<T>,只有提前声明了,我们才能在Point<T>中使用它,这样rust就知道 Point 的尖括号中的类型是泛型而不是具体类型。需要注意的是,这里的 Point<T> 不再是泛型声明,而是一个完整的结构体类型,因为我们定义的结构体就是 Point<T> 而不再是 Point。
除了结构体中的泛型参数,我们还能在该结构体的方法中定义额外的泛型参数,就跟泛型函数一样:
1 | struct Point<T, U> { |
T,U 是定义在结构体 Point 上的泛型参数,V,W 是单独定义在方法 mixup 上的泛型参数,它们并不冲突。
8.4 使用具体类型实现方法
对于 Point<T> 类型,你不仅能定义基于 T 的方法,还能针对特定的具体类型,进行方法定义:
1 | impl Point<f32> { |
这段代码意味着 Point<f32> 类型会有一个方法 distance_from_origin,而其他 T 不是 f32 类型的 Point<T> 实例则没有定义此方法。这个方法计算点实例与坐标(0.0, 0.0) 之间的距离,并使用了只能用于浮点型的数学运算符。
这样我们就能针对特定的泛型类型实现某个特定的方法,对于其它泛型类型则没有定义该方法。
8.5 const泛型
对于数组,[i32; 2] 和 [i32; 3] 是不同的数组类型:
1 | fn display_array(arr: [i32; 3]) { |
无法使用一个函数来接收这两个不同的类型。当然,你可以通过引用来解决:
1 | fn display_array(arr: &[i32]) { |
只要使用数组切片,然后传入 arr 的不可变引用即可。但是如果在某些场景下引用不适宜用或者干脆不能用呢,rust在后续引入的const泛型可以解决这个问题:
1 | fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) { |
如上所示,我们定义了一个类型为 [T; N] 的数组,其中 T 是一个基于类型的泛型参数,这个和之前讲的泛型没有区别,而重点在于 N 这个泛型参数,它是一个基于值的泛型参数,因为它用来替代的是数组的长度。
N 就是const泛型,定义的语法是 const N: usize,表示 const 泛型 N ,它基于的值类型是 usize。