rust语言基础学习笔记(上)
2020-07-01
1.语言环境安装和开发工具准备 #
1.1 Rust语言环境安装 #
MacOS下安装rust:
1brew install rustup
2
3rustup-init
4
5source ~/.cargo/env
1rustc --version
2
3cargo --version
1.2 开发工具VSCode + Rust扩展 #
VSCode中安装Rust扩展, 可参考https://code.visualstudio.com/docs/languages/rust。
当前我主要使用下面这些扩展:
- rust-analyzer: Rust官方推荐的Rust语言的VSCode扩展
- CodeLLDB: 调试器, 在线安装这个扩展时可能会出现
Acquiring CodeLLDB platform package...
下载很慢的情况,建议直接从https://github.com/vadimcn/vscode-lldb/releases下载扩展的vsix文件,离线安装。 - crates: 分析当前项目中的依赖及版本更新
- Better TOML: 为
cargo.toml
文件提供语法高亮 - Rust VS Code Test Explorer: 可以从VSCode侧边栏查看和运行的Rust测试
1.3 HelloWorld #
cargo是rust的构建系统和包管理器,使用cargo可以创建rust项目。
创建项目:
1cargo new c01_hello
2Created binary (application) `c01_hello` package
项目目录结构如下:
1c01_hello
2├── .git
3├── .gitignore
4├── Cargo.toml
5└── src
6 └── main.rs
main.rs:
1fn main() {
2 println!("Hello, world!");
3}
在项目目录中执行cargo run
运行程序。
关于cargo命令, 直接输入cargo可以打印出命令选项和子命令说明:
当项目最终准备好发布时,可以使用cargo build --release
来优化编译项目。这会在target/release
而不是target/debug
下生成可执行文件。这些优化可以让Rust代码运行的更快,但启用这些优化也需要消耗更长的编译时间。
2.变量和数据类型 #
2.1 变量定义 #
定义常量:
1const MAX_LENGTH: u32 = 99999;
变量定义支持类型推导:
1let hi = "hello";
let声明的变量默认是不可变的(immutable):
1let num = 2;
2num = 3; // 编译错误
let mut
声明mmutable变量:
1let mut num2 = 2;
2println!("num2 = {}", num2);
3num2 = 3;
4println!("num2 = {}", num2);
变量隐藏特性,后边定义的变量会隐藏前面已经定义过的同名变量:
1let mut num2 = 2;
2println!("num2 = {}", num2);
3
4let num2: f32 = 3.5;
5println!("num2 = {}", num2);
2.2 数据类型 #
基本数据类型比较简单,这里略过。
数组:
1let arr: [u32; 5] = [1, 2, 3, 4, 5];
2println!("arr[0] = {}", arr[0]);
3for ele in &arr {
4 println!("{}", ele)
5}
元组:
1// 元组 (String, i32, f64)
2let tup: (String, i32, f64) = (String::from("ha"), 3, 6.4);
3println!("{}", tup.0);
4println!("{}", tup.1);
5println!("{}", tup.2);
3.程序流程控制 #
这里只记录和其他编程语言不一样的地方。
if
可以和let
的变量声明一起使用:
1let ok = true;
2 let score = if ok {
3 100
4 }else {
5 59
6 };
循环可以使用loop
, for
, while
:
1let mut i = 1;
2loop {
3 if i > 10 {
4 break;
5 }
6 println!("in loop, current is {}", i);
7 i += 1
8}
4.函数 #
1fn add(num1 :i32, num2 :i32) -> i32{
2// let sum = num1 + num2;
3// return sum;
4 num1 + num2
5}
6
7let sum = add(1, 2);
8println!("{}", sum);
5.所有权 #
不同的编程语言写的程序在运行时可能采用不同类型的内存管理方式,一些语言(如:Go、Java、Python)具有垃圾回收机制; 另外有一些语言必须由程序员自己亲自分配和释放内存;rust则采用第三种方式: 通过所有权系统管理内存。
rust通过所有权机制管理内存,编译器在编译时会根据所有权规则对内存的使用进行检查。在运行时,所有权系统的任何功能都不会减慢程序。
所有权的存在主要是为了管理堆数据的,所有权系统负责跟踪那部分代码正在使用堆上的哪些数据,最大限度的减少堆上的重复数据数量,以及清理堆上不再使用的数据确保不会耗尽空间。
5.1 所有权规则 #
- rust中每一个值都有一个被称为其所有者(owner)的变量
- 一个值在任何时刻有且只有一个所有者
- 当所有者(变量)离开作用域时,这个值将被丢弃
5.2 所有权与变量(变量与数据交互的方式) #
在rust中,内存在拥有它的变量离开作用域后就会被自动释放。
1{
2 let s = String::from("hello"); // 从此处起,s 是有效的
3 // 使用 s
4} // 此作用域已结束,
5 // s 不再有效
上面的代码,首先String::from("hello")
是在堆上分配字符串,在s离开作用域时,Rust自动调用drop
函数并清理变量的堆内存。
1let s1 = String::from("hello");
2let s2 = s1;
3println!("{}, world!", s1); // 编译错误: use of moved value: `s1`
为了避免出现内存二次释放的问题,上面的代码当s1
被赋给s2
后,s1
不再有效,当s2, s1离开作用域时只有s2会清理变量的堆内存。
这里相当于s1
被移动到了s2
中。
rust永远不会自动创建数据的"深拷贝",如果确实需要深度复制String中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做clone
的通用函数。
1let s1 = String::from("hello");
2let s2 = s1.clone();
3println!("s1 = {}, s2 = {}", s1, s2);
只有在栈上的数据变量间赋值时才是拷贝(copy),而对在堆上的数据变量间赋值时是移动(move)。 下面是一些Copy的类型:
- 所有整数类型,比如 u32。
- 布尔类型,bool,它的值是 true 和 false。
- 所有浮点数类型,比如 f64。
- 符类型,char。
- 元组,当且仅当其包含的类型也都是Copy的时候。比如,(i32, i32) 是Copy的,但 (i32, String)不是
5.3 所有权与函数 #
将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。
1fn main() {
2 let s = String::from("hello"); // s 进入作用域
3 takes_ownership(s); // s 的值移动到函数里 ...
4 // ... 所以到这里不再有效
5 let x = 5; // x 进入作用域
6 makes_copy(x); // x 应该移动函数里,
7 // 但 i32 是 Copy 的,所以在后面可继续使用 x
8
9} // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走,
10 // 所以不会有特殊操作
11
12fn takes_ownership(some_string: String) { // some_string 进入作用域
13 println!("{}", some_string);
14} // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放
15
16fn makes_copy(some_integer: i32) { // some_integer 进入作用域
17 println!("{}", some_integer);
18}
5.4 返回值与作用域 #
返回值可以转移所有权:
1fn main() {
2 let s1 = gives_ownership(); // gives_ownership 将返回值
3 // 移给 s1
4 let s2 = String::from("hello"); // s2 进入作用域
5 let s3 = takes_and_gives_back(s2); // s2 被移动到
6 // takes_and_gives_back 中,
7 // 它也将返回值移给 s3
8} // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走,
9 // 所以什么也不会发生。s1 移出作用域并被丢弃
10
11fn gives_ownership() -> String { // gives_ownership 将返回值移动给
12 // 调用它的函数
13 let some_string = String::from("hello"); // some_string 进入作用域.
14 some_string // 返回 some_string 并移出给调用的函数
15}
16
17// takes_and_gives_back 将传入字符串并返回该值
18fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域
19 a_string // 返回 a_string 并移出给调用的函数
20}
变量的所有权总是遵循相同的模式:将值赋给另一个变量时移动它。当持有堆中数据值的变量离开作用域时,其值将通过drop
被清理掉,除非数据被移动为另一个变量所有
5.5 引用与借用 #
引用(reference),它允许多处代码访问同一处数据,而无需在内存中多次拷贝。
下面的代码中calculate_length
函数是以一个对象的引用作为参数而不是获取值的所有权:
1fn main() {
2 let s1 = String::from("hello");
3 let len = calculate_length(&s1);
4 println!("The length of '{}' is {}.", s1, len);
5}
6
7fn calculate_length(s: &String) -> usize { // s是对String的引用
8 s.len()
9}// 这里,s 离开了作用域。但因为它并不拥有引用值的所有权,
10 // 所以什么也不会发生
&
符号就是引用,它们允许你使用值但不获取其所有权。
&s1
语法让我们创建一个指向值s1
的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。
将获取引用作为函数参数称为借用(borrowing)。
对于引用的值默认是不允许修改的:
1fn main() {
2 let s = String::from("hello");
3 change(&s);
4}
5
6fn change(some_string: &String) {
7 some_string.push_str(", world");
8}// 编译错误: cannot borrow immutable borrowed content `*some_string` as mutable
这个时候需要借助可变引用:
1fn main() {
2 let mut s = String::from("hello");
3 change(&mut s);
4}
5
6fn change(some_string: &mut String) {
7 some_string.push_str(", world");
8}
总结引用的规则:
- 在任意给定时间,要么只能有一个可变引用,要么只能有多个不可变引用
- 引用必须总是有效的
6.结构体 #
6.1 定义结构体并创建结构体实例 #
1struct User {
2 name: String,
3 score: u32,
4 enabled: bool,
5}
6let user1 = User {
7 name: String::from("jane"),
8 score: 100,
9 enabled: true,
10};
11println!("{}-{}-{}", user1.name, user1.score, user1.enabled);
6.2 从其它结构体实例创建新实例 #
1let user2 = User{
2 name: String::from("joyce"),
3 ..user1
4};
5println!("{}-{}-{}", user2.name, user2.score, user2.enabled);
6.3 元组结构体 #
1struct Point(i32, i32);
2let p1 = Point(100, 50);
3let p2 = Point(100, 0);
4println!("{}, {}", p1.0, p1.1);
5println!("{}, {}", p2.0, p2.1);
6.4 不包含任何字段的结构体 #
1struct E{};
6.5 打印结构体调试信息 #
1#[derive(Debug)]
2struct User {
3 name: String,
4 score: u32,
5 enabled: bool,
6}
7let user1 = User {
8 name: String::from("jane"),
9 score: 100,
10 enabled: true,
11};
12println!("{:?}", user1);
13println!("{:#?}", user1);
6.6 结构体方法 #
1#[derive(Debug)]
2struct Bird {
3 name: String,
4 weight: f32,
5 color: (i16, i16, i16),
6}
7
8impl Bird {
9 fn get_name(&self) -> &str {
10 &(self.name[..])
11 }
12
13 fn get_weight(&self) -> f32 {
14 self.weight
15 }
16
17 fn get_color(&self) -> (i16, i16, i16) {
18 self.color
19 }
20}
21
22let bird = Bird{
23 name: String::from("kkk"),
24 weight: 0.32,
25 color: (254,254,254),
26};
27println!("{:#?}\n, {}, {}", bird, bird.get_name(), bird.get_weight());
定义关联函数(类似于其他语言中的静态方法):
1impl Bird {
2 fn from(name: &str) -> Bird {
3 Bird {
4 name: String::from(name),
5 weight: 0.5,
6 color: (225, 225, 225),
7 }
8 }
9}
10println!("{:#?}", Bird::from("angry"));
7.枚举和模式匹配 #
7.1 枚举 #
结构体和枚举:
1enum IpAddrKind {
2 V4,
3 V6,
4}
5struct IpAddr {
6 kind: IpAddrKind,
7 addr: String,
8}
9let loopback = IpAddr {
10 kind: IpAddrKind::V6,
11 addr: String::from("::1"),
12};
将数据直接附加到枚举的成员上, 这样无需再单独定义一个额外的结构体:
1enum IpAddr {
2 V4(String),
3 V6(String),
4}
5let loopback = IpAddr::V6(String::from("::1"));
每个枚举成员还可以处理不同类型和数量的数据:
1enum IpAddr {
2 V4(u8, u8, u8, u8),
3 V6(String),
4}
5
6let home = IpAddr::V4(127, 0, 0, 1);
7
8let loopback = IpAddr::V6(String::from("::1"));
可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体.
1enum Message {
2 Quit, // 没有关联数据
3 Move { x: i32, y: i32 }, // 关联了匿名结构体
4 Write(String), // 单独一个String
5 ChangeColor(i32, i32, i32), // 元组
6}
如果上面的代码不使用枚举则需要定义成下面这样:
1struct QuitMessage; // 类单元结构体
2struct MoveMessage {
3 x: i32,
4 y: i32,
5}
6struct WriteMessage(String); // 元组结构体
7struct ChangeColorMessage(i32, i32, i32); // 元组结构体
可以为枚举定义方法:
1impl Message {
2 fn call(&self) {
3 // 在这里定义方法体
4 }
5}
6
7let m = Message::Write(String::from("hello"));
8m.call();
7.2 match控制流运算符 #
Rust 有一个叫做 match 的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较,并根据相匹配的模式执行相应代码。模式可由字面值、变量、通配符和许多其他内容构成。值也会通过 match 的每一个模式,并且在遇到第一个 “符合” 的模式时,值会进入相关联的代码块并在执行中被使用。
1enum Message {
2 Quit, // 没有关联数据
3 Move { x: i32, y: i32 }, // 关联了匿名结构体
4 Write(String), // 单独一个String
5 ChangeColor(i32, i32, i32), // 元组
6}
7
8impl Message {
9 fn print(&self) {
10 match *self {
11 Message::Quit => println!("quit"),
12 Message::Move{x, y} => println!("move x={}, y={}", x, y),
13 Message::ChangeColor(x, y, z) => println!("change color {}, {}, {}", x, y, z),
14 // Message::Write(&s) => println!("write {}", s),
15 _ => println!("Write"),
16 }
17 }
18}
19let msg = Message::ChangeColor(100, 250, 100);
20msg.print()
1enum Coin {
2 Penny,
3 Nickel,
4 Dime,
5 Quarter,
6}
7
8fn value_in_cents(coin: Coin) -> u8 {
9 match coin {
10 Coin::Penny => {
11 println!("Lucky penny!");
12 1
13 },
14 Coin::Nickel => 5,
15 Coin::Dime => 10,
16 Coin::Quarter => 25,
17 }
18}
7.3 标准库中的Option枚举 #
Option是Rust标准库定义的一个枚举:
1enum Option<T> {
2 Some(T),
3 None,
4}
Option用于一个非常普遍的场景,即一个值要不有值要么没值。
Rust中没有空值Null的概念,需要的场景下使用Option枚举,这样更不容易出错。
因为Option
如此重要,它和它的成员Some
和None
被包含了Rust的prelude中,使用时无需显示引入Option,甚至不需要使用Option::
前缀即可直接使用Some
和None
。
1let some_number = Some(5);
2let some_string = Some("a string");
3let absent_number: Option<i32> = None;
4
5let a :i32 = 10;
6let b :Option<i32> = Some(10);
7let mut sum = 0;
8match b {
9 Some(i) => {sum = i;},
10 None => {println!("do nothing");},
11}
12sum += a;
13println!("sum = {}", sum);
7.4 if let简洁控制流 #
使用match模式匹配时必须处理枚举所有成员可能的情况,否则会编译错误。
可以使用if let
简洁控制流来处理只匹配一个模式的值而忽略其他模式的情况。
1let some_u8_value = Some(0u8);
2match some_u8_value {
3 Some(3) => println!("three"),
4 _ => (),
5}
6
7if let Some(3) = some_u8_value {
8 println!("three");
9}