Rust体验:编写n皇后

这辈子不想写第二个Rust程序了!(真香)

Posted by wyj on May 31, 2020

这几个月反复看见知乎等等各种地方很多人狂吹Rust,说这是将来可以取代C / C++的语言,我就忍不住好奇,去尝试了一下。结果令我相当的不爽,以后再也不写Rust了。

安装

apt大法好

sudo apt install rustc

吐槽

我曾经学过的语言中,Pascal让我感到严谨但冗长,C / C++是飞快却危险,Python是简洁优美却龟速,JavaScript是无任何限制的绝对自由与随之而来的各种坑。然而感觉Rust这门语言纯粹是为了恶心人而设计出来的,只不过附带了个完全得不偿失的“绝对安全”保障。

作为对照,让我们来回顾一下C语言的$n$皇后是怎么写的:代码链接。这份程序故意写得很通用,没有使用任何奇怪的语言特性,可以用最小的代价移植到其他语言当中。我努力做到保证实现细节尽量一致,然而移植到Rust之后还是变得面目全非了代码链接

可以看到,为了实现同样的功能,代码长度膨胀了$2.15$倍。当然有人会说了,C语言天生适合短码,这么比较不公平。那么就挑公认极为冗长的Pascal实现对比吧:长度甚至是Pascal代码的$1.64$倍

所以Rust到底干了什么恶心的事让代码必须这么长呢?下面我来具体分析一下。

没有全局变量

是的。这个愚蠢的语言甚至不允许全局变量!!!!!这意味着不同的函数之间完全不能共享信息。我可不想把每个变量,甚至数组,都当成函数的参数到处传来传去。就算栈空间受得了,常数也受不了啊!并且$n$皇后需要递归,几乎不可能不使用多个共享信息的函数。

然而Java也没有全局变量,人们不还是照样写Java吗?所以参考一下Java,唯一的解决方案是把需要共享信息的函数全部封装到一个类之中。幸亏Rust支持struct,还有救。

和C语言媲美的面向对象

然而Rust的类也不按套路出牌,和其他语言完全不一样,必须把数据和成员函数分开写!这个特性我只在Pascal的unit语法中见过。

并且,Rust的类没有构造函数。我必须手动实现一个new功能来创建一个类。并且类的成员不能有默认值,在new函数中我必须手动把所有“全局变量”手动初始化成0。哦,如果你非要钻牛角尖的话,可以有默认初始化,语法是这样的

#[derive(Default)]
struct SomeOptions {
    foo: i32,
    bar: f32,
}
fn main() {
    let options = SomeOptions { foo: 42, ..Default::default() };
}

如果你觉得这样比手动初始化成0简单而好懂的话,也可以这么写。

并且类的成员函数很奇怪,第一个参数必须和Python一样是self,成员函数内访问类中的变量$k$和Python一样必须用self.k不能省略这个self。导致代码里每句话都要写三四个完全没必要的self.,相当冗长:

self.a|=1<<i;self.b|=1<<i+d;self.c|=1<<i+self.n-d;

说到这里,是不是隐隐约约觉得这个奇怪的“类”语法好像在哪里见过?C语言的伪“面向对象”不也恰好是这么写的吗!C代码:

typedef struct _st{
	int a;
	void(*F)(struct _st*);
}st;
void _st_impl_F(st*self){self->a=7;}
st* new(){
	st* inst=malloc(sizeof(st));
	inst->F=_st_impl_F;
	return inst;
}

所以Rust的这个所谓的“成员函数”,本质上和C是一样的,(至少在语法上)相当原始而落后。打个比方:我和我妈说话肯定称呼她为“妈”,Rust却规定必须叫她“我妈”。真的多此一举。

变量声明

Rust的变量声明语法是反人类的,怎么恶心人怎么来。

众所周知,任何一个程序中变量的个数都肯定远远多于常量,所以在任何一门语言中声明变量的语法都比声明常量短。然而Rust偏偏反其道而行之,常量声明是let a=1,变量却要let mut a=1。引用也是这样,在C++中&默认是可变引用,const &才是(用得少很多的)不可变引用。Rust的&却偏偏不可变,非要&mut才能变。增加了代码的长度。

