🦀 Rust工程师养成记 Day2-掌握Rust基础语法

225 阅读12分钟

Rust简介

虽然大家来学 Rust 的应该都或多或少知道一些 Rust 的背景,但我还是要稍微介绍一下这门语言:

Rust 是由 Mozilla 团队打造的系统编程语言,巧妙平衡了硬件级性能控制与内存安全保障。其独特的所有权机制无需垃圾回收,成为C/C++的有力替代方案。2015年发布的1.0正式版确立稳定基础,采用六周迭代周期持续进化。该语言融合函数式编程特性,兼具底层控制能力和高级开发效率,在保证极致性能的同时显著降低编码复杂度

本节我就参照learnxinyminutes这个网站的相关Rust内容和大家一起过一下Rust的基本语法,为后面的学习做一下铺垫。

🔥 关注我的公众号「哈希茶馆」一起交流更多开发技巧

新建项目

开始肯定是利用我们上节做的模版直接一键三连🫣新建一个项目,边学边实践啦!

cargo generate EditYJ/rust-template
cd project-name
pre-commit install

基础

注释

Rust中的注释和Javascript基本一样,有单行注释,多行注释。他还有文档类型的注释,用来生成文档使用。

// 这是一个注释。行注释看起来像这样...
// 并且可以扩展到多行,像这样。

/* ...这是多行注释 */

/// 文档注释看起来像这样,并支持Markdown语法。
/// # 例如
///
/// ```
/// let five = 5
/// ```

函数 (Functions)

函数在大部分语言中都是很重要的一环,Rust中也不例外,fn关键词用来声明函数,入参和返回值都需要严格声明类型,它使用->声明返回值类型,最后的返回值可以不写return

// `i32` 是有符号 32 位整数类型(32-bit signed integers)
#[allow(dead_code)]
fn add2(x: i32, y: i32) -> i32 {
    // 隐式返回 (如果是这种返回形式需要不写分号)
    x + y
    // 声明返回需要加分号
	// return x + y;
}

细心的同学可能会观察到这个函数上面还有一个#[allow(dead_code)]的标记,这个是Rust中的,宏可以对Rust中的函数和变量做一些标注,经过编译器展开后会对这些被标注的函数和变量增加一些额外的修饰代码,后面我们还会去重点了解这一特性的。

每个Rust程序都会有一个函数,那就是主函数,打开我们新建的项目src/main.rs看看主函数的摸样

不可变量和可变量(let, let mut)

Rust中的变量声明以let开头,例如:

let x: i32 = 1;

let mut mutable_x: i32 = 1;

从这两句代码我们可以看出一些Rust声明变量的特点:

  1. 第一行以let开头,代表x不可变量,作用类似于Javascript中的const
  2. 第二行以let mut开头,代表mutable_x可变量,作用类似于Javascript中的let

之所以这样设计,是因为Rust设计者想让开发者在声明变量的时候尽量声明不可变量,在有需求的时候再加上mut使其变为可变量,这样设计是为了减少程序中一些变量在不可预知的条件下被改变,这个对于一个健壮的应用程序而言是非常重要的。

类型推导

// 类型推导
let implicit_x = 1;
let implicit_f = 1.3;

大部分情况中,Rust 编译器都会推导变量类型,所以不必把类型显式写出来。如果你在vscode中装了rust-analyzer插件,推导的类型大概会显示成下面这样:

字符串类型(String, &str)

我们可以直接声明一个字符串的字面量,&str表示是这个字面量的引用类型

// 字符串字面量
let x: &str = "hello world!";

// 输出
println!("{}", x);

我们cargo run一下,运行一下这个main函数

可以看到按照预想中的样子,输出了hello world,注意代码中的println!println后面加了一个!代表他是一个宏调用,他在编译的时候会被展开成一长串代码,你现在可以当做这个某几行代码的简写,后面我们在详细讲解这个东西。

我们还可以将&str通过to_string方法转为String类型的变量,String类型的变量会被分配到上:

// 在堆上分配空间的字符串
let s: String = "hello world".to_string();

// 可以通过&符号拿到他的不可变引用
let s_slice: &str = &s;

String是一个具有所有权的类型,所有权是Rust中一个重要的概念,后面我们会详细解释一下这方面的知识,现在我们来看一个例子,先简单了解一下所有权带给你的疑惑感:

fn main() {
	let s: String = "hello world".to_string();
	let s2 = s;
	println!("{}", s);
}

