👈 6 方法
泛型
多态:多态就像一个通用炮管,既可以发射普通炮弹,也可以发射制导炮弹,没有必要为每一种炮弹设计一个专用炮管。
泛型:泛型就是一种多态,目的在于提供编程的便利,简化代码。
fn add<T>(a: T, b: T) -> T {
a + b
}
fn main() {
println!("add i8: {}", add(2i8, 3i8));
println!("add i32: {}", add(20, 30));
println!("add f64: {}", add(1.23, 1.23));
}
它无法过编,只是为了展示泛型的概念。
T
为泛型参数,名称任意,按惯例取 type
的首字母 T
。
在使用泛型参数前,需要先声明:
fn largest<T>(list: &[T]) -> T
这是一个泛型函数的签名,其作用为从元素类型为 T
的列表中找到最大的值。<T>
是对 T
的声明。
// not ok
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
这是一个错误的实现,编译器提示运算符 >
不能用于类型 T
,并建议为 T
添加一个类型限制,使用 std::cmp::PartialOrd
特征对 T
进行限制,该特征的目的在于让类型实现可比较的功能。
// ok
fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
if item > largest {
largest = item;
}
}
largest
}
这样修改就可以过编了。
对于最初的 a + b
,该泛型 T
需实现 std::ops::Add<Output = T>
进行限制。
// ok
fn add<T: std::ops::Add<Output = T>>(a:T, b:T) -> T {
a + b
}
泛型的使用
在结构体中使用泛型
struct Point<T> {
x: T,
y: T,
}
结构体 Point
定义了一个坐标点,可以存放任何类型的坐标值:
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
使用说明:
- 类似于泛型函数的定义,在使用泛型参数前需要先声明,然后才能用
T
代替具体的类型。 - 创建
Point
实例时,同一个点的x
和y
必须类型相同。
// not ok
let p = Point{ x: 1, y: 2.0 }; // err: mismatched types
当把 1
赋给 x
时,T
的类型就确定为 i32
,随后 y
却接收了一个 f32
的值,因此出现错误。
如果希望 x
与 y
既能够类型相同、又能够类型不同,需要使用两种泛型参数:
// ok
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let p = Point{ x: 1, y: 2.0 };
}
需要避免泛型的滥用,如果声明了一个 struct Foo<T, U, V, W, X>
,就要考虑拆分结构体了。
在枚举中使用泛型
之前多次出现的 Option
用于枚举一个值存在与否:
enum Option<T> {
Some(T),
None,
}
与之类似的枚举 Result
用于枚举一个值正确与否:
enum Result<T, E> {
Ok(T),
Err(E),
}
- 如果函数正常运行,返回一个
Ok(T)
,T
代表具体的返回类型。 - 如果运行错误,返回一个
Err(E)
,E
代表错误类型。
例如,如果成功打开文件,返回 Ok(std::fs::File)
,否则返回 Err(std::io::Error)
。
在方法中使用泛型
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.x());
}
结构体 Point
的声明中就有泛型参数 T
,其 impl
块就应声明为 impl<T> Point<T>
,不过,块内的方法也可以使用其他的泛型类:
impl<T> Point<T> {
fn x<U>(&self) -> &U {
&self.x
}
}
const
泛型
[i32; 2]
与 [i32; 3]
是不同的数组类型:
fn main() {
let arr1: [i32;2] = [1,2];
display_array(arr); // ok
let arr2: [i32; 3] = [1, 2, 3];
display_array(arr); // err: mismatched types
}
fn print_array(arr: [i32; 2]) {
println!("{:?}", arr);
}
修改 print_array
使其能够打印任意长度的 i32
数组:
fn print_array(arr: &[i32]) { // 参数改为数组切片, 调用时传入引用
println!("{:?}", arr);
}
进一步修改,使其能够打印任意类型的数组:
// 限制 std::fmt::Debug 表明 T 可以用在 println! 中
fn print_array<T: std::fmt::Debug>(arr: &[T]) {
println!("{:?}", arr);
}
引用能满足打印不定长数组的需求,但如果需要的是数组本身呢?
const
泛型是针对值的泛型,可以解决这个问题:
fn print_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
println!("{:?}", arr);
}
现在,参数 arr
的类型为 [T; N]
,T
是基于类型的泛型参数,用以指代 i32
、f32
等类型;而 N
是基于值的泛型参数,用以指代 1
、4
等值,在这里指代数组的长度,这就是 const
泛型,它基于的值类型为 usize
。
泛型的性能
泛型不会对运行时性能产生影响,只会拖慢编译速度、增大文件大小。
特征
在实现一个文件系统时,将其与底层存储解耦很重要,但不可能为每种情况单独实现一套代码。将文件的各种操作抽象出来,就是特征(trait)的概念,它类似于其他语言的“接口”。
之前就使用过特征:
#[derive(Debug)]
,在自定义的类型上自动派生Debug
特征,以便用println!("{:?}", x)
打印出来。- 使用
std::ops::Add
特征限制泛型T
,使T
能够进行加法操作。
特征定义了一组可以被共享的行为,只要实现某特征,就可以使用这组行为。
特征的定义
如果若干类型具有相同的行为,就可以定义一个特征,并为这些类型实现。
例如,现在有文章 Post
与博客 Blog
两种内容载体,而文章的内容都可以“总结”,这个“总结”的行为就是共享的,由此可以定义特征 Summary
:
pub trait Summary {
fn summarize(&self) -> String;
}
trait
:关键字,声明一个特征。Summary
:特征名。summarize
:特征的方法,只有签名,以;
结尾。
为类型实现特征
为 Post
与 Blog
实现 Summary
特征:
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct Post {
pub title: String,
pub author: String,
pub content: String,
}
pub struct Blog {
pub username: String,
pub content: String,
}
impl Summary for Post {
fn summarize(&self) -> String {
format!("title: {}, author: {}", self.title, self.author)
}
}
impl Summary for Blog {
fn summarize(&self) -> String {
format!("user: {}, content: {}", self.username, self.content)
}
}
fn main() {
let post = Post {
title: "Rust Course".to_string(),
author: "Sunface".to_string(),
content: "Rust is the best lang!".to_string()
};
println!("{}", post.summarize());
let blog = Blog {
username: "Sunface".to_string(),
content: "learn Rust!".to_string()
};
println!("{}", blog.summarize());
}
孤儿规则:如需为类型 A
实现特征 T
,那么二者至少有一个的定义位于当前作用域内。
- 可以为
Post
实现标准库中的Display
特征。 - 可以在当前包中为标准库的
String
类型实现Summary
特征。
默认实现:可以在特征定义中为方法进行默认实现。
pub trait Summary {
fn summarize(&self) -> String {
String::from("This is a default impl for summarize()")
}
}
之后,为结构实现 Summary
特征时可以选择性地重载 summarize
。
默认实现也可以调用特征中的其他方法,被调用方法可以有、也可以没有默认实现。
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("author: {}", self.summarize_author())
}
}
特征作为函数参数
pub fn notify(item: &impl Summary) {
println!("NOTE: {}", item.summarize());
}
item
的类型为 impl Summary
类型的不可变引用,意为“实现了 Summary
特征”。
notify()
可以接收任何实现了 Summary
特征的类型作为参数,例如 Post
的实例,并可以在内部调用 Summary
的方法。
特征约束:impl Trait
是一种语法糖,完整的写法:
pub fn notify<T: Summary>(item: &T) {
println!("NOTE: {}", item.summarize());
}
特征约束对于复杂的场景很有用,例如,notify()
现在接收两个 &impl Summary
参数:
pub fn notify(item1: &impl Summary, item2: &impl Summary)
如果我们希望限制 item1
、item2
为同一实际类型(例如两个 Post
实例,而不允许一个 Post
实例与一个 Blog
实例),语法糖就无能为力了,但是特征约束就可以做到:
pub fn notify<T: Summary>(item1: &T, item2: &T)
泛型 T
说明 item1
与 item2
的类型相同,T: Summary
说明 T
需要实现 Summary
特征。
多重约束:约束参数实现多个特征。
pub fn notify(item: &(impl Summary + Display))
pub fn notify<T: Summary + Display>(item: &T)
where
约束:
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
通过 impl Trait
指示函数返回一个实现了 Trait
特征的类型:
fn returns_summarizable() -> impl Summary {
Blog {
username: String::from("Sunface"),
content: String::from("learn Rust!"),
}
}
impl Trait
返回类型通常用于实际类型极其复杂的情况,因为 Rust 要求必须指明所有类型。例如,返回一个迭代器时,其具体类型极其复杂,就可以将返回类型写为 impl Iterator
。
不过,这种抽象的返回类型只能指代一个具体的类型,例如:
// err
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
Post {
...
}
} else {
Weibo {
...
}
}
}
这是不行的,因为 impl Summary
不可以指代两个不同的类型,编译器提示 “`if` and `else` have incompatible types”。
要实现返回不同的类型,需使用特征对象(见下一章)。
修复 largest()
运算符 >
是标准库中特征 std::cmp::PartialOrd
的默认方法,将 largest()
签名修改为:
fn largest<T: PartialOrd>(list: &[T]) -> T
仍然无法过编,编译器提示 T
没有实现 Copy
特性,需要增加约束:
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T
👉 8 集合类型