Rust編程語言目前勢頭很猛,受到了幾大互聯(lián)網(wǎng)巨頭的青睞,比如谷歌、微軟、Meta,亞馬遜等,還作為第二語言進(jìn)入了Linux內(nèi)核,國內(nèi)的華為和字節(jié)跳動等公司也有Rust相關(guān)的應(yīng)用。
本人是一個Rust的粉絲,寫過不少的Rust應(yīng)用,這里我總結(jié)一下我認(rèn)為的Rust相比于其他編程語言的更為正確的設(shè)計(jì):
Sum type
Sum type是比較學(xué)術(shù)的說法,常見于函數(shù)式語言,但是sum type并非函數(shù)式語言的專屬,老大哥C語言的union加上tag之后就是sum type(俗稱tagged union),而動態(tài)類型語言的變量的類型其實(shí)就是封裝出來的sum type類型,比如PHP的變量定義:
typedef union _zend_value {
zend_long lval; /* long value */
double dval; /* double value */
zend_refcounted *counted;
zend_string *str;
zend_array *arr;
zend_object *obj;
zend_resource *res;
zend_reference *ref;
zend_ast_ref *ast;
zval *zv;
void *ptr;
zend_class_entry *ce;
zend_function *func;
struct {
uint32_t w1;
uint32_t w2;
} ww;
} zend_value;
但是為啥某些主流的靜態(tài)類型語言卻缺失了這個概念(比如Java、Go等)?我是沒想明白的,畢竟老大哥C語言都有這個概念了,缺失了sum type會導(dǎo)致某些需要動態(tài)的場景非常的別扭,比如序列化JSON,舉個例子,需要反序列化如下的JSON:
[1, "foo", null]
Java要怎么做?只能序列化到List<Object>,然后靠類型推斷來判斷是Integer還是String還是Object(null)了,這樣就喪失掉靜態(tài)類型的優(yōu)勢了。
而Rust的sum type關(guān)鍵字enum就很好地解決了這個問題,serde_json庫的Value就已經(jīng)完美地定義了JSON的結(jié)構(gòu):
pub enum Value {
Null,
Bool(bool),
Number(Number),
String(String),
Array(Vec<Value>),
Object(Map<String, Value>),
}
沒有null
這是個老生常談的問題,null的發(fā)明者Tony Hoare將null的設(shè)計(jì)稱為十億美元錯誤,意思是null造成的無法計(jì)數(shù)的錯誤、漏洞和系統(tǒng)崩潰可能已經(jīng)造成了十億美元的損失。
而Rust的解決方式是通過sum type,即Option,來強(qiáng)制用戶必須處理null的情況:
pub enum Option<T> {
None,
Some(T),
}
在其他一些沒有sum type語言中也有類似的做法,比如dart的null safety符號?,java的Optional(可惜出來得太晚也不是強(qiáng)制性的)。
賦值默認(rèn)是移動語義
這個是我認(rèn)為非常正確的設(shè)計(jì),普遍的編程語言的賦值都是復(fù)制語義,對于像bool、int、指針之類的基本類型,復(fù)制成本低,因此復(fù)制語義對于他們來說比較合適,包括Rust也為基本類型提供了Copy trait來轉(zhuǎn)換成復(fù)制語義。
但是對于大型的復(fù)合體,復(fù)制語義明顯不適合,因此某些語言區(qū)分了值類型和引用類型,成了各種面試題喜歡問到的東西。
而引用類型的本質(zhì)是封裝了指針,賦值的時候其實(shí)是復(fù)制的指針,指針指向的是同一份內(nèi)存,產(chǎn)生了“引用”的效果。
像Go這類存在指針的語言,那是否“引用”則清晰多了,不過依然存在slice和map這類引用類型,來對抗復(fù)制語義帶來的深拷貝造成的影響。
而Rust則更加正確,同一份內(nèi)存在同一時間只能被一個變量所擁有(所有權(quán)的概念),避免了復(fù)制語義帶來的影響,更加精妙的是,因?yàn)樗袡?quán)的概念,因此可以實(shí)現(xiàn)RAII,這是Rust能夠在無gc的情況下管理內(nèi)存的關(guān)鍵。