这还不够,在C、C++和Pascal中,无论类型前置还是后置,都可以把相同类型的变量合并到一起写。Rust却强行让人分开来

struct Main{
	n:i32, cnt:i32, a:i32, b:i32, c:i32
}

幸亏这里类型名比较短,要是类型名字一长,完全不能看了。

并且Rust还喜欢脱裤子放屁。我$F$函数的声明已经写成F(&mut x:i32)了,编译器和我都知道$F$的参数必须是可变引用了。我调用$F$的时候还是一定要被逼无奈地写成F(&mut x)而不是F(x),编译器简直把我像个zz一样对待。

这条和上一条结合起来就更加变态了,我每一个成员函数不仅仅是一定要把self当成第一个参数,还TM必须是&mut self,否则我可怜的函数就会像C++的const成员函数一样,啥都改不了。

语法功能不全

这点上Rust和Pascal比较像,连从i32自动转换到bool的功能都没有。这让if语句变得繁琐。不过我不得不承认,JavaScript在这一方面实在是灵活得过了头。

还是和Pascal比较像,连++--这样简单的运算都不支持,只能+=1-=1

语法繁琐而具有迷惑性

没有方便的输入方法。连JavaScript这样只专注于网络的语言,控制台输入都比Rust简短。随便干点啥都需要unwrap()一下,就算我完全不关心是否会抛异常。

和前文的Rust没有构造函数,变量声明语法特别反人类这两点结合起来,导致我只是想输入一个整数$n$就需要写这么冗长的三行

let mut input=String::new();
io::stdin().read_line(&mut input).unwrap();
let mut n=input.trim().parse().unwrap();

甚至连Pascal都不如的是,Rust的iffor里就算只有一条语句,也必须用一对大括号括起来。完全无法忍受。

还有表示范围的..语法,众所周知$1\dots n$一般表示$[1,n]$,在bash里{1..n}也是$[1,n]$,Rust却非要令1..n表示$[1,n)$,特别容易让人混淆。

投向unsafe的怀抱

rust真是一门恶心人的语言。但是如果可以使用全局变量,就不用管和面向对象有关的那么多麻烦事了,代码也会急剧缩短。此时unsafe关键字开始诱惑我了,我二话不说就抛弃了所谓的安全性,投入了unsafe的怀抱。unsafe Rust可以使用static mut的全局变量,虽然声明还是一如往昔那么繁琐,一条int n,cnt,a,b,c;相当于:

static mut n:i32=0;
static mut cnt:i32=0;
static mut a:i32=0;
static mut b:i32=0;
static mut c:i32=0;

每一个变量都必须显式初始化,必须指定类型,必须分开来写$\dots$然后每个函数前面加上unsafe关键字,就可以爽快的使用人类看得懂的Rust了。“简短”的代码只有Pascal代码的1.2倍长了!

为数不多的闪光点

Rust具有Cargo这一包管理器,而C++目前还缺少一个包管理器。

我觉得变量类型后置、函数返回值后置这两点大大改善了语言的可读性,比C / C++好一些。

函数的返回不用写returnif语句也有返回值(代替了三目运算符?:),这两点还是很棒的。

速度测试

都在time.md里面了。可以看到Rust和C++速度上的差距相当于C++和C的差距,但是仍然远快于除了C / C++之外的所有语言。(2020-08-09 更新 Rust比C++只慢在读入上。。。。把读入改成extern"C" scanf就可以4.671s了。其实Rust不比C++慢。)可惜的是,unsafe Rust虽然写起来爽,但是编译器貌似就不敢添加一些优化了,导致其比safe Rust慢一些。

就连编译时,Rust也不忘恶心我一下,每一个编译选项前面都必须加上一个-C,不能写在一起,C里的-Ofast -march=native,到了Rust这儿就变成了-C opt-level=3 -C target-cpu=native,不知道当编译选项有几十个时这一堆冗余的-C会有多壮观。