從JS和Rust的析構比較中發現Rust哲學:顯性化 - Paul

banq發表於2022-02-24

程式設計中最普遍的任務之一是將資料放入和取出複合資料型別。複合資料型別只是表示可以包含其他資料型別(如列表和物件)的資料型別的一種奇特方式,而原始型別是不能分解的“原子”(如數字和布林值)。
在 JavaScript 中,我們可以這樣做:

let user = new Object();
user.name = "Tim";
user.city = "Ottawa, ON";
user.country = "Canada";


不過,這有點乏味,所以通常我們編寫一個物件字面量:

let user = {
    name: "Tim",
    city: "Ottawa, ON",
    country: "Canada"
}


反過來也一樣。我們可以像這樣拉出user的欄位:

let name = user.name;
let city = user.city;
let country = user.country;

在現代 JavaScript 中,更常見的是映象這個物件字面量構造:

let {name, city, country} = user;

 
情況並非總是如此。JavaScript 並不總是有解構/析構賦值。如果我們回到史前版本的 JavaScript (ES3),它會將上面的程式碼轉換為:

var name = user.name, city = user.city, country = user.country;
 

我說這一切只是為了說明一點:解構/析構沒有什麼神奇的。它是語法糖:一種語言功能,可以為您(程式設計師)節省擊鍵,並使您的程式碼更具可讀性。它並沒有從根本上使語言更強大,但它是一個很好的生活質量改進。
 

Rust 中的解構賦值
Rust中的解構賦值在概念上與JavaScript相似。也就是說:在有些情況下,從一個資料結構中提取元素的語法糖反映了建立相同資料結構的語法。

我們可以透過對一個元組進行解構來看到這一點。

fn my_function(data: &(u32, &str)) {
    let (my_num, my_str) = data;
    println!("my_num: {}, my_str: {}", my_num, my_str);
}

fn main() {
    let data = (4, "ok");
    my_function(&data);
}


輸出:

my_num: 4, my_str: ok

 
我們也可以解構我們自己定義的型別。這是一個類似於我們在上面的 JavaScript 中看到的物件示例的示例。

struct User {
    name: String,
    city: String,
    country: String,
}

fn print_user(user: &User) {
    let User {
        name, city, country
    } = user;
    println!("User {} is from {}, {}", name, city, country);
}

fn main() {
    let user = User {
        name: "Tim".to_string(),
        city: "Ottawa, ON".to_string(),
        country: "Canada".to_string(),
    };
    print_user(&user);
}

有時,我們不需要解構所有欄位。讓我們看看如果上面程式碼中三個欄位中我們省略一個會發生什麼:


struct User {
    name: String,
    city: String,
    country: String,
}

fn city_name(user: &User) -> String {
    let User {city, country} = user;
    format!("{}, {}", city, country)
}


Rust會編譯錯誤:

  Compiling playground v0.0.1 (/playground)
error[E0027]: pattern does not mention field `name`
 --> src/lib.rs:8:9
  |
8 |     let User {city, country} = user;
  |         ^^^^^^^^^^^^^^^^^^^^ missing field `name`



明顯的解決方案是新增name,如下所示:

fn city_name(user: &User) -> String {
    let User {city, country, name} = user;
    format!("{}, {}", city, country)
}


編譯器接受了這一點,但不情願。它編譯我們的程式碼,但抱怨:

warning: unused variable: `name`
 --> src/lib.rs:8:30
  |
8 |     let User {city, country, name} = user;
  |                              ^^^^ help: try ignoring the field: `name: _`
  |
  = note: `#[warn(unused_variables)]` on by default



有用的是,它告訴我們如何修復它:我們可以顯式忽略 name 欄位:

fn city_name(user: &User) -> String {
    let User {city, country, name: _} = user;
    format!("{}, {}", city, country)
}

為了理解這一點,我們可以理解解構表示式中的每個欄位有兩個目的。它告訴編譯器你想從User中提取的欄位的名稱,並告訴編譯器你想把它分配給的本地名稱。
碰巧的是,連貫地使用名字是很好的程式設計實踐,所以使用欄位名作為區域性變數往往是一個很好的預設值。命名是電腦科學中的兩個難題之一,在這裡我們可以把變數的命名外包給我們所使用的資料結構的作者。
但重要的是要知道,沒有什麼能阻止我們把重新命名欄位作為析構、解構作業的一部分,我們只是要更明確地說明這一點。

