Rust
本篇文章记录一下Rust的入门学习过程,主要是一些基础的语法和一些常用的库的使用,以及一些常见的问题的解决方法。其实本来就想开始入门了,但是这学期教授们不知道怎么的,不约而同把作业量提到了往年的两倍,所以春假才有时间搞这个。
长期规划
因为今年实习被分配写rust,所以打算下一个项目用rust写一个kv数据库,类似于Leveldb那样。模仿一下前人的GitHub。但是在此之前,得先了解rust的基本语法和一些常用的知识。
基础语法
这个给我的感觉就是C++,golang和Ocaml揉碎了成一团之后的产物。当我看到let,match和option的时候我就知道为啥我被组长挑来写rust了哈哈哈。但是其中还是有很多imperative language feature的。虽然我目前还是不是很能体会为什么很多设计者试图在rust中追求functional language的feature,但是这种设计确实将rust的入门门槛和可用性降低了很多。我还记得当时有人评价Ocaml说,Once it compiles, it will never crash。在写代码的时候严格约束确实是一件好事。
基本语法
变量
首先所有的变量分为可变和不可变两种,可以和Ocaml一样用let声明,但是不同的是,rust的let是一个表达式,而不是一个语句。所以可以这样写:
let x = 5;
let y = {
let x_squared = x * x;
x_squared * x_squared
};
这句话的等价于:
int x = 5;
int x_squared = x * x;
int y = x_squared * x_squared;
和Ocaml的
let x = 5 in
let x_squared = x * x in
let y = x_squared * x_squared in
y
这种写法是一样的。这种写法的好处是可以在let中定义一些临时变量,而不用担心变量名的冲突,非常方便。
可变vs不可变
rust中的变量分为可变和不可变两种,不可变的变量在定义之后就不能再次赋值,而可变的变量可以在任何地方被赋值。默认情况下,变量是不可变的,如果想要定义一个可变的变量,需要在let前面加上mut关键字。比如:
let mut x = 5;
x = 6; // OK
let y = 5;
y = 6; // Error
数据类型
跳过基础类型,我们来看复合类型吧。
首先是arr和vec,这两个都是数组,但是vec是可变的,arr是在编译期确定的大小,有点类似于std::array和std::vector,但是vec的实现是unsafe的,但是显然也没法被低成本替换。
let a = [1, 2, 3, 4, 5];
let mut v = vec![1, 2, 3, 4, 5];
v.push(6); // OK
println!("{:?}", v); //1 2 3 4 5 6
println!("{:?}", a); //1 2 3 4 5
那么我们怎么访问数组中的元素呢?因为rust是strongly typed,所以我们需要指定下标中元素的类型。
在cpp中,我们可以使用int作为subscript,但是这里可能要改一下
int a[30];
for (int i = 0; i < 30; i++) {
a[i] = i;
}
在rust中,我们需要使用usize作为subscript,这个类型是一个unsigned int,所以我们这样写会哟对岸问题
let mut a = [0,1,2,3,4,5];
for i in 0..3 {
v[i] = i; //error
}
这里会报错,因为usize是一个unsigned int,但是for循环中的i是一个signed int,所以会报错。所以我们需要这样写:
for i in 0..3 {
v[i as usize] = i; //OK
}
虽然没cpp自由,但是在大型项目中,这种类型检查还是很有必要的。把时间花在写起来能跑的代码上总比花时间在debug上好。
如果我们需要对一个array的某一个范围进行排序,我们可以这样写:
let mut a = [5, 4, 3, 2, 1];
a[1..4].sort();
println!("{:?}", a); //5 2 3 4 1
或者获取mutable slice
let mut a = [5, 4, 3, 2, 1];
let b = &mut a[1..4];
b.sort();
println!("{:?}", a); //5 2 3 4 1
我们可以自定义比较符号
let mut a = [5, 4, 3, 2, 1];
a[1..4].sort_by(|a, b| b.cmp(a));
println!("{:?}", a); //5 4 3 2 1
或者自定义比较函数
let mut a = [5, 4, 3, 2, 1];
a[1..4].sort_by(|a, b| b.partial_cmp(a).unwrap());
println!("{:?}", a); //5 4 3 2 1
Match关键字
match关键字是rust中的一个非常重要的关键字,它可以用来匹配一个值的类型,然后执行相应的代码。比如:
这个和Ocaml里面的match关键字是一样的,但是rust的match更加高级,因为加入了一些imperative language的内容,所以肯定有所改变
常用的match就不说了,比如高级if
fn work(x: i32) -> Option<i32> {
if x < 0 {
None
} else {
Some(x)
}
}
fn main() {
let x = 5;
match work(x) {
Some(y) => println!("x is {}", y),
None => println!("x is negative"),
}
}
range match
fn main() {
let x = 5;
match x {
1 => println!("one"),
2 | 3 | 5 | 7 | 11 => println!("x is a prime"),
13..=19 => println!("x is a teen"),
_ => println!("x is something else"),
}
}
然后就是function match
fn test() -> i32 {
5
}
fn main() {
match test() {
0 => println!("zero"),
n @ 1..=12 => println!("n is {}", n),
n @ 13..=19 => println!("n is a teen"),
_ => println!("n is something else"),
}
}
这里的n是把test()这么一个暂时的结果绑定到了n上,使得我们可以在后面的代码中使用n。
Struct
rust的struct就是golang和cpp中的struct,但是rust中的struct是可以有方法的。但是不得不说,这点很像golang。在golang里面,我们一般是有一个类的interface,然后会有一个XXImpl类去实现抽象方法和多态,但是在rust里面,这么多并不是选择题,而是必须的。比如我定义一个foo的struct,那么我就必须要实现一个foo的方法,否则就会报错。这样的好处是,我们可以在编译期就发现一些错误,而不是在运行期。
struct Foo {
x: i32,
}
impl Foo {
fn new(x: i32) -> Foo {
Foo { x }
}
fn foo(&self) {
println!("x is {}", self.x);
}
}
fn main() {
let f = Foo::new(5);
f.foo(); //5
}
这点不得不说真的非常像golang,也不知道是golang借鉴了rust还是rust借鉴了golang。
然后我们可以来写一道简单的算法题,比如最大子段和
fn main() {
let cin = stdin();
let mut line = String::new();
cin.lock().read_line(&mut line).unwrap();
let n: i32 = line.trim().parse().unwrap();
cin.lock().read_line(&mut line).unwrap();
let mut a:Vec<i32> = line.trim().split_whitespace().map(|x| x.parse().unwrap()).collect();
let mut ans:i32 = 0;
let mut cur:i32 = a[0];
let mut prev:i32 = 0;
for i in 1..n {
cur = max(prev, 0) + a[i as usize];
ans = max(ans, cur);
prev = cur;
}
println!("{}", ans);
}
这个我们实际上是要用buffered reader来读取,但是这里为了方便,就直接用了cin了。
Trait
Trait是rust中的一个非常重要的概念,它是一种抽象的接口,比如我们可以定义一个trait,然后让struct去实现这个trait,这样的话,我们就可以在函数中传入这个trait,而不是具体的struct,这样的话,我们就可以在函数中使用这个trait的方法,而不是具体的struct的方法。这样的话,我们就可以在函数中使用多态了。
trait Foo {
fn foo(&self);
}
struct Bar {
x: i32,
}
impl Foo for Bar {
fn foo(&self) {
println!("x is {}", self.x);
}
}
fn test<T: Foo>(t: T) {
t.foo();
}
fn main() {
let b = Bar { x: 5 };
test(b);
}
这是一种非常强大的工具。我们具有了C++ programmer所有的工具和能力,去构造更多的,更高效的程序。