用可视化案例讲Rust编程:动态分发与最终封装
用可视化案例讲Rust编程:动态分发与最终封装
本文是《用可视化案例讲Rust编程》系列的第六篇,专注于讲解Rust中的动态分发与最终封装。通过具体的代码示例和可视化案例,详细介绍了Rust中泛型、动态反射(dyn)、trait对象等高级特性,并展示了如何使用这些特性来实现灵活的函数调用和类型处理。
函数单态化与静态分发
函数单态化(monomorphization)是Rust编译器为每个调用生成一个单独的、无运行时开销的函数副本,因此该函数副本的运行效率与不使用泛型的函数的运行效率是一致的。这是Rust对于泛型这种高级语法的解决方案,Rust的编译器,选择了编译期对此泛型的所有可能性,实现单态化,这样可以选择最高效率最低开销的运行。
动态分发与dyn关键字
除了泛型,要实现这种方式,还可以用Rust的另外一个高级特性,动态反射,即在运行时在检测相关类型的信息:dyn。dyn关键字用于强调相关trait的方法是动态分配的。要以这种方式使用trait,它必须是“对象安全”的。
与泛型参数或植入型特质不同,编译器不知道被传递的具体类型。也就是说,该类型已经被抹去。因此,一个dyn Trait引用包含两个指针。一个指针指向数据(例如,一个结构的实例)。另一个指针指向方法调用名称与函数指针的映射(被称为虚拟方法表各vtable)。
impl trait 和 dyn trait 在Rust分别被称为静态分发和动态分发,即当代码涉及多态时,需要某种机制决定实际调动类型。
动态分发的实现
这里定义了一个叫做double的方法,没有静态指定他的输入参数,而是用dyn这个关键字,这个就代表了Rust会采用动态分发,即运行的时候,才去确定它到底是什么内型。然后在方法里面,我们可以针对不同的参数类型要进行匹配相应的处理流程。这些参数,可以是系统内置的参数,例如整型、浮点型,也可以是自定义的结构。
例如我们定义的叫做year的结构体,double的意思,就是明年,所以只需要加1就可以了。而定义的dog的参数,默认狗的最大年纪就是24岁,所以如果你输入的狗的age小于12岁,则可以double,而大于12,直接清零……
测试如下:
可以看见最后两个测试,如果输入的狗子的年纪是8岁,double出来就是16,而输入的是15,则直接清零了……
但是这种写法,与传统的impl for <类型>实际上是一样的,只是对外部而言,调用的只是一个方法而已。
不过这种写法,很多人都觉得会破坏静态语言的固定性,不建议这样做,所以大家做个了解即可。(从编译器角度来说,函数单态化会把动态分发给编译成N个单态化的函数……所以这样写,并不会减少最后release出来的结果)
使用impl trait模式
我们也可以通过enum来实现,参考上一节颜色那个部分即可。用dyn的方式,你可以在参数里面传入任意类型的参数,然后在运行的时候在控制走哪条逻辑线,但是有没有一种可能,可以控制输入参数的类型,但是又可以根据类型进行逻辑选择的呢?答案当然是有,那就是官方推荐的impl trait模式。
而且官方在1.26之后的版本里面,推荐使用impl trait的方式来编写类型可控的泛型,如下所示:
代码非常简单,定义了一个trait,然后里面有一个方法,就是针对这个trait进行一个double处理。之后针对i32、f32、String和dog四种类型,进行了逻辑实现,最后测试如下:
//先写一个简单的测试性功能调用文件
//因为我们在trait里面实现了Any类型,所以有type_id这个方法能够获取对象类型唯一值
fn show_my_type(s: impl my_type){
if s.type_id() ==TypeId::of::<i32>(){
println!("i32 = {:?}",s);
}
else if s.type_id() ==TypeId::of::<f32>(){
println!("f32 = {:?}",s);
}
else if s.type_id() ==TypeId::of::<String>(){
println!("String = {:?}",s);
}
else if s.type_id() ==TypeId::of::<dog>(){
println!("dog = {:?}",s);
}
s.double();
}
测试效果如下:
如果在调用的时候,我们输入了没有定义的类型,IDE工具就会提示:
如果没有IDE的话,编译器就会自动检测出来,说你输入的参数类型是没有被实现过的,不让使用了:
而为什么可以这样做,又涉及到Rust具备函数式编程的设计思想了……函数式编程里面,函数是一等公民,函数也是一种对象,是可以定义和传递的,所以这里也通常把这种trait叫做trait对象,如果要论起写法来,下面两种写法效果是完全一样的:
trait Trait {}

