上一節(jié)本來準備結(jié)束的,后來很多同學問,說我覺得處理顏色那個地方太麻煩了,憑什么要寫兩次?寫一次不行么? 這里涉及到了靜態(tài)語言的一個核心概念,即:函數(shù)單態(tài)化 。 單態(tài)化(monomorphization),即 Rust 編譯器為每個調(diào)用生成一個單獨的、無運行時開銷的函數(shù)副本,因此該函數(shù)副本的運行效率與不使用泛型的函數(shù)的運行效率是一致的。 這是Rust對于泛型這種高級語法的解決方案,Rust的編譯器,選擇了編譯期對此泛型的所有可能性,實現(xiàn)單態(tài)化,這樣可以選擇最高效率最低開銷的運行。
所以,不管你寫不寫,最終編譯的時候,都會編譯成多個函數(shù),不過對于實現(xiàn)來說,靜態(tài)語言就只能靜態(tài)實現(xiàn),而對于提供對外調(diào)用接口的情況,自然是記憶開銷越小越好,正如我們前幾節(jié)寫的利用泛型返回讀取shapefile以及用泛型處理點線面的方法。 泛型這種東西,仁者見仁智者見智,有人說泛型實際上是加大了系統(tǒng)的復雜性和冗繁度,但是對于高層架構(gòu)人員來說,有泛型實在太方便了……所以就得到了一個比較主觀的說法: —— 泛型就是給造輪子的人用的。 除了泛型,要實現(xiàn)這種方式,還可以用Rust的另外一個高級特性,動態(tài)反射,即在運行時在檢測相關類型的信息:dyn。 dyn關鍵字用于強調(diào)相關trait的方法是動態(tài)分配的。要以這種方式使用trait,它必須是“對象安全”的。 與泛型參數(shù)或植入型特質(zhì)不同,編譯器不知道被傳遞的具體類型。也就是說,該類型已經(jīng)被抹去。因此,一個dyn Trait引用包含兩個指針。一個指針指向數(shù)據(jù)(例如,一個結(jié)構(gòu)的實例)。另一個指針指向方法調(diào)用名稱與函數(shù)指針的映射(被稱為虛擬方法表各vtable)。 impl trait 和 dyn trait 在Rust分別被稱為靜態(tài)分發(fā)和動態(tài)分發(fā),即當代碼涉及多態(tài)時,需要某種機制決定實際調(diào)動類型。
看到這里,可能有同學就會覺得: 既然是高級特性,看不懂的同學就暫時別去糾結(jié)了,我們來看看下面這個簡單的例子: 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的方法,沒有靜態(tài)指定他的輸入?yún)?shù),而是用dyn 這個關鍵字,這個就代表了Rust會采用動態(tài)分發(fā),即運行的時候,才去確定它到底是什么內(nèi)型。 然后在方法里面,我們可以針對不同的參數(shù)類型要進行匹配相應的處理流程。這些參數(shù),可以是系統(tǒng)內(nèi)置的參數(shù),例如整型、浮點型,也可以是自定義的結(jié)構(gòu)。 例如我們定義的叫做year的結(jié)構(gòu)體,double的意思,就是明年,所以只需要加1就可以了。而定義的dog的參數(shù),默認狗的最大年紀就是24歲,所以如果你輸入的狗的age小于12歲,則可以double,而大于12,直接清零…… 測試如下: 可以看見最后兩個測試,如果輸入的狗子的年紀是8歲,double出來就是16,而輸入的是15,則直接清零了…… 但是這種寫法,與傳統(tǒng)的impl for <類型> 實際上是一樣的,只是對外部而言,調(diào)用的只是一個方法而已。 不過這種寫法,很多人都覺得會破壞靜態(tài)語言的固定性,不建議這樣做,所以大家做個了解即可。 (從編譯器角度來說,函數(shù)單態(tài)化 會把動態(tài)分發(fā)給編譯成N個單態(tài)化的函數(shù)……所以這樣寫,并不會減少最后release出來的結(jié)果) 我們也可以通過enum來實現(xiàn),參考上一節(jié)顏色那個部分即可。 用dyn的方式,你可以在參數(shù)里面?zhèn)魅肴我忸愋偷膮?shù),然后在運行的時候在控制走哪條邏輯線,但是有沒有一種可能,可以控制輸入?yún)?shù)的類型,但是又可以根據(jù)類型進行邏輯選擇的呢?答案當然是有,那就是官方推薦的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四種類型,進行了邏輯實現(xiàn),最后測試如下: //先寫一個簡單的測試性功能調(diào)用文件 //因為我們在trait里面實現(xiàn)了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(); }
測試效果如下: 如果在調(diào)用的時候,我們輸入了沒有定義的類型,IDE工具就會提示: 如果沒有IDE的話,編譯器就會自動檢測出來,說你輸入的參數(shù)類型是沒有被實現(xiàn)過的,不讓使用了: 而為什么可以這樣做,又涉及到Rust具備函數(shù)式編程的設計思想了……函數(shù)式編程里面,函數(shù)是一等公民,函數(shù)也是一種對象,是可以定義和傳遞的,所以這里也通常把這種trait叫做trait對象 ,如果要論起寫法來,下面兩種寫法效果是完全一樣的: trait Trait {}
fn foo<T: Trait>(arg: T) { }
fn foo(arg: impl Trait) { } 但是,在技術(shù)上,T: Trait 和 impl Trait 有著一個很重要的不同點。當用前者時,可以使用turbo-fish語法在調(diào)用的時候指定T的類型,如 foo::(1)。在 impl Trait 的情況下,只要它在函數(shù)定義中使用了,不管什么地方,都不能再使用turbo-fish。
最后,我來封裝一下讀取shapefile的方法和構(gòu)造trace的方法,讓調(diào)用者不在關心具體的類型: 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 } 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 } }
然后在調(diào)用的時候,就可以直接一擊完成了: #[test] fn draw_db_style2(){ let shp1 = "./data/shp/北京行政區(qū)劃.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); } 繪制效果如下: 放大之后,效果如下: 注意:順義出現(xiàn)了一個白色底,是因為做數(shù)據(jù)的時候,順義因為首都機場出現(xiàn)了一個環(huán)形構(gòu)造,我們在繪制Polygon的時候,內(nèi)部環(huán)設置為了白色,如果不想用這個顏色,也可以直接設置為輸入色就可以了,如下所示: 打完收工。 所有例子和代碼在以下位置: https:///godxia/blog 008.用可視化案例講Rust編程 自取。
|