这段代码对于有编程经验的人来说再简单不过了,大部分人应该都会认为最后的结果肯定是打印出了hello world。但在Rust中,这段代码连编译都不会通过的,他会报错:

Rust编译器还贴心的为我们提供了解决方案,这边我就不多赘述了,着急的同学可以先搜一搜相关的资料,理解一下这边为什么会出错。

数组 (Vectors, arrays)

我们可以声明一个长度固定的数组,他会被放在上,[i32; 4]第二个参数代表数组的固定长度,这里这个数组的长度固定是4:

// 长度固定的数组 (array)
let four_ints: [i32; 4] = [1, 2, 3, 4];

我们还可以声明一个可变长数组Vec,他会被分配在上面,如果你要后续改变它的值,注意要加上mut,注意vec!println!一样也是一个宏调用,用来声明一个可变长数组,就像下面这段代码一样:

// 变长数组 (vector)
let mut vector: Vec<i32> = vec![1, 2, 3, 4];
vector.push(5);

当然也可以像上面的字符串一样拿到他的不可变视图slice: &[i32]

// 变长数组 (vector)
let mut vector: Vec<i32> = vec![1, 2, 3, 4];
vector.push(5);

let slice: &[i32] = &vector;
// 使用 `{:?}` 按调试样式输出
println!("{:?} {:?}", vector, slice); // [1, 2, 3, 4, 5] [1, 2, 3, 4, 5]

聪明的你一定想到了,那Vec是不是和String一样是一个拥有所有权的类型呢,答案是对的,这边我们只需先记住这一点。

元组 (Tuples)

元组是固定大小长度的一组值,可以是不同类型,他可以通过解构获取内部的值,也可以通过索引拿到某个位置的值:

// 内部可以是不同的值
let x: (i32, &str, f64) = (1, "hello", 3.4);

// 解构 `let`
let (a, b, c) = x;
println!("{} {} {}", a, b, c); // 1 hello 3.4

// 索引
println!("{}", x.1); // hello

类型

结构体(Sturct)

结构体我个人觉得很像为Typescript中的type,学过c/c++的同学对这个应该很熟悉,他在Rust中有两种形态,一种是有名字的结构体,还有一种叫匿名结构体

// 结构体(Sturct)
struct Point {
	x: i32,
	y: i32,
}
let origin: Point = Point { x: 0, y: 0 };

// 匿名成员结构体,又叫“元组结构体”(‘tuple struct’)
struct Point2(i32, i32);
let origin2 = Point2(0, 0);

从上面代码我们可以看到,通过结构体可以直接初始化一个对应类型的变量。

枚举(enum)

枚举在Rust下是非常强大的,在Rust下你可以将枚举理解为是多个不同类型的一个集合,其他语言中的枚举大部分只是状态的集合,类似于Rust下面这样的写法:

enum Direction {
	Left,
	Right,
	Up,
	Down,
}
let up = Direction::Up;

这段代码大家如果有编程基础的话应该都很熟悉,他就是枚举了上下左右四个状态,但是Rust的强大之处在于它还可以枚举不同的类型:

enum OptionalI32 {
	AnI32(i32),
	Nothing,
}

let two: OptionalI32 = OptionalI32::AnI32(2);
let nothing = OptionalI32::Nothing;

现在你可能还看不出这种方式的强大之处,这种用法在Rust构建的应用程序中非常常见,使用率是很高的,后面我们遇到了再详细体会一下这个形式的好处。

泛型 (Generics)

熟悉JavaTypeScript的同学对于泛型一定不陌生,听说Golang也加入的对泛型的支持,泛型对于我们的同类型行为代码的抽取是很重要的,类似大部分语言,Rust的泛型也是写在<T>内的,支持多个:

struct Foo<T> { bar: T }

// 这个在标准库里面有实现,叫 `Option`
enum Optional<T> {
	SomeVal(T),
	NoVal,
}

// 方法 (Methods) //
impl<T> Foo<T> {
	// 方法需要一个显式的 `self` 参数
	fn get_bar(self) -> T {
		self.bar
	}
}
let a_foo = Foo { bar: 1 };
println!("{}", a_foo.get_bar()); // 1

上面这段代码展示了几种泛型可以放的位置,这里出现了一个新的关键字implimpl这段代码的作用类似于像Java,Javascript中的类,他在向类中添加方法。这里他对于Foo结构体添加了一个get_bar的方法,这个方法接受的参数selfself代表Foo的实例对象,可以看到后面两行代码做了使用示例。

