Rust之泛型特化

简介: Rust之泛型特化

泛型特化


Rust语言支持泛型特化,听说现在已经可用的,有的已经用上了。


Rust不支持函数/结构体的特化,它支持的是针对 impl 块的特化。我们可以为一组类型,impl 一个 trait,同时为其中的一部分更特殊的类型,impl 同一个 trait。


示例如下:


use std::fmt::Display;
trait Example {
    fn call(&self);
}
impl<T> Example for T
{
    default fn call(&self) {
        println!("most generic");
    }
}
impl<T> Example for T
    where T: Display
{
    default fn call(&self) {
        println!("generic for Display, {}", self);
    }
}
impl Example for str {
    fn call(&self) {
        println!("specialized for str, {}", self);
    }
}
fn main() {
    let v1 = vec![1i32,2,3];
    let v2 = 1_i32;
    let v3 = "hello";
    v1.call();
    v2.call();
    v3.call();
}


执行结果:


most generic
generic for Display, 1
specialized for str, hello


这段代码中,有三个 impl 块。


第一个是,针对所有类型,实现 Example。


第二个是,针对所有的满足 T:Display 的类型,实现 Example。


第三个是,针对具体的 str 类型,实现 Example。一个比一个更具体,更特化。


对于主程序中 v1.call()调用,因为 Vec<i32> 类型只能匹配第一个 impl 块,因此它调用的是那个最基本的版本。


对于主程序中的 v2.call()调用,因为 i32 类型满足 Display 约束,所以它能选择第一个或者第二个 impl 块的实现版本,而第二个 impl 块比第一个更具体,更匹配,所以编译器选择了调用第二个 impl 块的版本。


对于 v3.call()这句代码,实际上三个 impl 块都能和 str 类型相匹配,但是,第三个 impl 块明显比其它两个 impl 块更“特化”。因此在主程序中调用的时候,选择了执行第三个 impl 块提供的版本。


在这个示例中,前面的 impl 块针对的类型范围更广,后面的 impl 块针对的类型更具体,它们针对的类型集合是包含关系,这就是特化。当编译器发现,对某个类型有多个 impl 都能满足条件的时候,它会自动选择使用那个最特殊最匹配的版本。


特化的意义


  1. 性能优化。泛型特化可以为某些情况提供统一抽象下的特殊实现。


  1. 代码重用。泛型特化可以提供一些默认(但不完整的)实现,某些情况下可以减少重复代码。


  1. 为“高效继承”铺路。泛型特化其实跟OOP中的继承很像。


拿标准库中的代码,举例说明。


标准库中存在一个 ToString trait,它的定义如下:


pub trait ToString {
    fn to_string(&self) -> String;
}


凡是实现了这个 trait 的类型,都可以调用 to_string 来得到一个 String 类型的结果。同时,标准库中还存在一个 std::fmt::Display trait,其实也可以做到类似的事情。而且 Display 是可以通过 #[derive(Display)] 由编译器自动实现的。所以,我们可以想到,针对所有满足 T: Display 的类型,我们可以为它们提供一个统一的实现:


impl<T: fmt::Display + ?Sized> ToString for T {
    #[inline]
    fn to_string(&self) -> String {
        use core::fmt::Write;
        let mut buf = String::new();
        let _ = buf.write_fmt(format_args!("{}", self));
        buf.shrink_to_fit();
        buf
    }
}


这样一来,我们就没必要针对每一个具体类型来实现 ToString。这么做非常有利于代码重用。所有的满足 T:Display 的类型,都自动拥有了 to_string 方法。这么做代码确实简洁了,但是,对于某些类型,比如说 &str 类型想调用 to_string 方法,效率就有点让人忧伤了。因为这段代码针对的是所有满足 Display 约束的类型来实现的,它调用的是 fmt 模块的功能,内部实现非常复杂而繁琐。如果我们用 &str 类型调用 to_string 方法的话,还走这么复杂的一套逻辑。这也是为什么在早期的 rust 代码中,&str 转 String 类型比较推荐的是以下方式:


// 推荐
let x : String = "hello".into();
// 推荐
let x : String = String::from("hello");
// 不推荐,因为效率低
let x : String = "hello".to_string();


现在有了泛型特化,这个性能问题就可以修复了,方案如下:


// 通用版的 impl,更普适
impl<T: fmt::Display + ?Sized> ToString for T {
    #[inline]
    default fn to_string(&self) -> String {
        ...
    }
}
// 针对 str 的 impl,效率更高
impl ToString for str {
    #[inline]
    fn to_string(&self) -> String {
        String::from(self)
    }
}


我们可以把那个更通用的版本,加一个 default 标记,然后再为 str 类型专门写一个特殊的实现。这样,对外接口依然保持了统一,但内部实现有所区别。尽可能的提高了效率,满足了“零开销抽象”的原则。


使用泛型特化


下面举例来使用一下泛型特化,可以看到它的好处,不但复用了代码,且使得接口更好用了。


以实现结构体序列化为例子,如果不使用泛型特化,势必需要为每个结构体都实现自己的序列化和反序列化。看下使用泛型特化后的效果:


