从JS和Rust的析构比较中发现Rust哲学:显性化 - Paul


编程中最普遍的任务之一是将数据放入和取出复合数据类型。复合数据类型只是表示可以包含其他数据类型(如列表和对象)的数据类型的一种奇特方式,而原始类型是不能分解的“原子”(如数字和布尔值)。
在 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 语言。