目录
前言
这节课来介绍一下Rust中的泛型和特质,泛型在大部分语言中都有,和Java中的泛型差不多,特质在Java并没有,但是在Scala中有,特质其实类似于Java中的接口Interface。
泛型
(1)容器中的泛型
Rust中我们使用的Vector、HashMap其实都是有泛型的,和其他编程语言中的泛型一样
(2)结构体中的泛型
下面代码展示了结构体中的泛型,包括结构体的方法如何定义泛型,包括方法和关联函数。值的注意的是在调用关联函数的时候,我们指定了具体的类型,可以试一下如果不指定会怎么样?不指定编译会报错,为什么会报错呢?这里涉及Rust中泛型的实现方式。
use std::fmt::Display;
fn main() {
let r1 = RecTangle {
width: 1,
height: 2.0,
};
println!("r1.width={}", r1.width);
println!("r1.height={}", r1.height);
r1.custom_print1("hello");
RecTangle::<i32, i32>::custom_print2("hello");
}
struct RecTangle<T, U> {
width: T,
height: U,
}
impl<T, U> RecTangle<T, U> {
fn get_width(&self) -> &T {
&self.width
}
fn get_height(&self) -> &U {
&self.height
}
fn custom_print1<E: Display>(&self, other: E) {
println!("other={}", other);
}
fn custom_print2<E: Display>(text: E) {
println!("text = {}", text);
}
}
单态化
我们来聊一聊Rust中泛型的实现方式:单态化。
有了解过Java中的泛型实现可以知道,Java中的泛型是伪泛型,存在泛型擦除,不管什么泛型最终都是Object。但是Rust中的泛型实现不一样,他是依赖编译期将存在的类型都固定死。什么意思呢?假设我们定义了一个泛型函数,在整个程序中,该泛型可以是i32,f64.那么在编译期间,Rust编译器会根据类型推断生成具体类型的代码,比如这个函数会生成i32和f64对应的函数,在调用函数的地方也会替换为i32和f64的函数,这种行为就叫做单态化,单态化的好处是类型确定且性能优化。我们提供一段代码解释一下,下面的代码中我们定义了泛型函数,在main方法中,调用了&str和i32和f64类型的函数,那么在编译期间,会生成对应类型的my_print函数,并且在main方法中替换函数。
use std::fmt::Display;
fn main() {
my_print("hello");
my_print(12);
my_print(3.14);
}
fn my_print<T: Display>(input: T) {
println!("input is {}", input);
}
特质
Rust中的特质trait可以类比于Java中的接口interface理解。
在下面的代码中,我们定义了一个Trait:Say;并且定义了抽象方法say_hi,又定义了三个不同的结构体,并且为这些结构体实现了Say特质。
use std::fmt::Display;
fn main() {
let p1 = Person {
name: String::from("p"),
};
let d1 = Dog {
name: String::from("d1"),
};
let c1 = Cat {
name: String::from("c1"),
};
p1.say_hi();
d1.say_hi();
c1.say_hi();
}
trait Say {
fn name_f() {
println!("我是接口Say")
}
fn say_hi(&self);
}
struct Person {
name: String,
}
impl Say for Person {
fn say_hi(&self) {
println!("{} say hi", self.name);
}
}
struct Dog {
name: String,
}
impl Say for Dog {
fn say_hi(&self) {
println!("{} say wang", self.name);
}
}
struct Cat {
name: String,
}
impl Say for Cat {
fn say_hi(&self) {
println!("{} say miao", self.name);
}
}
多态
由于Rust中没有继承的概念,不像Java等语言,有父类和子类的概念,在java代码中经常看到使用父类接收子类对象的代码,在Rust中都是依赖Trait来实现这样的功能的。Trait可以当作参数也可以当作返回值。
use std::fmt::Display;
fn main() {
let p1 = Person {
name: String::from("p"),
};
let d1 = Dog {
name: String::from("d1"),
};
let c1 = Cat {
name: String::from("c1"),
};
custom_print(&p1);
p1.say_hi();
}
fn custom_print(input: &impl Say) {
input.say_hi();
}
trait Say {
fn name_f() {
println!("我是接口Say")
}
fn say_hi(&self);
}
struct Person {
name: String,
}
impl Say for Person {
fn say_hi(&self) {
println!("{} say hi", self.name);
}
}
struct Dog {
name: String,
}
impl Say for Dog {
fn say_hi(&self) {
println!("{} say wang", self.name);
}
}
struct Cat {
name: String,
}
impl Say for Cat {
fn say_hi(&self) {
println!("{} say miao", self.name);
}
}
上面的代码是用Trait当作参数的例子,定义了custom_print函数,函数的参数使用impl Say表示接收一个实现了Say特质的参数,注意这里我使用了&表示这只是一个引用,我们复习一下所有权转移的知识,如果不加&,所以权会转移给入参,当函数运行结束后,这一块内存会被释放,于是我们还在想函数调用结束后,使用结构体对象的话,就会报错,于是我们使用&借用只读权限,这样不会报错,由此可见,所有权无处不在。
下面的代码展示了如何使用Trait作为函数的返回值
在函数的返回值处使用impl Say表示函数返回的是一个实现了Say特质的对象。
use std::fmt::Display;
fn main() {
let p1 = Person {
name: String::from("p"),
};
let d1 = Dog {
name: String::from("d1"),
};
let c1 = Cat {
name: String::from("c1"),
};
Say::name_f();
Person::name_f();
custom_print(&p1);
p1.say_hi();
let xx = new_say();
xx.say_hi();
}
fn new_say() -> impl Say {
Dog {
name: String::from("www"),
}
}
fn custom_print(input: &impl Say) {
input.say_hi();
}
trait Say {
fn name_f() {
println!("我是接口Say")
}
fn say_hi(&self);
}
struct Person {
name: String,
}
impl Say for Person {
fn say_hi(&self) {
println!("{} say hi", self.name);
}
}
struct Dog {
name: String,
}
impl Say for Dog {
fn say_hi(&self) {
println!("{} say wang", self.name);
}
}
struct Cat {
name: String,
}
impl Say for Cat {
fn say_hi(&self) {
println!("{} say miao", self.name);
}
}
总结
这节课讲述了Rust中面向对象的部分,对于泛型,Rust中泛型实现方式和其他语言不用,采用了单态化的方式在编译器生成代码,Rust将很多工作都移动到了编译器执行,这也是编译时间长的原因之一,Rust中的特质和Scala中的特质Java中的接口类似。