👈 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 实例时,同一个点的 xy 必须类型相同。
// not ok
let p = Point{ x: 1, y: 2.0 }; // err: mismatched types

当把 1 赋给 x 时,T 的类型就确定为 i32,随后 y 却接收了一个 f32 的值,因此出现错误。

如果希望 xy 既能够类型相同、又能够类型不同,需要使用两种泛型参数:

// 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 是基于类型的泛型参数,用以指代 i32f32 等类型;而 N 是基于值的泛型参数,用以指代 14 等值,在这里指代数组的长度,这就是 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:特征的方法,只有签名,以 ; 结尾。

为类型实现特征

PostBlog 实现 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)

如果我们希望限制 item1item2 为同一实际类型(例如两个 Post 实例,而不允许一个 Post 实例与一个 Blog 实例),语法糖就无能为力了,但是特征约束就可以做到:

pub fn notify<T: Summary>(item1: &T, item2: &T)

泛型 T 说明 item1item2 的类型相同,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 集合类型