本節(jié)已經(jīng)涉及Rust學習曲線上的一個大坑:泛型和特性了,屬于語言的深水區(qū),如果初學者,建議看一眼知道有這個功能即可。 如果我們立足于功能實現(xiàn),那么做到像上一節(jié)那樣就可以了,從原理上來說,每個函數(shù)滿足唯一的功能,是一種好的設計,軟件工程里面“高內(nèi)聚低耦合”是有利于系統(tǒng)的獨立性的。 但是在調(diào)用的時候,就需要有一點的心智了,例如我們要讀取點線面三種不同的shapefile,就要記住三種不同的函數(shù)名和三種不同的繪圖要素構(gòu)建程序,那也太麻煩了,有沒有一種方法,讓我們調(diào)用一種方法,根據(jù)不同的參數(shù)類型自動適配不同的處理邏輯呢? 如果你是學Python/Java/JavaScript這樣的高級語言的同學,你肯定已經(jīng)有了自己的實現(xiàn)方法,例如Python/Javascript作為動態(tài)類型的語言,可以只寫一個入口函數(shù),然后根據(jù)函數(shù)的輸入?yún)?shù)自動去選擇不同的分支來處理。 而Java也有同名方法覆蓋,利用不同的函數(shù)簽名(參數(shù)類型)來解決用同名函數(shù)實現(xiàn)不同的邏輯流程。 而C++則是可以用泛型和函數(shù)重載來實現(xiàn)這個功能。 那么在Rust里面如何解決呢? 老規(guī)矩,先看結(jié)果: 用同名的一個方法,實現(xiàn)對于三種不同的shapefile讀取,并且獲取三種不同的返回類型值,那么這是這么實現(xiàn)的呢? 先看代碼: 架構(gòu)定義首先定義了一個結(jié)構(gòu)體,這個空結(jié)構(gòu)體的作用類似于Java里面的class。 然后定義了一個特性(trait),特性這個東西,在Rust中,類似于Java里面的接口,或者C++里面的虛函數(shù),但是與接口和虛函數(shù)不同的時候,特性可以直接在里面寫實現(xiàn),也可以留空,如果你是學Java的,你把下面的代碼理解為定義了一個接口,并且定義了一個實現(xiàn)接口的工廠方法即可。 在這里的特性中,我們定義一個泛型T,然后定義了一個方法,就叫做read_shp,輸入產(chǎn)生就是一個shapefile的路徑,返回值是一個T ,這個T就是Rust中的泛型,即代表一切可能的類型。
pub struct shp; pub trait ReadShapfile<T> { fn read_shp(shp_path:&str) -> T; } 然后我們開始寫針對不同類型的shapefile的實現(xiàn),實現(xiàn)如下: //實現(xiàn)1:讀取polygon類型的shapefile,通過返回值來匹配。 impl ReadShapfile<Vec<Polygon>> for shp{ fn read_shp(shp_path:&str) -> Vec<Polygon>{ let shp_read = shapefile::read_as::<_, shapefile::Polygon, shapefile::dbase::Record>(shp_path) .expect(&format!("Could not open polygon-shapefile, error: {}", shp_path));
let mut polygons:Vec<Polygon> = Vec::new(); for (polygon, polygon_record) in shp_read { let geo_mpolygon: geo_types::MultiPolygon<f64> = polygon.into(); for poly in geo_mpolygon.iter(){ polygons.push(poly.to_owned()); } } polygons } }
//實現(xiàn)2:讀取Polyline類型的shapefile,通過返回值來匹配。 impl ReadShapfile<Vec<LineString>> for shp{ fn read_shp(shp_path:&str) -> Vec<LineString>{ let shp_read = shapefile::read_as::<_, shapefile::Polyline, shapefile::dbase::Record>(shp_path) .expect(&format!("Could not open polyline-shapefile, error: {}", shp_path));
let mut linestrings:Vec<LineString> = Vec::new(); for (pline, pline_record) in shp_read { let geo_mline: geo_types::MultiLineString<f64> = pline.into(); for line in geo_mline.iter(){ linestrings.push(line.to_owned()); } } linestrings } } //實現(xiàn)2:讀取Point類型的shapefile,通過返回值來匹配。 impl ReadShapfile<Vec<Point>> for shp{ fn read_shp(shp_path:&str) -> Vec<Point>{ let shp_read = shapefile::read_as::<_, shapefile::Point, shapefile::dbase::Record>(shp_path) .expect(&format!("Could not open polyline-shapefile, error: {}", shp_path));
let mut pnts:Vec<Point> = Vec::new(); for (pnt, pnt_record) in shp_read { let geo_pnt: geo_types::Point<f64> = pnt.into(); pnts.push(geo_pnt.to_owned()); } pnts } } 簡要的解釋一下: 在Rust的語法中,impl 特性名稱 <返回值> for 結(jié)構(gòu)名 這個語法,代表了具體的實現(xiàn),而里面那個 <返回值> 部分,就是我們定義的泛型T,注意,T只是一個代稱,習慣性用大寫字母,你用ABCDEFG,或者abcdefg都是可以的。 然后在這個實現(xiàn)里面,去寫具體方法的read_shp的實現(xiàn),寫完之后,就可以通過在調(diào)用的時候,再指定返回類型,來適配具體調(diào)用哪個方法了。
看到這里,可能有同學又有問題了: 上一節(jié),直接寫多個方法,多實用啊,讀polygon就是polygon方法,讀point就是point方法,干凈整潔又高效,干嘛搞這么麻煩? 實際上兩種方式,都是可以的,每個方法用獨立名稱進行調(diào)用,實際上是一種比較傳統(tǒng),但是很高效的方式(C語言里面寫操作系統(tǒng)內(nèi)核都是這樣寫的)。 而后面這種,則是現(xiàn)代編程架構(gòu)里面推薦的,用同名函數(shù),來減少調(diào)用者的心智負擔,反正都是讀取shapefile,我干嘛還要記那么多個函數(shù)名???為什么不能用一個函數(shù)就全部解決了呢? 所以在軟件開發(fā)中,同名函數(shù),通過定義不同的返回值,或者輸入值來區(qū)分的方式,叫做多態(tài) ,而在Rust里面,多態(tài)又可以分為靜態(tài)分發(fā) :即靜態(tài)多態(tài)模式 或者單一態(tài)模式 ;以及動態(tài)分發(fā) :即動態(tài)多態(tài)模式 。 這種所謂的靜態(tài) 和動態(tài) ,在Rust中,是針對編譯而言的,靜態(tài)即代碼和方法,在被編譯的時候,就已經(jīng)決定了編譯的結(jié)果,你的輸入、輸出、處理流程等,都在編譯的時候被固定下來了。 這種也是函數(shù)式編程 的核心思想:函數(shù)式編程強調(diào):不可變性和純函數(shù),這意味著函數(shù)的輸出只取決于它的輸入,而不依賴于外部狀態(tài) 而動態(tài) 則相反,在編譯的時候,系統(tǒng)并不知道要輸入的參數(shù)和輸出的結(jié)果具體是什么類型,只有在運行的時候,根據(jù)輸入的數(shù)據(jù)情況才知道。 一般來說,動態(tài)語言中的動,就是這種,通常解釋性的語言,都是動態(tài)的,例如Python/Javascript等。 當然,Rust也是支持動態(tài)編譯的,但是在使用動態(tài)編譯的時候,需要有一系列的手續(xù),這個我們以后再說。 從兩種編譯模式來看,靜態(tài)模式的開銷小,安全性高,但是靈活性比較差;反之,動態(tài)模式靈活性好,但是開銷大,安全性和穩(wěn)定性較差。 具體在開發(fā)過程中,用哪種模式,就仁者見仁智者見智了。 最后我們來畫一個UML圖,就很容易看出來這個架構(gòu)的實現(xiàn)思路了:
|