fn print_user(user: &User) {
    let User {
        name: fullname, city: metro, country: nation
    } = user;
    println!("User {} is from {}, {}", fullname, metro, nation);
}


順便說一句,這在TypeScript和現代JavaScript中也是可行的。它甚至碰巧使用相同的語法。

回到上面的編譯器警告,Rust抱怨這個未使用的name,原因與它抱怨下面這段程式碼中未使用的變數名name一樣:

fn main() {
    let name = "Tim";
}


如果我們把這個值賦給_,它就不會抱怨,儘管這個賦值同樣毫無意義。

fn main() {
    let _ = "Tim";
}

當解構時,_就像一種黑洞。我們可以將它與任何值相匹配,但我們不能將值拿回來。
 
我們看看的另一個地方是對元組tuple進行解構,我們只關心其中的一些值。

fn main() {
    let my_tuple = (4, "foo", false);
    
    let (num, _, truthy) = my_tuple;
    
    println!("{} {}", num, truthy);
}


你經常在習慣性的JavaScript中看到類似的東西,像這樣的東西。

let [_, triggerRerender] = React.useState();


程式設計師向他們程式碼的讀者傳遞的意圖是一樣的,
但在JavaScript中,_實際上只是一個變數名,程式設計師把它當作一個黑洞。如果你想的話,你可以讀取分配給它的值(儘管構建時工具可能會賦予它特殊的含義,並在你試圖讀取它時抱怨)。
在Rust中,_是語言的一部分,而不是一個變數。如果你給它賦值,它真的什麼都不做。
 
回到程式碼:

fn city_name(user: &User) -> String {
    let User {city, country, name: _} = user;
    format!("{}, {}", city, country)
}

我們的意思是 "把user.city分配給變數city,把user.country分配給變數country,而對user.name不做任何處理"。
 
我在這裡繞了個彎,因為我想向你展示_,這是一個基本的概念,在以後的文章中,當我們看模式匹配時,會再次出現。但是在重構欄位的情況下,實際上有一個更好的方法。事實上,如果我們閱讀了完整的編譯器錯誤(我在上面擷取的),它將有助於引導我們走向正確的方向。

   Compiling playground v0.0.1 (/playground)
error[E0027]: pattern does not mention field `name`
 --> src/lib.rs:8:9
  |
8 |     let User {city, country} = user;
  |         ^^^^^^^^^^^^^^^^^^^^ missing field `name`
  |
help: include the missing field in the pattern
  |
8 |     let User {city, country, name } = user;
  |                            ~~~~~~~~
help: if you don't care about this missing field, you can explicitly ignore it
  |
8 |     let User {city, country, .. } = user;
  |                            ~~~~~~

For more information about this error, try `rustc --explain E0027`.
error: could not compile `playground` due to previous error


help: if you don't care about this missing field, you can explicitly ignore it 
"幫助:如果你不關心這個缺失的欄位"
- 這聽起來確實像提示我們。讓我們試試吧。

struct User {
    name: String,
    city: String,
    country: String,
}

fn city_name(user: &User) -> String {
    let User {city, country, ..} = user;
    format!("{}, {}", city, country)
}


 

Rust的哲學:顯性化、明確化
在同樣的情況下,JavaScript 知道我們的意思。TypeScript,也不會吹毛求疵。Rust 編譯器足夠聰明,可以提出一個可以解決問題的更改,為什麼它會選擇責備我們?
這個問題觸及了 Rust 的一般特徵。任何著手建立程式語言的人都必須在假設意圖和要求程式設計師明確、明確的指令之間做出決定。
Rust 的一般方法是在要求明確性方面犯錯。這意味著初學者 Rust 開發人員將花費相當多的時間嘗試以他們不習慣的方式安撫編譯器,無論他們在其他語言方面有多聰明或經驗如何。
透過強迫你變得明確,Rust 迫使你在程式碼中深思熟慮。
把它當成一個姿勢訓練器:與其怨恨它,不如用它來養成良好的習慣,直到不再經常發生嘮叨。隨著時間的推移,我想你會發現它會讓你成為一個更有思想的程式設計師,即使你使用的不是 Rust 語言。

相關文章