1 泛型
1.1 泛型定义
泛型是一种编程思维,即程序中将类型抽象为变量编写的程序模版,泛型可以提高代码的复用能力。泛型代码并不是最终的代码,而是一种带有“占位符”(类型参数)的模板,在编译时编译器将“占位符”替换为具体的代码(单态化)。类型参数使用CamelCase命名规则。Rust中泛型的概念涉猎十分广泛,除了基础的类型模版外,trait、生命周期都可以理解为泛型。
使用下面的方式定义泛型:
fn largest<T> (list: &[T]) -> T {...}
下面是一个在结构体中使用泛型的例子,例子中T
和U
就是两个类型参数,创建的实例中类型可以是不同的,也可以是相同的。可以使用多个类型参数,但是类型参数过多会降低代码的可读性。
struct Point<T, U> {
X: T,
Y: U,
}
fn main() {
let point1 = Point {X: 1, Y: 2.0};
let point2 = Point {X: 2.0, Y:1}
}
1.2 在 Enum 中定义泛型
枚举中使用泛型的主要作用是可以让枚举的变体持有泛型数据类型。比如Option<T>
和Reselt<T, E>
两个枚举。
1.3 在方法的定义中使用泛型
把<T>
放在impl
关键字后,表示在类型T
上实现方法;如果只针对某个具体类型实现方法,那么就不需要<T>
关键字(其余类型就没有实现该方法)。
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> { // 针对泛型类型(大多数)实现,impl后要加关键字<T>
fn x(&self) -> &T {
&self.x
}
}
impl Point<i32> { // 针对特定类型实现,impl后不需要关键字
fn x_i32(&self) -> &i32 {
&self.x
}
}
fn main() {
let point = Point {x: 1, y: 2};
println!("{}", point.x);
}
方法的类型参数可以和结构体的类型参数不同:
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 point = Point {x: 1, y: 2.0};
let char_point = Point {x: "a", y: "b"};
let mix = point.mixup(char_point);
println!("{} {}", mix.x, mix.y);
}
Rust的运行方式决定了它使用泛型的运行速度和运行特定类型的代码的速度是一样的,因为Rust编译时就会实现单态化,即将所有类型参数替换为特定的具体类型。
2 Trait
Trait是用来定义某种类型具有哪些并且可以和其他类型共享的功能,它抽象地定义了共享的行为。
Trait bounds(约束):将泛型类型参数指定为实现了特定行为的类型,即指定了泛型的类型参数实现了某些Trait。Trait和其他语言的接口(interface)较为类似,但是也有一定的区别。
2.1 定义一个 Trait
类型可用的行为用该类型可调用的方法组成,但是有时在不同的类型上,都需要一个或多个相同(类似)的方法,这时我们就称这些类型共享了相同的行为。Trait把方法的签名放在一起,从而定义实现某种目的所必须的某一组行为。在Trait的定义中,只有方法的签名,而没有具体的实现。实现该Trait的类型,必须提供该方法的具体实现。
pub trait Summary {
fn summarize(&self) -> String;
}
fn main() {
}
在上面的例子中,定一个了一个Summary
trait,之后在这个trait中声明了一个方法的签名。签名可以有多条,签名之间用;
隔开。
2.2 在类型上实现 Trait
为特定方法实现Trait和为类型实现方法比较类似,具体的语句结构为impl trait_name for struct_name { }
,在这个impl
块中,需要对Trait的方法签名进行具体的实现:
lib.rs
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
main.rs
// 工程名为 templete
use templete::Summary; // 引入Summary Trait
use templete::Tweet; // 引入Tweet类型
fn main() {
let tweet = Tweet {
username: String::from("debussy"),
content: String::from("Of course, you can learn Rust well!"),
reply: false,
retweet: false,
};
println!("1 new tweet: {}", tweet.summarize());
}
可以在某个类型上实现某个Trait的前提条件是:这个类型或者这个Trait是在本地的Crate中定义的。例如上面的例子中,
Tweet
这个类型和Summary
这个Trait都是本地定义的,实际上只要有一个是在本地定义的就可以实现这个Trait;相对的,不能为外部类型实现(重写)外部的Trait,例如我们想为标准库的i32类型实现标准库中的Display
Trait就是不被允许的。这样的限制是Rust程序属性的一部分(一致性),更具体的说就是孤儿规则(父类型不存在)。此规则确保他人的代码不会破坏您的代码,反之亦然。如果没有这个规则,那么两个Crate就可以为同一个类型实现同一个Trait,调用时Rust就不知道调用哪一个Trait了。Rust这样的做法牺牲了多态特性,但是保证了Crate之间的低耦合特性。
2.3 默认实现
有时候,为Trait中的有些方法或者所有方法提供默认实现是非常有用的,它可以让我们不对每一个类型的实现都提供自定义的行为,我们可以针对某些特定的类型实现Trait中的方法。当我们为某个类型实现Trait时,可以选择保留或者重写Trait中的方法。
pub trait Summary {
fn summarize(&self) -> String {
String::from("Read more...")
}
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle { // 未重写
// fn summarize(&self) -> String {
// format!("{}, by {} ({})", self.headline, self.author, self.location)
// }
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet { // 重写
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Trait中的默认实现可以使用Trait中的其他方法,即使这些其他方法没有默认实现;但是如果某个类型要调用该默认实现,那么就必须保证未默认实现的方法已被定义。
2.4 将 Trait 作为函数参数和 Trait bounds
在上面的例子中,如果定义了一个函数,要求这个函数的参数item
既可以是NewArticle
,也可以是Tweet
,即要求这个参数实现了Summary
这个Trait,就可以使用下面的方式定义:
pub fn notify(item: impl Summary) -> String {
format!("{}", item.summarize());
}
impl Trait
只适用于简单情况,如果需要定义的参数较多,可以采用Trait bounds语法:
pub fn notify<T: Summary>(item1: T, item2: T) -> String {
format!("{} {}", item1.summarize(), item2.summarize());
}
实际上,impl Trait
语法就是Trait bounds的一个语法糖。此外,还可以使用+
运算符连接多个需要实现的Trait :
use std::fmt::Display;
pub fn notify<T: Summary + Display>(item1: T, item2: T) -> String {
format!("{} {}", item1.summarize(), item2.summarize());
}
当类型参数的Trait约束过多时,函数名和类型之间相隔距离太远,不利于阅读,所以可以采用where
子句简化这个定义。例如,下面两个函数定义写法的作用完全相同:
pub fn notify1<T: Summary + Display, U: Clone + Debug>(item: T, item2: U) -> String {
format!("{}", item.summarize())
}
pub fn notify2<T, U>(item: T, item2: U) -> String
where
T: Summary + Display,
U: Clone + Debug,
{
format!("{}", item.summarize())
}
2.5 将 Trait 作为函数返回类型
可以指定函数的返回类型实现了某个Trait,下面是例子。
pub fn notify (s: &str) -> impl Summary {
Tweet {
username: String::from("debussy"),
content: String::from("Of course, you can learn Rust well!"),
reply: false,
retweet: false,
}
}
需要注意,将Trait作为函数返回类型时,必须要保证函数返回的类型必须是确定的类型,即必须在编译时就确定函数返回的是什么类型,否则代码就会报错。
2.6 使用 Trait bounds 有条件地实现方法
在使用泛型类型参数的impl块上使用Trait Bound,我们可以有条件地为实现了特定Trait的类型来实现特定的方法:
use std::fmt::Display;
pub struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> { // 所有类型都实现了new方法
pub fn new (x: T, y: T) -> Self { // 注意这里是Self而不是self
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> { // 只有实现了Display和PartialOrd两个Trait的类型才有cmp_display方法
pub fn cmp_display(&self) {
if self.x > self.y {
println!("largest number is x: {}", self.x);
} else {
println!("largest number is y: {}", self.y);
}
}
}
也可以为实现了其他Trait的任意类型有条件地实现某个Trait。为满足Trait Bound的所有类型上实现Trait叫做覆盖实现(blanket implementations)。下面的例子来自标准库:所有实现了Display这个Trait的类型都实现了ToString这个Trait。
pub trait ToString {
// ...
fn to_string(&self) -> String;
}
// ...
impl<T: fmt::Display> ToString for T { // 为所有实现Display的类型实现ToString这个Trait
// ...
default fn to_string(&self) -> String {
// ...
}
}
原创笔记,码字不易,如有谬误欢迎在评论区指出,感激不尽!