泛型特化
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 都能满足条件的时候,它会自动选择使用那个最特殊最匹配的版本。
特化的意义
- 性能优化。泛型特化可以为某些情况提供统一抽象下的特殊实现。
- 代码重用。泛型特化可以提供一些默认(但不完整的)实现,某些情况下可以减少重复代码。
- 为“高效继承”铺路。泛型特化其实跟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。这个实现也是泛型特化的体现。