接口(Trait)

trait乍一看你可能觉得很陌生,其实不然,他在其他的编程语言里面很常见,Java和Typescript中叫他interface,这么一说,你是不是恍然大悟了。其实接口的作用就是对一个数据结构所能做的行为做定义。

trait Frobnicate<T> {
	fn frobnicate(self) -> Option<T>;
}
impl<T> Frobnicate<T> for Foo<T> {
	fn frobnicate(self) -> Option<T> {
		Some(self.bar)
	}
}

let another_foo = Foo { bar: 1 };
println!("{:?}", another_foo.frobnicate()); // Some(1)

上面的代码中trait Frobnicate定义了一个方法frobnicate。使用impl...for...关键字对Foo做了trait Frobnicate的实现,最后Foo的实例another_foo就可以调用frobnicate方法了。

模式匹配 (Pattern matching)

Rust中的模式匹配要比其他语言强大不少,先看看他和上面的枚举结合是怎么使用的:

enum OptionalI32 {
	AnI32(i32),
	Nothing,
}

let foo = OptionalI32::AnI32(1);
match foo {
	OptionalI32::AnI32(n) => println!("it’s an i32: {}", n),
	OptionalI32::Nothing  => println!("it’s nothing!"),
}

这种用法,想象大家已经看到这个枚举和模式匹配结合的厉害了,他们居然可以通过对类型的匹配,提取内部的值做处理,反正我在JavaScript中没见过这种搞法,js中只能对普通的值做匹配。

Rust中的模式匹配还可以精细到对对象内部的值做匹配,并且还可以添加一些逻辑,大家看下面这段代码:

struct FooBar { x: i32, y: OptionalI32 }
let bar = FooBar { x: 15, y: OptionalI32::AnI32(32) };

match bar {
	FooBar { x: 0, y: OptionalI32::AnI32(0) } =>
		println!("The numbers are zero!"),
	FooBar { x: n, y: OptionalI32::AnI32(m) } if n == m =>
		println!("The numbers are the same"),
	FooBar { x: n, y: OptionalI32::AnI32(m) } =>
		println!("Different numbers: {} {}", n, m),
	FooBar { x: _, y: OptionalI32::Nothing } =>
		println!("The second number is Nothing!"),
}

可以看到match的第二个匹配还加入了对n和m判断的匹配。

看的仔细的小伙伴可能发现了,这些match代码段中居然没有break的身影,那是因为match在Rust中的设计是自动跳出的,一旦某个分支匹配成功并执行完毕,整个match表达式就会结束,不会继续检查后续分支。哈哈!这个特性倒是我喜欢的,相信大家在平常开发中总是或多或少会被这个break坑过。

流程控制 (Control flow)

最后我们说一说Rust中的流程控制

for 循环

简单的for循环我们可以这样写

let array = [1, 2, 3];
for i in array {
	println!("{}", i);
}
// 输出:
// 1
// 2
// 3

还可以对一个区间做遍历

for i in 0u32..10 {
	print!("{} ", i);
}
println!("");
// 输出 `0 1 2 3 4 5 6 7 8 9 `

0u32..10是一个范围表达式,表示从09(不包括10)的整数序列。0u32表示一个无符号32位整数(u32类型),值为0..10表示范围的上限是10,但不包括10。所以最后输出的最大值只是到了9。

if

if在Rust中也有特殊用法,我们先看看他的一般用法:

if 1 == 1 {
	println!("Maths is working!");
} else {
	println!("Oh no...");
}

上述代码在我们的眼中应该没什么亮点,但是Rust的if是可以当表达式的,他可以这样使用:

// `if` 可以当表达式
let value = if true {
	"good"
} else {
	"bad"
};

这个就可以通过条件判断对value赋值。

while循环

while循环和其他语言中应该差别不大,Rust中还提供了loop关键字,专门处理无限循环的情况

// `while` 循环
while 1 == 1 {
	println!("The universe is operating normally.");
}
// 无限循环
loop {
	println!("Hello!");
}

总结

今天我们一起过了一下Rust中的基本语法,如果大家对着文档敲了一遍上述的代码的话,相信你对Rust已经有了一个初步的了解,大家如果有什么问题可以留言给我进一步交流哦!

下节预告

下节我们将用Rust做一个简单的猜价格的小例子,巩固一下今天学的基本语法,现在感觉不错的同学可以乘胜追击,自己尝试实现看看!

🔥 关注我的公众号「哈希茶馆」一起交流更多开发技巧