You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
这可能是我最讨厌的一点。在大多数沿袭 C 语言设计的语言中(C,C#,Java 等),经常使用一个特殊值来表示某个函数执行失败的情况。比如,在 C# 中,用于在字符串中查找另一个字符串的索引位置的函数 String.IndexOf(t) 会在找不到 t 时返回 -1,从而写出这样的 C# 代码:
stringsentence="The fox jumps over the dog";intindex=sentence.IndexOf("fox");if(index!=-1){stringwordsAfterFox=sentence.SubString(index);Console.WriteLine(wordsAfterFox);}
let sentence = "The fox jumps over the dog";let index = sentence.find("fox");// let words_after_fox = &sentence[index..];// 如果你直接使用 index,会得到报错:Error: Can't index str with Option<usize>ifletSome(fox) = index {let words_after_fox = &sentence[fox..];println!("{}", words_after_fox);}
error[E0596]: cannot borrow `counter` asmutable,as it is a captured variable in a `Fn` closure
--> src/main.rs:47:48
|
47 | monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));
| ^^^^^^^ cannot borrow asmutable
error[E0499]: cannot borrow `counter` asmutable more than once at a time
--> src/main.rs:47:39
|
47 | monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));
| ---------^^^^^^^^------------------------------------
| | | |
| | | borrows occur due to use of `counter` in closure
| | `counter` was mutably borrowed here in the previous iteration of the loop
| cast requires that `counter` is borrowed for `'static`
error[E0597]: `counter` does not live long enough
--> src/main.rs:47:48
|
47 | monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));
| ------------------^^^^^^^----------------------------
| | | |
| | | borrowed value does not live long enough
| | value captured here
| cast requires that `counter` is borrowed for `'static`
...
60 | }
| - `counter` dropped here while still borrowed
error[E0502]: cannot borrow `counter` asimmutable because it is also borrowed asmutable
--> src/main.rs:50:12
|
47 | monster.add_listener(Box::new(|damage| counter.on_damage_received(damage)));
| -----------------------------------------------------
| | | |
| | | first borrow occurs due to use of `counter` in closure
| | mutable borrow occurs here
| cast requires that `counter` is borrowed for `'static`
...
50 | while !counter.reached_target_damage(){
| ^^^^^^^ immutable borrow occurs here
C 语言离不开 for (int i = 0; i < n; i ++),就像西方不能没有耶路撒冷。
所以下面的 Rust 的代码也就不足为奇:
let points:Vec<Coordinate> = ...;let differences = Vec::new();for i in1..points.len()[
let current = points[i];let previous = points[i-1];
differences.push(current - previous);]
就像呼吸一样自然。然而,就算是老司机也难免会中招下标越界 bug ,尤其当你想在循环里取前一个值时,你就得花心思去考虑 i 是否是从 1 开始的。
let dict = Dictionary::from_file("./words.txt")?;implDictionary{fnfrom_file(filename:implAsRef<Path>) -> Result<Self,Error>{let text = std::fs::read_to_string(filename)?;letmut words = Vec::new();for line in text.lines(){
words.push(line);}Ok(Dictionary{ words })}}
很多从其他语言过来的 Rust 新手都会不可避免地利用之前的编码经验来写 Rust,这无可厚非,毕竟确实没必要从头开始学习编程知识。但是,这些经验性知识,却极有可能导致你写出来很垃圾的 Rust 代码。
别再用哨兵值了
这可能是我最讨厌的一点。在大多数沿袭 C 语言设计的语言中(C,C#,Java 等),经常使用一个特殊值来表示某个函数执行失败的情况。比如,在 C# 中,用于在字符串中查找另一个字符串的索引位置的函数
String.IndexOf(t)
会在找不到t
时返回-1
,从而写出这样的 C# 代码:在其他的语言中,这种用法更是数不胜数,类似的哨兵值还有空字符串
""
或者null
、None
之类的空值。既然它这么常用,为什么还要说它很差劲呢?原因就是你极有可能会忘记处理哨兵值所代表的失败情况,然后导致整个程序直接崩溃。
Rust 则提供了很好的解决方案,那就是
Option
。Option
从设计层面就杜绝了忘记考虑None
时的情况,编译器会在编译时就进行强制检查,如果你忘了处理None
,编译器会马上告诉你。上面字符串例子的代码,在 Rust 中可以写成这样:别再用匈牙利命名了
上世纪 70 年代,程序员们逐渐开始在无类型或动态类型语言中使用匈牙利命名法,他们给变量名加上不同的前缀来表示变量的类型,比如
bVisited
表示布尔型的变量visited
,strName
表示字符串类型的变量name
。我们可以在
Delphi
语言中见到大量的例子,T
开头的表示类(class)或者类型(type),F
表示属性值(fields),A
表示参数(arguments),诸如此类。C# 中也有类似的使用习惯,比如用
I
开头表示一个接口(interface),所以 C# 程序员很可能会写出这种 Rust 代码:你大可以直接扔掉前面的
I
,因为 Rust 的语法已经保证了我们很难混淆trait
和type
,不像 C# 很容易就分不清interface
和class
(译者按:Typescript 中就是interface
、type
和class
大混战了,狗头.jpg)。此外,你也没有必要在给一些工具函数或者中间变量命名时带上它的类型信息,比如下面的代码:
既然
String.from_utf8()
已经明明白白地返回了一个字符串,为什么还要在命名时加上_str
后缀呢?与其他语言不同,Rust 语言鼓励程序员在对变量进行一系列变换操作时,使用同名变量覆写掉不再使用的旧值,比如:
使用相同的变量名可以很好地保证概念的一致性。
有些语言会明令禁止覆写变量,尤其像 Javascript 这种动态类型语言,因为频繁变化的类型,在缺少类型推断的情况下,尤其有可能会导致 bug 出现。
你可能不需要这么多
Rc<RefCell<T>>
OOP 编程实践常常会保存其他对象的引用,并在合适的时候调用他们的函数,这没啥不好的,依赖注入(Dependency Injection)是个蛮不错的实践,不过有别于大多数面向对象的语言,Rust 并没有垃圾内存回收机制(Garbage Collector),并且对共享可变性非常敏感。
举个例子,我们正要实现一个打怪兽的游戏,玩家需要对怪物们造成足量伤害才算打败他们(我也不知道为什么要这么设定,可能是接受了什么委托?)。
先创建一个
Monster
类,包含health
生命值属性以及takeDamage()
遭受伤害的方法,为了能知道怪物遭受了多少伤害,我们允许为Monster
类注入一个回调函数,该回调函数可以接收每次遭受的伤害值。然后设计一个伤害计数类
DamageCounter
,可以累计怪物伤害值:然后我们对怪物造成随机伤害,直到伤害计数达到上限。
这里是在线运行示例。
然后我们用 Rust 重写上述逻辑,
Monster
结构体结构保持不变,使用Box<dyn Fn(u32)>
来接收闭包,该闭包接受一个u32
型参数。随后是
DamageCounter
类:然后开始打怪:
同样地,Rust 代码也有一个在线运行示例。
然而,编译器狠狠地给我们报了四个错误,全都集中在
monster.add_listener()
这一行:看起来是一团乱麻,让我来翻译翻译
,什么叫惊喜:counter
的引用;counter.on_damage_received()
方法接受&mut self
,所以闭包需要&mut
可变引用。由于这个闭包在循环里,所以对同一个对象的可变引用&mut
会执行多次,这是不被 Rust 允许的;counter
需要被移动到闭包内部,而在循环中,重复移动某值就会造成use of moved value
错误;counter
被移动到闭包内后,我们又尝试在条件语句中使用它,显然也会报错。总之,情况不太妙。
一个经典解决方法是把
DamageCounter
用引用计数指针裹起来,这样我们可以重复使用它了,此外由于我们需要使用&mut self
,所以我们需要使用RefCell
来在运行时做借用检查(borrow checking),而不是在编译时。这里是改动后的代码。
虽然现在代码可以正常运行了,但是整块代码会被
Rc<RefCell>Vec<Foo>>>
之类的玩意儿搞得乌烟瘴气。而且当代码变得更复杂时,RefCell
也有可能会被可变借用多次。而如果在多线程中使用了Arc<Mutex<Vec<Foo>>>
,RefCell
引发 panic 之后,整个程序会死锁。所以,一个更好的解决方法是避免在结构体中存储持久化引用。我们对
Monster::take_damage()
稍加改造:这里是在线示例。
由于避免了存储回调函数,改造后的代码行数从 62 行下降到了 47 行。
此外,我们也可以给
take_damage()
加个返回值,这样可以把伤害值放在返回值里,以备后用:当代码复杂度上升时,代码也不会变成一团糟,而且它看起来更“函数式”。
错用整数类型
另一个从 C 语言带来的坏毛病是错用整数类型,导致代码里到处都是
usize
的类型转换,尤其是在对数组做索引时。C 程序员在初学时就被各种教程教会了使用
int
类型来做索引和循环,当他们开始写 Rust 时,也自然而然地用Vec<i32>
类型来做数组切片。但是阿 Rust 真的很严格,不让程序员使用usize
以外的类型对数组、切片和Vec
进行索引,这就不得不在索引的时候进行一次i32 as usize
的类型转换。Rust 这么做有诸多好处:
usize
与普通指针的大小相同,指针运算不会造成隐式类型转换;std::mem::size_of()
和std::mem::align_of()
返回usize
类型。所以,请尽量使用
usize
类型作为可能涉及索引操作的中间变量的首选类型。没人比我更懂
unsafe
每次当我看到 C 程序员使用
std::mem::transmute()
函数或者裸指针来跳过编译器的借用检查时,我都会想起论坛中那条古老的 Rust 圣经:Obstacles, by Daniel Keep。建议你现在就去读一读,我可以等。(译者按:有空了给大伙翻译一下。)
你可能已经身经百战见得多了,精通八种编程语言,所以毫无顾忌地破坏 Rust 精心构筑的规则:创建自引用的结构体、用
unsafe
创建全局变量。而且每一次,你都用同样的借口:“这是个单线程程序,所以static mut
百分之一万没问题”、“这在 C 语言里跑得好好的”。unsafe
很微妙,你必须要对 Rust 的借用检查规则和内存模型有深刻的认识才行。我也不想像祥林嫂一样念叨:“未成年人请在编译器监督下编写unsafe
多线程代码”,但如果你刚开始学这门语言,我衷心建议你耐心从编译器报错的痛苦中慢慢品味 Rust 的美妙。当你成为了 Rust 大师,你可以尽情玩弄
unsafe
代码,但在那之前,我还是想告诉你,unsafe
不是杀死编译器报错的板蓝根,也不是能让你自在书写 C 风味 Rust 代码的作弊码。不舍得用命名空间
C 语言中的另一个常见实践是给函数增加所属的库名或者模块名作为前缀,比如
rune_wasmer_runtime_load()
就表示rune
库的wasmer/runtime
模块下的load()
函数。Rust 提供了非常好用的命名空间机制,请尽情使用它,比如刚刚这个函数就可以写成rune::wasmer::Runtime::load()
。滥用切片索引
C 语言离不开
for (int i = 0; i < n; i ++)
,就像西方不能没有耶路撒冷。所以下面的 Rust 的代码也就不足为奇:
就像呼吸一样自然。然而,就算是老司机也难免会中招下标越界 bug ,尤其当你想在循环里取前一个值时,你就得花心思去考虑
i
是否是从 1 开始的。Rust 很担心你,所以拿出了迭代器,切片类型甚至还有
windows()
和array_windows()
这种高级函数来获取相邻的元素对。上面的代码可以重写为:甚至可以用链式调用来炫技:
有些人会主张使用了
map()
和collect
版本的代码更加“函数式“,我则觉得仁者见仁,智者见智。不仅如此,迭代器的性能往往比朴素的
for
循环更好,你可以在这里了解原因。滥用迭代器
一旦你用迭代器用上瘾了,你极有可能跑向对立面:拿着迭代器这个锤子,看啥都像钉子。由
map
,filter
和and_then()
堆叠成的链式调用会让代码可读性下降,而且频繁使用闭包,会让数据类型变得不再直观。下面有个例子,演示了迭代器如何让你的代码变得更复杂,你可以读一读这段代码,并猜猜它是干啥的:
在线示例。
看起来好像并不难,做个均值滤波罢了,不过我这里有个更好的实现:
在线示例。
我想你的心里已经有答案了吧。
不会用模式匹配
让我们回到一开始的
IndexOf()
函数,我们用Option
类型举了一个很好的例子,先看下原始代码:然后,你可能会看到这样的 Rust 代码:
或者这样的:
这些条件语句都在避免某些边界条件,不过就像之前说到的哨兵值一样,我们在重构的时候依然会极有可能引入 bug。
而使用 Rust 的模式匹配,你可以保证当且仅当值有效时才会执行到对应的代码:
相比于之前的代码,由于避免了
opt.unwrap()
和list[index]
,模式匹配可以有更好的性能(作者的一点忠告:不要在网上听风就是雨,如果你真的想知道真相,建议写个 Benchmark 验证下)。别再构造函数后初始化
许多语言都会在构造对象后调用对应的初始化函数(
init()
之类的),但这有悖于 Rust 的约定:让无效状态不可见。假设你在写个 NLP 程序,需要加载一个包含所有关键词的词表:
然而,如果这么写了,意味着
Dictionary
类有两个状态:空的和满的。那么如果后续代码假设
Dictionary
有值,并且直接使用它,那当我们错误地对一个空状态的Dictionary
进行索引时,就会造成 panic。在 Rust 中,最好在构造时就对结构体进行初始化,来避免结构体的空状态。
Dictionary::from_file()
直接执行了初始化操作,并返回了初始化后的、立即可用的结构体,从而避免了上述问题。当然,遇到这种问题的频率因人而异,完全取决于你的编码经验和代码风格。
一般来讲,函数式语言强调不可变性,所以函数式语言的使用者会天然地掌握这个经验。毕竟当你不能随便改变某个值时,你也不大可能创建一个初始化了一半的变量,然后再用什么其他值去填满它。
但面向对象的语言就不太一样了,它可能更鼓励你先构造个空对象,然后再调用具体函数初始化它,毕竟对象引用很容易为
null
,而且他们也不关心什么可变性之类的玩意儿……现在你知道为啥那些面向对象语言会经常由于NullPointerException
崩溃了吧。保护性拷贝
不可变对象的一个显而易见的优点时,你永远可以相信它不会发生变化,而放心地使用它的值。但某些语言,比如 Python 或者 Java,不可变性没有传递性。举个例子,
x
是个不可变对象,x.y
却不一定是不可变的,除非显式地定义它的可变性。这意味着会出现下面的 Python 代码:
后来其他人用了这个号称不可变的
ImmutablePerson
,但是却不小心把它搞乱了:我承认,这个例子确实有点刻意了,但是修改一个函数的传参却非常常见(译者按:尤其在某些深度学习项目里)。当你知道你自己定义的
ImmutablePerson
的addresses
属性不可变时,不会有什么大问题,但是当你和别人协作,而且别人还不知道addresses
不可变时,那就出大问题了。事情也不是无法挽回,解决这个问题的经典方法是,总是在获取属性值的时候,返回它的拷贝,而非它自己:
这样就可以保证别人在使用该对象的属性时,不会意外改变它的原始值。
考虑到这篇文章的主题是 Rust,你可能已经猜到了造成这种问题的根本原因:别名与可变性。
并且你也可能想到,这种情况不会发生在 Rust 中,生命周期机制以及“有且只能有一处可变引用”的机制,保证了程序员无法在取得变量的所有权情况下去改动它的值,也没法显式地使用
std::sync::Mutex<T>
去改变某个共享引用值。总结
本文并不能覆盖所有的最差实践,有些是因为我没亲身经历过,有些则是由于没法给出精简的例子。
衷心地感谢回复我在 Rust 论坛发布的这个帖子的各位同仁,尽管帖子的最后有点跑偏,各位 Rust 老鸟的论战还是让我受益颇深。
The text was updated successfully, but these errors were encountered: