Rust的泛型和Trait详解
为什么要用泛型
使用泛型的主要原因是为了提高代码的复用性和灵活性。通过泛型,我们可以编写更通用的代码,而不必为每种具体类型重复编写相同的逻辑。泛型使得代码更具适应性,可以处理多种数据类型,从而减少代码的重复和冗余。
泛型的使用
在函数定义中使用泛型
当使用泛型定义函数时,本来在函数签名中指定参数和返回值的类型的地方,会改用泛型来表示。采用这种技术,使得代码适应性更强,从而为函数的调用者提供更多的功能,同时也避免了代码的重复。
回到 largest
函数,示例 10-4 中展示了两个函数,它们的功能都是寻找 slice 中最大值。接着我们使用泛型将其合并为一个函数。
文件名:src/main.rs
fn largest_i32(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn largest_char(list: &[char]) -> &char {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest_i32(&number_list);
println!("The largest number is {result}");
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest_char(&char_list);
println!("The largest char is {result}");
}
示例 10-4:两个函数,不同点****只是名称和签名类型。
largest_i32
函数是用来寻找 slice 中最大的 i32
。largest_char
函数寻找 slice 中最大的 char
。因为两者函数体的代码是一样的,我们可以定义一个函数,再引进泛型参数来消除这种重复。
为了参数化这个新函数中的这些类型,我们需要为类型参数命名,道理和给函数的形参起名一样。任何标识符都可以作为类型参数的名字。这里选用 T ,因为传统上来说,Rust 的类型参数名字都比较短,通常仅为一个字母,同时,Rust 类型名的命名规范是首字母大写驼峰式命名法(UpperCamelCase)。
T` 作为 “type” 的缩写是大部分 Rust 程序员的首选。
泛型版本的largest函数
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 25, 100, 65];
let result = largest(&number_list);
println!("The largest number is {result}");
let char_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&char_list);
println!("The largest char is {result}");
}
这个定义表明:函数 largest
有泛型类型 T
。它有个参数 list
,其类型是元素为 T
的 slice。largest
函数会返回一个与 T
相同类型的引用。
结构体定义中的泛型
同样也可以用 <>
语法来定义结构体,它包含一个或多个泛型参数类型字段。
文件名:src/main.rs
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 1.0, y: 4.0 };
}
其语法类似于函数定义中使用泛型。首先,必须在结构体名称后面的尖括号中声明泛型参数的名称。接着在结构体定义中可以指定具体数据类型的位置使用泛型类型。
多个泛型参数的结构体
如果想要定义一个 x
和 y
可以有不同类型且仍然是泛型的 Point
结构体,我们可以使用多个泛型类型参数。
文件名:src/main.rs
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let both_integer = Point { x: 5, y: 10 };
let both_float = Point { x: 1.0, y: 4.0 };
let integer_and_float = Point { x: 5, y: 4.0 };
}
\现在所有这些 Point
实例都合法了!你可以在定义中使用任意多的泛型类型参数,不过太多的话,代码将难以阅读和理解。当你发现代码中需要很多泛型时,这可能表明你的代码需要重构分解成更小的结构。
枚举定义中的泛型
枚举和结构体类似,枚举也可以在成员中存放泛型数据类型。
Option枚举的定义
enum Option<T> {
Some(T),
None,
}
Option枚举有一个泛型参数 T
,它有两个成员:Some
存放一个类型 T
的值,None
不存放任何值。通过 Option<T>
枚举可以表达一个可能的值的抽象概念,同时因为 Option<T>
是泛型的,无论这个可能的值是什么类型都可以使用这个抽象。
方法定义中的泛型
在为结构体和枚举实现方法时,也可以使用泛型。
文件名:src/main.rs
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>上定义了一个叫做
x 的方法来返回字段
x` 中数据的引用。
泛型方法中的多种泛型参数
结构体定义中的泛型类型参数并不总是与结构体方法签名中使用的泛型是同一类型。
文件名:src/main.rs
struct Point<X1, Y1> {
x: X1,
y: Y1,
}
impl<X1, Y1> Point<X1, Y1> {
fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
Point {
x: self.x,
y: other.y,
}
}
}
fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
在这个例子中,我们定义了一个 x
和 y
可以有不同类型且仍然是泛型的 Point
结构体。然后,我们实现了一个方法 mixup
,它接受另一个 Point
实例并返回一个新的 Point
实例,其 x
来自调用者,而 y
来自参数。
泛型代码的性能
在阅读本部分内容的同时,你可能会好奇使用泛型类型参数是否会有运行时消耗。好消息是泛型并不会使程序比具体类型运行得慢。
Rust 通过在编译时进行泛型代码的单态化(monomorphization)来保证效率。单态化是一个通过填充编译时使用的具体类型,将通用代码转换为特定代码的过程。在这个过程中,编译器会寻找所有泛型代码被调用的位置,并使用泛型代码针对具体类型生成代码。
编译器生成的单态化版本的代码
enum Option_i32 {
Some(i32),
None,
}
enum Option_f64 {
Some(f64),
None,
}
fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}
编译器通过将泛型定义替换为具体的定义,确保在运行时没有额外的开销。因此,使用泛型的代码在执行效率上与手写每个具体定义的重复代码一样高效。
Rust中的Trait详解(与Go语言对比)
Trait 是 Rust 中的一种功能,用于定义一种类型应该具有的行为。Trait 类似于其他语言中的接口(interfaces),但也有其独特之处。通过Trait,Rust能够实现多态性和代码复用。以下是对Rust中Trait的详细讲解,并与Go中的接口进行对比。
定义Trait
\一个类型的行为由其可供调用的方法构成。如果可以对不同类型调用相同的方法,这些类型就可以共享相同的行为了。Trait 定义是一种将方法签名组合起来的方法,目的是定义一个实现某些目的所必需的行为的集合。
示例:定义一个 Summary Trait
pub trait Summary {
fn summarize(&self) -> String;
}
** **这里使用 trait
关键字来声明一个 Trait,后面是 Trait 的名字,在这个例子中是 Summary
。在大括号中声明描述实现这个 Trait 的类型所需要的行为的方法签名,在这个例子中是 fn summarize(&self) -> String
。在方法签名后跟分号,而不是在大括号中提供其实现。接着每一个实现这个 Trait 的类型都需要提供其自定义行为的方法体。
为类型实现Trait
现在我们定义了 Summary
Trait 的签名,接着就可以在多媒体聚合库中实现这个类型了。下面展示了 NewsArticle
和 Tweet
结构体上 Summary
Trait 的一个实现。
示例:实现 Summary Trait
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)
}
}
\在类型上实现 Trait 类似于实现常规方法。区别在于 impl
关键字之后,我们提供需要实现的 Trait 的名称,接着是 for
和需要实现 Trait 的类型的名称。在 impl
块中,使用 Trait 定义中的方法签名,不过不再后跟分号,而是需要在大括号中编写函数体来为特定类型实现 Trait 方法所拥有的行为。
默认实现
有时为 Trait 中的某些或全部方法提供默认的行为,而不是在每个类型的每个实现中都定义自己的行为是很有用的。
示例:带有默认实现的 Summary Trait
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
如果想要对 NewsArticle
实例使用这个默认实现,可以通过 impl Summary for NewsArticle {}
指定一个空的 impl
块。
Trait作为参数
知道了如何定义 Trait 和在类型上实现这些 Trait 之后,我们可以探索一下如何使用 Trait 来接受多种不同类型的参数。
示例:Trait作为函数参数
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
对于 item
参数,我们指定了 impl
关键字和 Trait 名称,而不是具体的类型。该参数支持任何实现了指定 Trait 的类型。
Trait Bound 语法
impl Trait
语法适用于直观的例子,它实际上是一种较长形式我们称为 Trait Bound
语法的语法糖。
示例:使用Trait Bound语法
pub fn notify<T: Summary>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
多Trait Bound
如果一个函数需要多个 Trait Bound,可以通过 +
语法来指定多个 Trait。
示例:多个Trait Bound
pub fn notify(item: &(impl Summary + Display)) {
println!("Breaking news! {}", item.summarize());
}
pub fn notify<T: Summary + Display>(item: &T) {
println!("Breaking news! {}", item.summarize());
}
使用 where 简化Trait Bound
使用过多的 Trait Bound 会使得函数签名难以阅读。为此,Rust 有一个 where
从句用于简化。
示例:使用 where 简化Trait Bound
fn some_function<T, U>(t: &T, u: &U) -> i32
where
T: Display + Clone,
U: Clone + Debug,
{
// 函数体
}
返回实现了Trait的类型
*也可以在返回值中使用 impl Trait` 语法,来返回实现了某个 Trait 的类型。
示例:返回实现Trait的类型
fn returns_summarizable() -> impl Summary {
Tweet {
username: String::from("horse_ebooks"),
content: String::from("of course, as you probably already know, people"),
reply: false,
retweet: false,
}
}
使用Trait Bound有条件地实现方法
可以有条件地为那些实现了特定 Trait 的类型实现方法。
示例:有条件地实现方法
use std::fmt::Display;
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
整体trait实例
// 定义一个 Summary Trait
pub trait Summary {
fn summarize(&self) -> String;
// 提供一个默认实现
fn summarize_author(&self) -> String {
String::from("(Unknown author)")
}
}
// 实现 Summary Trait 的 NewsArticle 结构体
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)
}
fn summarize_author(&self) -> String {
format!("{}", self.author)
}
}
// 实现 Summary Trait 的 Tweet 结构体
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)
}
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
// 使用 impl Trait 作为函数参数
pub fn notify(item: &impl Summary) {
println!("Breaking news! {}", item.summarize());
}
// 使用泛型参数和 Trait Bound 语法
pub fn notify_generic<T: Summary>(item: &T) {
println!("Generic breaking news! {}", item.summarize());
}
fn main() {
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
};
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
};
// 直接调用 summarize 和 summarize_author 方法
println!("New article available! {}", article.summarize());
println!("Article author: {}", article.summarize_author());
println!("New tweet available! {}", tweet.summarize());
println!("Tweet author: {}", tweet.summarize_author());
// 调用 notify 函数
notify(&article);
notify(&tweet);
// 调用 notify_generic 函数
notify_generic(&article);
notify_generic(&tweet);
}
在这个示例中,我们定义了一个 Summary
Trait,并为 NewsArticle
和 Tweet
结构体实现了该 Trait。每个实现都提供了 summarize
方法,并覆盖了 summarize_author
方法。我们还定义了两个函数 notify
和 notify_generic
,它们接受实现了 Summary
Trait 的类型作为参数。最后,我们在 main
函数中创建了 NewsArticle
和 Tweet
的实例,并调用了它们的 summarize
方法以及 notify
和 notify_generic
函数。
New article available! Penguins win the Stanley Cup Championship!, by Iceburgh (Pittsburgh, PA, USA)
Article author: Iceburgh
New tweet available! horse_ebooks: of course, as you probably already know, people
Tweet author: @horse_ebooks
Breaking news! Penguins win the Stanley Cup Championship!, by Iceburgh (Pittsburgh, PA, USA)
Breaking news! horse_ebooks: of course, as you probably already know, people
Generic breaking news! Penguins win the Stanley Cup Championship!, by Iceburgh (Pittsburgh, PA, USA)
Generic breaking news! horse_ebooks: of course, as you probably already know, people
Rust中的 dyn
Trait(可选看)
在Rust中,Trait是一种定义某类类型行为的机制。通过Trait,我们可以实现多态性,使得不同的类型可以共享相同的行为。在某些情况下,我们希望在运行时确定具体的类型,这时就需要使用动态分发。Rust中通过 dyn
关键字来实现动态分发。
什么是动态分发
动态分发(dynamic dispatch)是一种在运行时确定调用哪个具体实现的方法。这与静态分发(static dispatch)不同,后者在编译时就确定了具体调用哪个方法。在Rust中,动态分发通过Trait对象(trait objects)实现,而Trait对象则通过 dyn
关键字来表示。
定义和使用Trait对象
Trait对象允许我们将实现了某个Trait的不同类型存储在同一个数据结构中。通过 dyn
关键字,可以将Trait作为一种类型使用。
示例:使用Trait对象
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)
}
}
fn main() {
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
};
let tweet = Tweet {
username: String::from("horse_ebooks"),
content: String::from(
"of course, as you probably already know, people",
),
reply: false,
retweet: false,
};
// 创建一个Trait对象的向量
let items: Vec<&dyn Summary> = vec![&article, &tweet];
for item in items {
println!("Summary: {}", item.summarize());
}
}
在这个示例中,我们定义了一个 Summary
Trait,并为 NewsArticle
和 Tweet
结构体实现了这个 Trait。在 main
函数中,我们创建了一个存储 &dyn Summary
类型的向量 items
,其中包含了 NewsArticle
和 Tweet
的实例。通过迭代这个向量,我们可以调用每个项的 summarize
方法。
dyn
Trait的用法和限制
- 动态分发的开销:使用
dyn
Trait 时,每次方法调用都要进行动态分发,因此会有一定的运行时开销。静态分发在编译时确定方法调用,因此性能更高。 - 对象安全性:并非所有的Trait都能用作Trait对象。为了使Trait可以用于动态分发,它必须是对象安全的。对象安全性要求Trait的方法签名中不包含
Self
或者泛型参数。 - Trait对象不能有泛型方法:由于Trait对象的类型在编译时未知,因此泛型方法无法在Trait对象中使用。
对象安全性示例
为了使Trait成为对象安全的,它必须满足以下条件:
- 方法的返回类型不是
Self
。 - 方法没有泛型参数。
对象安全的Trait示例
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)
}
}
fn main() {
let article = NewsArticle {
headline: String::from("Penguins win the Stanley Cup Championship!"),
location: String::from("Pittsburgh, PA, USA"),
author: String::from("Iceburgh"),
content: String::from(
"The Pittsburgh Penguins once again are the best \
hockey team in the NHL.",
),
};
// 使用Trait对象
let item: &dyn Summary = &article;
println!("Summary: {}", item.summarize());
}
Go中的接口
Go语言中的接口定义了一组方法签名,任何实现了这些方法的类型都实现了该接口。
示例:定义和实现接口
package main
import "fmt"
type Stringer interface {
String() string
}
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d)", p.Name, p.Age)
}
func main() {
var p Stringer = Person{Name: "Alice", Age: 30}
fmt.Println(p.String())
}
在这个例子中,我们定义了一个 Stringer
接口和一个 Person
结构体。Person
结构体实现了 Stringer
接口的 String
方法。因此,Person
类型满足 Stringer
接口。
Rust和Go中的Trait和接口对比
- 定义方式:
- Rust:使用
trait
关键字定义,方法签名可以有默认实现。 - Go:使用
interface
关键字定义,只包含方法签名,没有方法实现。
- Rust:使用
- 实现方式:
- Rust:使用
impl Trait for Type
语法在类型上实现Trait。 - Go:通过实现接口方法,隐式地实现接口。
- Rust:使用
- 使用方式:
- Rust:Trait 可以作为函数参数和返回值,并且可以使用Trait Bound来指定泛型类型。
- Go:接口可以作为函数参数和返回值,接口类型本身就是一种类型。
- 多态性:
- Rust:通过Trait实现多态性,可以使用动态分发(trait对象)和静态分发(泛型约束)。
- Go:通过接口实现多态性,所有实现了接口的类型都可以作为接口类型使用。
- 条件实现:
- Rust:可以有条件地为实现了特定Trait的类型实现方法。
use std::fmt::Display; // 定义一个泛型结构体 Wrapper struct Wrapper<T> { value: T, } // 实现 Wrapper 的关联函数 new impl<T> Wrapper<T> { fn new(value: T) -> Self { Wrapper { value } } } // 有条件地为 Wrapper 实现方法 print_value impl<T: Display> Wrapper<T> { fn print_value(&self) { println!("Value: {}", self.value); } } fn main() { // 创建实现了 Display 的类型的 Wrapper 实例 let wrapper_with_display = Wrapper::new(42); wrapper_with_display.print_value(); // 可以调用 print_value 方法 // 创建没有实现 Display 的类型的 Wrapper 实例 let wrapper_without_display = Wrapper::new(vec![1, 2, 3]); // wrapper_without_display.print_value(); // 编译错误,无法调用 print_value 方法 }
- Go:不支持条件实现。
- Rust:可以有条件地为实现了特定Trait的类型实现方法。