泛型是类型编程中的一种工具。本质上是类型的变量,目的是提高代码的复用。泛型是
具体类型或其他属性的抽象替代。
为了复用,我们会使用函数将功能封装,同样,泛型也是为了复用,只不过是为了类型的复用。
例如:Option
函数封装步骤:
1. 找到重复的代码。
2. 将重复的代码提取至函数体中,考虑函数参数和返回值。
采用同样步骤考虑函数加入泛型,让函数可以处理更多的参数类型和返回值类型。
fn largest(list: &[T]) -> T
使用trait来定义通用行为。在定义泛型时,使用trait可以将其限制为拥有某些特定行为的类型,而不是任意类型。
- struct Point
{ - x: T,
- y: T,
- }
-
- let wont_work = Point { x: 5, y: 4 }; // T的类型被自动推断为i32
-
-
- struct Point
{ // 泛型也可以定义多个 - x:T,
- y:U,
- }
枚举中定义泛型:
- enum Option
{ - Some(T),
- None,
- }
-
- enum Result
{ - Ok(T),
- Err(E),
- }
结构体和枚举的函数:
- struct Point
{ - x: T,
- y: T,
- }
-
- impl
Point { // 结构体函数定义泛型,枚举的函数同理 - fn x(&self) -> &T { // 返回结构体中属性x的T类型值的引用
- &self.x
- }
- }
Point
必须紧跟着impl关键字声明T,以便能够在实现方法时指定类型Point
通过在impl之后将T声明为泛型,Rust能够识别出Point尖括号内的类型是泛型而不是具体类型。
因为可以单独为Point
- impl Point<f32> {
- fn distance_from_origin(&self) -> f32 {
- (self.x.powi(2) + self.y.powi(2)).sqrt()
- }
- }
这里的impl代码块只作用于使用具体类型替换了泛型参数T的结构体
这段代码意味着,类型Point
结构体定义中的泛型参数并不总是与我们在方法签名上使用的类型参数一致。
- impl
Point { - fn mixup
(self, other: Point) -> Point { - Point {
- x: self.x,
- y: other.y,
- }
- }
- }
泛型可以向编译器提供引用之间的相互关系,它允许我们在借用值时通过编译器来确保这些引用的有效性。
Rust实现泛型的方式决定了使用泛型的代码与使用具体类型的代码相比不会有任何速度上的差异,原因是:
Rust会在编译时执行泛型代码的单态化(monomorphization)。单态化 是一个在编译期将泛型代码转换为特定代码的过程,它会将所有使用过的具体类型填入泛型参数从而得到有具体类型的代码。
trait(特征)被用来向Rust编译器描述某些特定类型拥有的且能够被其他类型共享的功能,它使我们可以以一种抽象的方式来定义共享行为。
trait提供了一种将特定方法签名组合起来的途径,它定义了为达成某种目的所必需的行为集合。
使用trait关键字来声明trait。花括号用于定义类型行为的方法签名。
- pub trait Summary {
- fn summarize(&self) -> String;
- }
一个trait可以包含多个方法:每个方法签名占据单独一行并以分号结尾。
- 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)
- }
- }
通过impl和for关键字,结构体NewsArticle实现了Summary trait。
当第三方开发者想要为他们自定义的结构体实现Summary trait并使用相关功能时,就必须将这个trait引入自己的作用域中
实现trait有一个限制:只有当trait或类型定义于我们的库中时,我们才能为该类型实现对应的trait。
这段话的意思是,trait和类型,至少一个属于自己的库的时候才可以为类型实现trait:
1. trait是自己定义的:那么也可以为标准库的类型来impl trait
2. 类型是自己定义的:那么可以为该类型实现从第三方引入的trait
不能为外部类型实现外部trait
trait重定义的是抽象的方法声明。但也可以具体实现,即默认实现。
- pub trait Summary {
- fn summarize(&self) -> String {
- String::from("(Read more...)")
- }
- }
默认实现中调用相同trait中的其他方法,即使该方法并没有默认实现
- pub trait Summary {
- fn summarize_author(&self) -> String;
-
- fn summarize(&self) -> String {
- format!("(Read more from {}...)", self.summarize_author())
- }
- }
可以将trait理解为接口模板,也类似Java里的abstract类。
使用trait来定义接收不同类型参数的函数
- pub fn notify(item: impl Summary) {// 这里使用impl关键字,表示参数实现了Summary trait
- println!("Breaking news! {}", item.summarize());
- }
在调用notify时向其中传入任意一个实现了Summary的结构体实例
impl Trait是trait约束的语法糖。
等同于使用泛型:
- pub fn notify
(item: T) { // 泛型T添加了约束Summary - println!("Breaking news! {}", item.summarize());
- }
泛型参数与trait约束同时放置在尖括号中,并使用冒号分隔。
- pub fn notify(item1: impl Summary, item2: impl Summary) {
-
- pub fn notify
(item1: T, item2: T) { // 相对比,使用泛型更简约
通过+语法来指定多个trait约束
- pub fn notify(item: impl Summary + Display) {
-
- pub fn notify
(item: T) {
使用where从句来简化trait约束
将泛型的约束提取到where语句中:
- fn some_function
(t: T, u: U) -> i32 - where T: Display + Clone,
- U: Clone + Debug
- {
可以在返回值中使用impl 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,
- }
- }
impl Trait可以精练地声明函数会返回实现了trait的类型,而不需要写出具体的类型。
你只能在返回一个类型时使用impl Trait
通过在带有泛型参数的impl代码块中使用trait约束,我们可以单独为实现了指定trait的类型编写方法。
- use std::fmt::Display;
-
- struct Pair
{ - x: T,
- y: T,
- }
-
- impl
Pair { - fn new(x: T, y: T) -> Self {
- Self {
- x,
- y,
- }
- }
- }
-
- impl
PartialOrd> Pair { // 这里定义的cmp_display方法只能是在实现了Display和PortialOrd的trait的泛型实例上使用 - 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);
- }
- }
- }
类型Pair
可以为实现了某个trait的类型有条件地实现另一个trait
impl ToString for T {
标准库对所有满足Display trait约束的类型实现了ToString trait。
之前学到的是for关键字后面是结构体或枚举,但这里是泛型T;impl后面的ToString是trait。
可以为任何实现了Display trait的类型调用ToString trait中的to_string方法
生命周期是另外一种泛型
生命周期能够确保引用在我们的使用过程中一直有效
Rust的每个引用都有生命周期,对应着引用保持有效性的作用域。
生命周期都是隐式且可以被推导出来的,但当引用的生命周期以不同方式相互关联时,必须手动标注生命周期。Rust需要注明泛型生命周期参数之间的关系,确保运行时实际使用的引用一定是有效的。
- {
- let r;
- {
- let x = 5;
- r = &x; // 外部的变量,borrow借用了内部作用域的变量。但是x指向的内存在内部作用域结束的时候会被销毁。
- }
- println!("r:{}",r);
- }
上面代码r借用了内部作用域变量x。当内部作用域结束的时候x变量被销毁。因此,r会指向空的内存地址。如果还有其他代码赋值,可能就引用到非法的地址,造成内存内容泄露,造成安全问题。
用于比较不同的作用域并确定所有借用的合法性:
被引用对象的生命周期(存在范围)是否短于引用者(的生命周期)。
返回函数中引用生命周期最短的那个:
fn longest<'a>(str1:&'a str,str2:&'a str)->&'a str{
总结:1. 函数参数都是引用
2. 其次生命周期参数的标注类似泛型。以'单引号开头,用来区别泛型。
3. 手动标注生命周期参数的目的是告诉编译器,参数和返回值的引用生命周期的长度。因为如果不加泛型生命周期,Rust借用检查器无法判断这些引用的作用域范围。(如果单单给出函数的签名,相信我们自己也无法做出判断)
返回第一个参数引用:
fn longest<'a>(x: &'a str, y: &str) -> &'a str {
总结:
返回的引用和第二个参数没有关系,因此,无需标注生命周期参数
返回引用和参数没有关系:
- fn longest<'a>(x: &str, y: &str) -> &'a str {
- let result = String::from("really long string");
- result.as_str()
- }
编译器会报错。因为返回引用和参数都没关系,因此,返回的引用必然是函数内部变量的引用。
因为函数结束后,内部变量会被销毁,返回的引用会造成悬垂引用。因此,编译器无法通过编译。
结构体中定义过自持有类型,也可以在结构体中存储引用,需要为结构体定义中的每一个引用都添加生命周期标注
- struct ImportantExcept<'a>{
- part:&'a str,
- }
标注意味着ImportantExcerpt实例的存活时间不能超过存储在part字段中的引用的存活时间。
参数名称以撇号(')开头,使用全小写字符。'a被大部分开发者选择作为默认使用的名称。
生命周期参数的标注填写在&引用运算符之后,并通过一个空格符来将标注与引用类型区分开来
如同使用了泛型参数的函数可以接收任何类型一样,使用了泛型生命周期的函数也可以接收带有任何生命周期的引用
- let novel = String::from("Call me Ishmael.Some years ago...");
-
- let first_sentence = novel.split('.').next().expect("Could not find a '.'");
-
- let i = ImportantExcerpt{
- part:first_sentence
- };
-
- // 标注意味着ImportantExcerpt实例的存活时间不能超过存储在part字段中的引用的存活时间
- struct ImportantExcerpt<'a>{
- part:&'a str,
- }
novel会在ImportantExcerpt离开作用域后才离开作用域,所以ImportantExcerpt实例中的引用总是有效的。
任何引用都有一个生命周期,并且需要为使用引用的函数或结构体指定生命周期参数。
- fn first_word(s: &str) -> &str {
- let bytes = s.as_bytes();
- for (i, &item) in bytes.iter().enumerate() {
- if item == b' ' {
- return &s[0..i];
- }
- }
- &s[..]
- }
对于可预测的场景,Rust将这些模式直接写入编译器,使用借用检查器自动对这些生命周期进行推导无需显示标注。
这些被写入Rust引用分析部分的模式也就是所谓的生命周期省略规则
函数参数或方法参数中的生命周期被称为输入生命周期(input lifetime),而返回值的生命周期则被称为输出生命周期(output lifetime)。
Rust编译器使用了3种规则计算引用的生命周期:
1. 每一个引用参数都会拥有自己的生命周期参数
2. 当只存在一个输入生命周期参数时,这个生命周期会被赋予给所有输出生命周期参数(该规则使用上面代码实例)
3. 当拥有多个输入生命周期参数,而其中一个是&self或&mut self时,self的生命周期会被赋予给所有的输出生命周期参数。
区别于函数,方法是针对结构体和枚举实现的方法
声明和使用生命周期参数的位置取决于它们是与结构体字段相关,还是与方法参数、返回值相关。
结构体字段中的生命周期:
生命周期的名字总是需要被声明在impl关键字之后,并被用于结构体名称之后,因为这些生命周期是结构体类型的一部分。
- struct ImportantExcerpt<'a>{
- part:&'a str,
- }
-
- impl<'a> ImportantExcerpt<'a>{
- fn level(&self) -> i32{
- 3
- }
- }
&self生命周期参数会应用于输出生命周期参数
声明在impl及类型名称之后的生命周期是不能省略的
- impl<'a> ImportantExcerpt<'a>{
-
- fn announce_and_return_part(&self,announcement:&str) -> &str{
- println!("Attention please:{}",announcement);
- &self.part
- }
- }
&self的生命周期会应用于输出生命周期参数
静态生命周期'static, 表示整个程序执行期间。
所有字符换字面量都拥有静态生命周期,因为字符串的文本被直接存储在二进制程序中,并总是可用的。
1. 泛型:允许代码应用于不同类型
2.trait与trait约束:在代码中指定泛型的行为
3. 生命周期:用来确保代码不会出现悬垂引用