fn foo<T: Trait>(arg: T) {
}
fn foo(arg: impl Trait) {
}
但是,在技术上,T: Trait 和 impl Trait 有着一个很重要的不同点。当用前者时,可以使用turbo-fish语法在调用的时候指定T的类型,如 foo::(1)。在 impl Trait 的情况下,只要它在函数定义中使用了,不管什么地方,都不能再使用turbo-fish。
最终封装
最后,我来封装一下读取shapefile的方法和构造trace的方法,让调用者不在关心具体的类型:
- 直接读取shape类型,并且转换为Geometry
pub fn shapeToGeometry(shp_path:&str)-> Vec<Geometry>{
let shps:Vec<Shape> = shapefile::read_shapes(shp_path)
.expect(&format!("Could not open shapefile, error: {}", shp_path));
let mut geometrys:Vec<Geometry> = Vec::new();
for s in shps{
geometrys.push(Geometry::<f64>::try_from(s).unwrap())
}
geometrys
}
用Geometry来构造trace:
impl BuildTrace for traceParam<Geometry>{
fn build_trace(&self) -> Vec<Box<ScatterMapbox<f64,f64>>> {
let mut traces: Vec<Box<ScatterMapbox<f64,f64>>> = Vec::new();
for (geom,color) in zip(self.geometrys.iter(),self.colors.iter()){
let mut tr = match geom {
Geometry::Point(_)=>{
let p:Point<_> = geom.to_owned().try_into().unwrap();
traceParam{geometrys:vec![p],colors:vec![color.to_owned()],size:self.size}.build_trace()
},
Geometry::MultiPoint(_)=>{
let p:MultiPoint<_> = geom.to_owned().try_into().unwrap();
let pnts:Vec<Point> = p.iter().map(|p|p.to_owned()).collect();
let color = (0..pnts.len()).map(|i|color.to_owned()).collect();
traceParam{geometrys:pnts,colors:color,size:self.size}.build_trace()
},
Geometry::LineString(_)=>{
let p:LineString<_> = geom.to_owned().try_into().unwrap();
traceParam{geometrys:vec![p],colors:vec![color.to_owned()],size:self.size}.build_trace()
},
Geometry::MultiLineString(_)=>{
let p:MultiLineString<_> = geom.to_owned().try_into().unwrap();
let lines:Vec<LineString> = p.iter().map(|p|p.to_owned()).collect();
let color = (0..lines.len()).map(|i|color.to_owned()).collect();
traceParam{geometrys:lines,colors:color,size:self.size}.build_trace()
},
Geometry::Polygon(_)=>{
let p:Polygon<_> = geom.to_owned().try_into().unwrap();
traceParam{geometrys:vec![p],colors:vec![color.to_owned()],size:self.size}.build_trace()
},
Geometry::MultiPolygon(_)=>{
let p:MultiPolygon<_> = geom.to_owned().try_into().unwrap();
let poly:Vec<Polygon> = p.iter().map(|p|p.to_owned()).collect();
let color = (0..poly.len()).map(|i|color.to_owned()).collect();
traceParam{geometrys:poly,colors:color,size:self.size}.build_trace()
},
_ => panic!("no geometry"),
};
traces.append(&mut tr);
}
traces
}
}
然后在调用的时候,就可以直接一击完成了:
#[test]
fn draw_db_style2(){
let shp1 = "./data/shp/北京行政区划.shp";
let color1 = inputColor::Rgba(Rgba::new(240,243,250,1.0));
let shp2 = "./data/shp/面状水系.shp";
let color2 = inputColor::Rgba(Rgba::new(108,213,250,1.0));
let shp3 = "./data/shp/植被.shp";
let color3 = inputColor::Rgba(Rgba::new(172,232,207,1.0));
let shp4 = "./data/shp/高速.shp";
let color4 = inputColor::Rgba(Rgba::new(255,182,118,1.0));
let shp5 = "./data/shp/快速路.shp";
let color5 = inputColor::Rgba(Rgba::new(255,216,107,1.0));
let mut traces:Vec<Box<ScatterMapbox<f64,f64>>>= Vec::new();
for (shp_path,color) in zip(vec![shp1,shp2,shp3,shp4,shp5]
,vec![color1,color2,color3,color4,color5]) {
let gs = readShapefile::shapeToGeometry(shp_path);
let colors:Vec<inputColor> = (0..gs.len())
.map(|x|color.to_owned()).collect();
let mut t = traceParam{geometrys:gs,colors:colors,size:2}.build_trace();
traces.append(&mut t);
}
plot_draw_trace(traces,None);
}
绘制效果如下:
放大之后,效果如下:
注意:顺义出现了一个白色底,是因为做数据的时候,顺义因为首都机场出现了一个环形构造,我们在绘制Polygon的时候,内部环设置为了白色,如果不想用这个颜色,也可以直接设置为输入色就可以了,如下所示:
打完收工。
所有例子和代码在以下位置:
008.用可视化案例讲Rust编程
自取。