类型推导另外一个很多人误解的地方是类型推导。在 Rust 和 C# 之类的语言里面,你不需要像 Java 那样写 int x = 8;
这样显式的指出变量的类型,而是可以让编译器把类型推导出来。比如你写: let x = 8; // x 的类型推导为 i32
编译器的类型推导就可以知道 x 的类型是 i32,而不需要你把“i32”写在那里。这似乎是一个很方便的东西。然而看过很多 C# 代码之后你发现,这看似方便,却让程序变得不好读。在看 C# 代码的时候,我经常看到一堆的变量定义,每一个的前面都是 var。我没法一眼就看出它们表示什么,是整数,bool,还是字符串,还是某个用户定义的类? var correct = ...;
var id = ...;
var slot = ...;
var user = ...;
var passwd = ...;
我需要把鼠标移到变量上面,让 Visual Studio 显示出它推导出来的类型,可是鼠标移开之后,我可能又忘了它是什么。有时候发现看同一片代码,都需要反复的做这件事,鼠标移来移去的。而且要是没有 Visual Studio,用其它编辑器,或者在 github 上看代码或者 code review 的时候,你就得不到这种信息了。很多 C# 程序员为了避免这个问题,开始用很长的变量名,把类型的名字加在变量名字里面去,这样一来反而更复杂了,却没有想到直接把类型写出来。所以这种形式的类型推导,看似先进或者方便,其实还不如直接在声明处写下变量的类型,就像 Java 那样。 所以,虽然 Rust 在变量声明上似乎有更灵活的设计,然而我觉得 C 和 Java 之类的语言那样看似死板的方式其实更好。我建议不要使用 Rust 变量的重复绑定,避免使用类型推导,尽量明确的写出类型,以方便读者。如果你真的在乎代码的质量,就会发现大部分时候你的代码的读者是你自己,而不是别人,因为你需要反复的阅读和提炼你的代码。 动作的“返回值”Rust 的文档说它是一种“大部分基于表达式”的语言,并且给出这样一个例子: let mut y = 5;
let x = (y = 6); // x has the value `()`, not `6`
奇怪的是,这里变量 x 会得到一个值,空的 tuple,()。这种思路不大对,它是从像 OCaml 那样的语言照搬过来的,而 OCaml 本身就有问题。在 OCaml 里面,如果你使用 print_string,那你会得到如下的结果: print_string "hello world!\n";;
hello world!
- : unit = ()
这里,print_string 是一个“动作”,它对应过程式语言里面的“statement”。就像 C 语言的 printf。动作通常只产生“副作用”,而不返回值。在 OCaml 里面,为了“理论的优雅”,动作也会返回一个值,这个值叫做 ()。其实 () 相当于 C 语言的 void。C 语言里面有 void 类型,然而它却不允许你声明一个 void 类型的变量。比如你写 int main()
{
void x;
}
程序是没法编译通过的(试一试?)。让人惊讶的是,古老的 C 的做法其实是正确的,这里有比较深入的原因。如果你把一个类型看成是一个集合(比如 int 是机器整数的集合),那么 void 所表示的集合是个空集,它里面是不含有任何元素的。声明一个 void 类型的变量是没有任何意义的,因为它不可能有一个值。如果一个函数返回 void,你是没法把它赋值给一个变量的。 可是在 Rust 里面,不但动作(比如 y = 6 )会返回一个值 (),你居然可以把这个值赋给一个变量。其实这是错误的作法。原因在于 y = 6 只是一个“动作”,它只是把 6 放进变量 y 里面,这个动作发生了就发生了,它根本不应该返回一个值,它不应该可以出现在 let x = (y = 6); 的右边。就算你牵强附会说 y = 6 的返回值是 (),这个值是没有任何用处的。更不要说使用空的 tuple 来表示这个值,会引起更大的类型混淆,因为 () 本身有另外的,更有用的含义。 你根本就不应该可以写 let x = (y = 6); 这样的代码。只有当你犯错误或者逻辑不清晰的时候,才有可能把 y = 6 当成一个值来用。Rust 允许你把这种毫无意义的返回值赋给一个变量,这种错误就没有被及时发现,反而能够通过变量传播到另外一个地方去。有时候这种错误会传播挺远,然后导致问题(运行时错误或者类型检查错误),可是当它出问题的时候,你就不大容易找到错误的起源了。 这是很多语言的通病,特别是像 JavaScript 或者 PHP 之类的语言。它们把毫无意义或者牵强附会的结果(比如 undefined)到处传播,结果使错误很难被发现和追踪。