Rust的缺点
这世界有太多的传教士了,比如一部分Haskell传教士会拿一个两行的快排,但是不告诉你那不是原地排序,而等价的原地排序可能比java还长。
那Rust真的有传教士说的那么美吗,是否遗漏了什么。
trait不是类型,同时也不支持子类型
当然,这可能是设计需要,但让人难受但地方,我先把他归类为缺点。
1 | fn compare(a: Ord,b: Ord) -> bool{ |
1 | error[E0038]: the trait `std::cmp::Ord` cannot be made into an object |
不过你可以用类型推导,这里要求该类型实现了Ord
trait, 于是我们就可以用<
来比较了。
1 | fn compare<A:Ord>(a: A,b: A) -> bool{ |
这与子类型有本质的区别,比如在返回值位置就没办法这么写了。返回值在函数内部已经给出,推导的时候就对不上了。
再愛你,依然是兩個人~
1 | fn compare<A:Ord>() -> A{ |
1 | error[E0308]: mismatched types |
不是所有的值都有一个明确的类型,比如闭包,这个时候就要存在类型:
世界上没有两片相同的叶子,Rust里也没有两个的闭包
1 | fn counter() -> impl Fn() -> i32{ |
与此同时存在类型是一个非常难用的东西,他对应存在量词
延时求值
1 | trait Iterator{ |
听上去很美,但假设他不做所谓的延时求值,甚至都没有办法抽象出来一套东西。
比如map方法并没有立刻转换,而是直接把原来的Iterator和函数包在了结构体里:
1 | fn map<B, F>(self, f: F) -> Map<Self, F> where |
假如你想把他的返回值改成Iterator<Item=B>
是编译不过的,因为trait不是类型。这里需要一个具体的类型,比如结构体或者枚举,于是你会看到大量这样的代码:
1 | fn step_by(self, step: usize) -> StepBy<Self> |
每定义一个新的方法,你就需要定义一个新的结构体(因为你不能在同一个结构体上实现同一个trait两次),然后再在结构体上实现trait的方法。这种代码风格Java程序员一定很熟悉,我把他称为实名内部类。如果比Iterator官方实现的代码长度,Rust肯定是首屈一指的。
如果只是代码长倒不是很大的问题,毕竟我是一名Java程序员。
再看一个具体一点的问题:
1 | fn flat_map<U, F>(self, f: F) -> FlatMap<Self, U, F> |
F
(这里代表一个函数)的返回值类型U
是根据类型推导来的,这就造成一个问题,假如函数中有if else或者模式匹配这样的分支,两个分支的类型会对不上。注意这里如果不用return,if表达式的类型就已经对不上了(在Rust中if else是一个表达式,和Scala类似)。
1 | let j = (1..9).flat_map(|x|{ |
1 | error[E0308]: mismatched types |
假如你的代码里出现了分支,其中一个用的map,另外一个用的filter,那他类型是对不上的,甚至于说都是map,闭包的类型也对不上(闭包是Map的类型参数)。
如果你听说Rust支持type class,想来搞点函数式,你是否搞错了什么?
面向对象
你不要把FP那套东西搞过来,同时也不要OOP那套东西搞过来,Rust就是Rust。
Rust可以用trait object来表达多态,但是编译器需要在编译期知道函数的参数、局部变量的大小,所以这里必须套上一个引用或者Box。而且编译器对trait object有额外的限制:
Object Safety Is Required for Trait Objects
You can only make object-safe traits into trait objects. Some complex rules govern all the properties that make a trait object safe, but in practice, only two rules are relevant. A trait is object safe if all the methods defined in the trait have the following properties:
- The return type isn’t Self.
- There are no generic type parameters.
https://doc.rust-lang.org/book/ch17-02-trait-objects.html
所有的方法都不能有泛型参数,零成本抽象。不过在老一点的文档里有另外一条,这一点我也认为非常坑爹,Rust的文档散落在世界的各个角落里。
你渴望力量吗?
https://doc.rust-lang.org/book/title-page.html
https://doc.rust-lang.org/edition-guide/rust-2018/index.html
https://doc.rust-lang.org/1.19.0/book/first-edition/README.html
在老版的Rust Book里面关于object safety有更详细的描述:
The error says that
Clone
is not ‘object-safe’. Only traits that are object-safe can be made into trait objects. A trait is object-safe if both of these are true:
- the trait does not require that
Self: Sized
- all of its methods are object-safe
So what makes a method object-safe? Each method must require that
Self: Sized
or all of the following:
- must not have any type parameters
- must not use
Self
Each method must require that Self: Sized
or all of the following…
新版不仅把这句话删了,我不知道是怎么想的。这里可以理解为加了Self: Sized
就可以绕过这个限制,与此同时trait object就不能再调用这个方法了,欲练此功,必先自宫。
依据这一条,我们可以这样来定义Iterator。
1 | trait Iterator<E>{ |
缺点就是你需要在各种地方套Box。
引用
叔叔,叔叔,听说你们这个语言写不了链表是吗?
Rust的引用规则很简单:可变引用可以同时存在多个,不可以引用同时只能存在一个,而可变引用存在的时候不能创建不可变引用。
常见的链表next是一个可变引用,tail指针也是可变的,他们同时指向链表的最后一个节点,这是Rust绕不过去的坎。
规则简单,不代表他用起来容易。你可以看到这个规则,就马上想到Rust不用unsafe无法实现链表,然后享受一个周末而不是和编译器死磕吗?
生命周期
王垠:rust所谓的生命周期就是把引用计数限制为1,那处理起来当然简单了。
不得不佩服王垠的敏锐,那么这句话要如何理解呢?因为看上去rust允许多个不可变引用。
这也是生命周期的设计理念,引用的生命周期被包含在值的生命周期内,所以他可以直接忽略,由所有者负责释放,而所有者就是那个1。这也是为什么你想在线程之间传引用通常是一个错误的选择。
而可变引用的不同之处在于,假如把引用指向另外一个值,他需要释放前一个值。
这都不知道?再去看看TRPL吧,都在里面。
TRPL
说到这里就不得不提The Rust Programming Language
,Rust吹们把他简称为TRPL以示尊敬。你必须知道它,同时也必须知道这个缩写。
那么TRPL,有没有那么详细?显然是没有的,首先他相对于老的文档做过一定的删减,比如上面提到的trait object。那回到正题,我认为这篇文档漏掉了一个相当重要的东西就是std::mem::replace
,Option里把他包装成take
。他的作用是把旧的值取出来,同时给一个新值。这样我们就可以对原来的值随意操作了。我认为在你知道这个东西之前,你可能至少会浪费一个星期尝试去对可变引用做模式匹配,直到你知道有这个东西,或者精通unsafe。
比如,假如我想把栈顶取出来,然后向后挪一位,可能会这么实现。
1 | enum Stack<E> { |
那么你显然犯了错误,self可变引用存在的同时,模式匹配会创建新的引用。那有了replace,就可以这样写。
1 | fn pop(&mut self) -> Option<E>{ |
但是这显然不够零成本抽象,因为Less是一个垃圾值,随后就会被释放掉。当然你也可以用unsafe。
1 | fn pop(&mut self) -> Option<E>{ |
什么,竟然有人在用unsafe? unsafe 警察,出警👮♀️!
学习成本
很多人以为Rust学习成本很高,他只不过把复杂的部分都暴露出来了。
不,Rust的学习成本比你想象的还要高。不仅仅是学习语法,而是花大量但时间总结在这一套限制之下该如何写代码。正如同学习如何戴着镣铐跳舞💃💃💃。
那么Rust值得吗?