use serde::{Serialize,Deserialize};
use serde::de::DeserializeOwned;
use serde_json::Result;
pub trait BaseSaver: Sized {
    fn save(&self) -> Result<()>;
    //fn load(&self){}
}
impl<T> BaseSaver for T
    where T: Serialize+DeserializeOwned
{
    fn save(&self) -> Result<()>{
        let j = serde_json::to_string(&self)?;
        let name = std::any::type_name::<T>();
        let k = name.rsplit_once("::");
        println!("k={:?}", k);
        let v:Vec<&str> = name.split("::").collect();
        let name = format!("{}.json", v[v.len()-1]);
        println!("{},T is {}", j,v[v.len()-1]);
        let mut file = File::create(name).expect("create failed");
        file.write_all(j.as_bytes()).expect("write failed");
        Ok(())
    }
}


#[derive(Debug,Serialize, Deserialize)]
pub struct LvMsg {
    pub user: String,
    pub email: String,
    pub text: String,
    pub times: String,
    pub url: String,
}
/// StatCfg 留言统计
#[derive(Debug,Serialize, Deserialize)]
pub struct StatCfg {
    pub total_cnt: u32,
    // 访客IP
    pub ips: Vec<String>,
    // 访客留言
    pub msgs: Vec<LvMsg>,
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_save_load() {
       println!("hello test");
       let mut cnf = StatCfg{total_cnt:0,ips:vec![],msgs:vec![]};
       cnf.total_cnt = 10;
       cnf.ips.push("127.0.0.1".into());
       cnf.msgs.push(LvMsg{user:"a".into(),email:"534117529@qq.com".into(),text:"aab".into(),times:"11".into(),url:"ht".into()});
       cnf.save();
       println!("result is {:#?}", cnf);
    }
}


从以上示例可以看到,我们没有为 StatCfg结构体实现save()接口,然而它已经有了save()方法。调用自身的save()即完成了序列化,是不是很神奇很好用啊,这样使用起来简单多了。


再看一例


From和Into


这2个Trait定义在标准库的convert模块中,其实他们做的是同一件事情。


Rust还为此定义了一个定理:如果类型A实现了From,则B类型实例调用into方法就可以转换为类型A。


例如,我们常见的字符串String类型实现了From(&str),那么&str就可以into()为String。大多数情况下,我们只需要实现From这个Trait即可,Rust为所有实现From的自动实现了反方向Into。这个实现也是泛型特化的体现。



引用


泛型特化 Specialization - 知乎

相关文章
|
2月前
|
Rust 编译器
Rust 泛型
Rust 泛型
38 1
|
2月前
|
存储 Rust 程序员
【一起学Rust | 基础篇 | rust新特性】Rust 1.65.0——泛型关联类型、let-else语句
【一起学Rust | 基础篇 | rust新特性】Rust 1.65.0——泛型关联类型、let-else语句
68 0
|
2月前
|
Rust 编译器
rust 泛型和特征(三)
rust 泛型和特征
53 0
|
2月前
|
Rust 编译器
rust 泛型和特征(二)
rust 泛型和特征
35 0
|
2月前
|
Rust
rust 泛型和特征(一)
rust 泛型和特征
54 0
|
Rust 安全 Java
Rust学习笔记之泛型、trait 与生命周期
1. 泛型大补汤 推荐阅读指数 ⭐️⭐️⭐️⭐️ 2. 泛型数据类型 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️ 3. trait:定义共享的行为 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️ 4. 生命周期与引用有效性 推荐阅读指数 ⭐️⭐️⭐️⭐️⭐️
Rust学习笔记之泛型、trait 与生命周期
|
存储 Rust 安全
【Rust 中级教程】 01 泛型
【Rust 中级教程】 01 泛型
【Rust 中级教程】 01 泛型
|
存储 Rust 编译器
【Rust 中级教程】 02 结构体与泛型
【Rust 中级教程】 02 结构体与泛型
|
27天前
|
Rust 安全 开发者
探索Rust语言的内存安全特性
【6月更文挑战第8天】Rust语言针对内存安全问题提供了创新解决方案,包括所有权系统、借用规则和生命周期参数。所有权系统确保值与其所有者绑定,防止内存泄漏;借用规则保证同一时间只有一个可变引用或多个不可变引用,消除数据竞争和野指针;生命周期参数则强化了引用的有效范围,提升安全性。通过这些特性,Rust帮助开发者编写出更健壮、安全的高性能软件,有望成为系统编程领域的领头羊。
|
1月前
|
机器学习/深度学习 Rust 安全
Rust语言:为何备受开发者青睐?
Rust编程语言以其内存安全、高性能、并发编程支持和强大社区获得青睐。作为系统编程语言,Rust的所有权与借用检查机制确保了内存安全,适用于高可靠性系统。它拥有接近C/C++的运行时性能,适合游戏开发和数据分析。Rust的并发特性包括轻量级线程和原子操作,便于构建高性能并发系统。活跃的社区和完善的生态系统,如丰富的库和框架,加速了开发者的学习和项目开发进程。【6月更文挑战第3天】
47 3