Rust的缺点

这世界有太多的传教士了,比如一部分Haskell传教士会拿一个两行的快排,但是不告诉你那不是原地排序,而等价的原地排序可能比java还长。

那Rust真的有传教士说的那么美吗,是否遗漏了什么。

trait不是类型,同时也不支持子类型

当然,这可能是设计需要,但让人难受但地方,我先把他归类为缺点。

1
2
3
fn compare(a: Ord,b: Ord) -> bool{
a < b
}
1
2
3
4
5
6
7
error[E0038]: the trait `std::cmp::Ord` cannot be made into an object
--> src/main.rs:2:15
|
2 | fn compare(a: Ord,b: Ord) -> bool{
| ^^^ the trait `std::cmp::Ord` cannot be made into an object
|
= note: the trait cannot use `Self` as a type parameter in the supertraits or where-clauses

不过你可以用类型推导,这里要求该类型实现了Ord trait, 于是我们就可以用<来比较了。

1
2
3
fn compare<A:Ord>(a: A,b: A) -> bool{
a < b
}

这与子类型有本质的区别,比如在返回值位置就没办法这么写了。返回值在函数内部已经给出,推导的时候就对不上了。

再愛你,依然是兩個人~

1
2
3
fn compare<A:Ord>() -> A{
1
}
1
2
3
4
5
6
7
8
9
10
error[E0308]: mismatched types
--> src/main.rs:3:5
|
2 | fn compare<A:Ord>() -> A{
| - expected `A` because of return type
3 | 1
| ^ expected type parameter, found integer
|
= note: expected type `A`
found type `{integer}`

不是所有的值都有一个明确的类型,比如闭包,这个时候就要存在类型:

世界上没有两片相同的叶子,Rust里也没有两个的闭包

1
2
3
4
5
6
7
8
fn counter() -> impl Fn() -> i32{
let mut i = 0;
|| {
let r = i;
i = i + 1;
r
}
}

与此同时存在类型是一个非常难用的东西,他对应存在量词,而范型对应全称量词

延时求值

1
2
3
4
trait Iterator{
type Item;
fn next(&mut self) -> Option<Self::Item>;
}

听上去很美,但假设他不做所谓的延时求值,甚至都没有办法抽象出来一套东西。

比如map方法并没有立刻转换,而是直接把原来的Iterator和函数包在了结构体里:

1
2
3
4
5
fn map<B, F>(self, f: F) -> Map<Self, F> where
Self: Sized, F: FnMut(Self::Item) -> B,
{
Map::new(self, f)
}

假如你想把他的返回值改成Iterator<Item=B>是编译不过的,因为trait不是类型。这里需要一个具体的类型,比如结构体或者枚举,于是你会看到大量这样的代码:

1
2
3
4
5
6
7
fn step_by(self, step: usize) -> StepBy<Self>
fn chain<U>(self, other: U) -> Chain<Self, U::IntoIter>
fn zip<U>(self, other: U) -> Zip<Self, U::IntoIter>
fn map<B, F>(self, f: F) -> Map<Self, F>
fn filter<P>(self, predicate: P) -> Filter<Self, P>
fn enumerate(self) -> Enumerate<Self>
fn peekable(self) -> Peekable<Self>

每定义一个新的方法,你就需要定义一个新的结构体(因为你不能在同一个结构体上实现同一个trait两次),然后再在结构体上实现trait的方法。这种代码风格Java程序员一定很熟悉,我把他称为实名内部类。如果比Iterator官方实现的代码长度,Rust肯定是首屈一指的。

如果只是代码长倒不是很大的问题,毕竟我是一名Java程序员。

再看一个具体一点的问题:

1
2
3
4
5
fn flat_map<U, F>(self, f: F) -> FlatMap<Self, U, F>
where Self: Sized, U: IntoIterator, F: FnMut(Self::Item) -> U,
{
FlatMap::new(self, f)
}

F(这里代表一个函数)的返回值类型U是根据类型推导来的,这就造成一个问题,假如函数中有if else或者模式匹配这样的分支,两个分支的类型会对不上。注意这里如果不用return,if表达式的类型就已经对不上了(在Rust中if else是一个表达式,和Scala类似)。

1
2
3
4
5
6
7
let j = (1..9).flat_map(|x|{
if (x % 2) == 0 {
return empty();
}else{
return vec![x].iter();
}
}).collect();
1
2
3
4
5
6
7
8
error[E0308]: mismatched types
--> src/main.rs:8:16
|
8 | return vec![x].iter();
| ^^^^^^^^^^^^^^ expected struct `std::iter::Empty`, found struct `std::slice::Iter`
|
= note: expected type `std::iter::Empty<_>`
found type `std::slice::Iter<'_, {integer}>`

假如你的代码里出现了分支,其中一个用的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
2
3
4
5
6
7
trait Iterator<E>{
fn next(&mut self) -> E;
fn flat_map<B,F>(self,f: F) -> FlatMap<Self,F>
where Self: Sized,F: Fn(E) -> Box<dyn Iterator<B>>{
Map(self,f)
}
}

缺点就是你需要在各种地方套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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum Stack<E> {
More(E,Box<Stack<E>>),
Less
}

impl <E> Stack<E> {
fn new() -> Stack<E>{
Stack::Less
}

fn pop(&mut self) -> Option<E>{
match self {
Stack::More(item,next) => {
*self = **next;
Some(*item)
},
Stack::Less => None
}
}
}

那么你显然犯了错误,self可变引用存在的同时,模式匹配会创建新的引用。那有了replace,就可以这样写。

1
2
3
4
5
6
7
8
9
10
fn pop(&mut self) -> Option<E>{
let pre = std::mem::replace(self, Stack::Less);
match pre {
Stack::More(item,next) => {
*self = *next;
Some(item)
},
Stack::Less => None
}
}

但是这显然不够零成本抽象,因为Less是一个垃圾值,随后就会被释放掉。当然你也可以用unsafe。

1
2
3
4
5
6
7
8
9
fn pop(&mut self) -> Option<E>{
match unsafe{std::ptr::read(self)} {
Stack::More(item,next) => {
unsafe{std::ptr::write(self, *next)}
Some(item)
},
Stack::Less => None
}
}

什么,竟然有人在用unsafe? unsafe 警察,出警👮‍♀️!

学习成本

很多人以为Rust学习成本很高,他只不过把复杂的部分都暴露出来了。

不,Rust的学习成本比你想象的还要高。不仅仅是学习语法,而是花大量但时间总结在这一套限制之下该如何写代码。正如同学习如何戴着镣铐跳舞💃💃💃。

那么Rust值得吗?