全系列合集
[Rust开发]用可视化案例讲Rust编程1.用Rust画个百度地图
[Rust开发]用可视化案例讲Rust编程2. 编码的核心组成:函数
[Rust开发]用可视化案例讲Rust编程3.函数分解与参数传递
[Rust开发]用可视化案例讲Rust编程4.用泛型和特性实现自适配shapefile的读取
[Rust开发]用可视化案例讲Rust编程5.用泛型和特性实现自适配绘制和颜色设置
上一节本来准备结束的,后来很多同学问,说我觉得处理颜色那个地方太麻烦了,凭什么要写两次?写一次不行么?
这里涉及到了静态语言的一个核心概念,即:函数单态化
。
单态化(monomorphization),即 Rust 编译器为每个调用生成一个单独的、无运行时开销的函数副本,因此该函数副本的运行效率与不使用泛型的函数的运行效率是一致的。
这是Rust对于泛型这种高级语法的解决方案,Rust的编译器,选择了编译期对此泛型的所有可能性,实现单态化,这样可以选择最高效率最低开销的运行。
所以,不管你写不写,最终编译的时候,都会编译成多个函数,不过对于实现来说,静态语言就只能静态实现,而对于提供对外调用接口的情况,自然是记忆开销越小越好,正如我们前几节写的利用泛型返回读取shapefile以及用泛型处理点线面的方法。
泛型这种东西,仁者见仁智者见智,有人说泛型实际上是加大了系统的复杂性和冗繁度,但是对于高层架构人员来说,有泛型实在太方便了……所以就得到了一个比较主观的说法:
—— 泛型就是给造轮子的人用的。
除了泛型,要实现这种方式,还可以用Rust的另外一个高级特性,动态反射,即在运行时在检测相关类型的信息:dyn。
dyn关键字用于强调相关trait的方法是动态分配的。要以这种方式使用trait,它必须是“对象安全”的。
与泛型参数或植入型特质不同,编译器不知道被传递的具体类型。也就是说,该类型已经被抹去。因此,一个dyn Trait引用包含两个指针。一个指针指向数据(例如,一个结构的实例)。另一个指针指向方法调用名称与函数指针的映射(被称为虚拟方法表各vtable)。
impl trait 和 dyn trait 在Rust分别被称为静态分发和动态分发,即当代码涉及多态时,需要某种机制决定实际调动类型。
看到这里,可能有同学就会觉得:
既然是高级特性,看不懂的同学就暂时别去纠结了,我们来看看下面这个简单的例子:
use std::{any::Any, ops::Add};
#[derive(Debug)]
struct year{
y:usize
}
#[derive(Debug,Clone)]
struct dog{
name:String,
age:usize,
}
fn double(s: &dyn Any){
if let Some(v) = s.downcast_ref::<u32>() {
println!("u32 double= {:?}",*v * 2);
}
else if let Some(v) = s.downcast_ref::<f32>() {
println!("f32 double= {:?}",*v * 2.0);
}
else if let Some(v) = s.downcast_ref::<String>() {
let x = v.clone();
let x2 = v.clone();
println!("string double= {:?}",x.add("_").add(&x2));
}
else if let Some(v) = s.downcast_ref::<year>() {
let y = year{y:v.y +1};
println!("year double= {:?}",y);
}
else if let Some(v) = s.downcast_ref::<dog>() {
let mut d = dog{name:v.name.clone(), age:v.age};
if d.age > 12{
d.age =0;
}
else{
d.age =d.age * 2;
}
println!("dog double= {:?}",d);
}
}
这里定义了一个叫做double的方法,没有静态指定他的输入参数,而是用dyn
这个关键字,这个就代表了Rust会采用动态分发,即运行的时候,才去确定它到底是什么内型。
然后在方法里面,我们可以针对不同的参数类型要进行匹配相应的处理流程。这些参数,可以是系统内置的参数,例如整型、浮点型,也可以是自定义的结构。
例如我们定义的叫做year的结构体,double的意思,就是明年,所以只需要加1就可以了。而定义的dog的参数,默认狗的最大年纪就是24岁,所以如果你输入的狗的age小于12岁,则可以double,而大于12,直接清零……
测试如下:
可以看见最后两个测试,如果输入的狗子的年纪是8岁,double出来就是16,而输入的是15,则直接清零了……
但是这种写法,与传统的impl for <类型>
实际上是一样的,只是对外部而言,调用的只是一个方法而已。
不过这种写法,很多人都觉得会破坏静态语言的固定性,不建议这样做,所以大家做个了解即可。
(从编译器角度来说,函数单态化
会把动态分发给编译成N个单态化的函数……所以这样写,并不会减少最后release出来的结果)
我们也可以通过enum来实现,参考上一节颜色那个部分即可。
用dyn的方式,你可以在参数里面传入任意类型的参数,然后在运行的时候在控制走哪条逻辑线,但是有没有一种可能,可以控制输入参数的类型,但是又可以根据类型进行逻辑选择的呢?答案当然是有,那就是官方推荐的impl trait
模式。
而且官方在1.26之后的版本里面,推荐使用impl trait的方式来编写类型可控的泛型,如下所示:
trait my_type:std::fmt::Debug+'static+Any{
fn double(&self);
}
impl my_type for i32{
fn double(&self) {
println!("i32 double= {:?}",self * self);
}
}
impl my_type for f32{
fn double(&self) {
println!("f32 double= {:?}",self * self);
}
}
impl my_type for String{
fn double(&self) {
println!("String double= {}_{}",self,self);
}
}
impl my_type for dog{
fn double(&self) {
let mut d2 = self.clone();
d2.age = d2.age +1;
println!("dog double= {:?}",d2);
}
}
代码非常简单,定义了一个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的时候,内部环设置为了白色,如果不想用这个颜色,也可以直接设置为输入色就可以了,如下所示:
打完收工。
所有例子和代码在以下位置:
https://gitee.com/godxia/blog
008.用可视化案例讲Rust编程
自取。