Hello World!
一般将函数体的左花括号与函数声明置于同一行并以空格分隔.
可以用 rustfmt
格式化代码.
Rust 的缩进风格是 4 个空格, 而不是一个 Tab.
!
, 表明调用的是一个宏, 而不是一个函数, 如 println!
.
编译
Hello Cargo
Cargo 是 Rust 的构建系统和包管理器.
创建项目
创建的项目的目录结构为:
1 2 3 4 5
| . └── hello_cargo ├── Cargo.toml └── src └── main.rs
|
若没有在 Git 仓库中, 其还会初始化一个 git 仓库以及一个 .gitignore
文件.
Cargo.toml
文件是 Cargo 的配置文件, 其内容如:
1 2 3 4 5 6
| [package] name = "hello_cargo" version = "0.1.0" edition = "2021"
[dependencies]
|
构建
可执行文件会生成在 target/debug/
目录下.
其会在项目根目录创建 Cargo.lock
文件, 其会记录项目实际使用的以来以及其版本.
构建并运行
检查代码但不运行
项目发布
其会开启优化编译, 并在 target/release
下生成可执行文件.
编写 “猜猜看” 游戏
导入包
导入标准 io
库:
默认情况下. Rust 会自动引入部分标准库中的模块.
函数声明
创建变量
1
| let mut guess = String::new();
|
Rust 中, 变量默认不可变, 需加上 mut
(mutable) 才可变.
这里将 guess
绑定到 String::new()
的返回值 (UTF-8 编码的可增长文本块).
调用方法
用 .
号:
1 2
| io::stdin().read_line(&mut guess) .expect("Failed to read line");
|
引用
用 &
Result
类型
这里 read_line
的返回值是一个 Result
类型, 其本质是 “枚举”, 即 enums.
Result
类型的成员有:
crate
Rust 称外部的模块为 crate.
引入一个模块, 需要先修改 Cargo.toml
文件:
1 2 3
| [dependencies]
rand = "0.8.3"
|
然后在 cargo build
时, 会自动拉取.
更新 crate
其会忽略 Cargo.lock
文件. 并将新的版本信息写入其中.
cmp
方法
任何可以用于比较的值, 都可以调用 cmp
方法, 其获取一个用于比较的值的引用:
1
| guess.cmp(&another_number);
|
其比较 guess
和 another_number
. 其会返回 Ordering
类型的成员.
match
表达式
由:
组成.
传递给 match
的值与模式向匹配, 则运行对应的代码:
1 2 3 4 5
| match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => println!("You win!"), }
|
Rust 有一个静态强类型系统, 同时也有类型推断.
变量声明时指定类型
trim()
和 parse()
方法
字符串的 trim()
方法用于去除字符串开头和结尾的空白字符.
parse()
方法用于将字符串解析为数字.
循环
用 loop
关键字:
1 2 3 4 5 6 7 8 9 10 11 12
| loop { println!("Please input your guess."); match guess.cmp(&secret_number) { Ordering::Less => println!("Too small!"), Ordering::Greater => println!("Too big!"), Ordering::Equal => { println!("You win!"), break; } } }
|
同样可以用 break
和 continue
处理.
常量
和普通不可变的变量类似, 但其值在编译期确定.
不能对常量使用 mut
.
声明为:
1
| const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
|
命名约定为: 单词之间使用全大写加下划线.
隐藏 (Shadowing)
发生于重复声明时, 如:
1 2 3 4 5 6 7 8 9 10 11 12
| fn main() { let x = 5;
let x = x + 1;
{ let x = x * 2; println!("The value of x in the inner scope is: {}", x); }
println!("The value of x is: {}", x); }
|
数据类型
两类数据类型子集:
Rust 编译器一般能推断出变量的类型, 但最好是加上类型标注:
字符类型
Rust 中用:
- 单引号声明
char
(Unicode 字符, 4 个字节)
- 双引号声明字符串字面量
复合类型
- 元组 (tuple), 元素可类型不同
- 数组 (array), 元素类型相同
元组
元组声明如:
1 2 3
| fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); }
|
可用 .
加上索引来访问:
1 2 3 4 5 6
| fn main() { let tup: (i32, f64, u8) = (500, 6.4, 1); let first = tup.0; let second = tup.1; let third = tup.2; }
|
没有值的元组 ()
是一种特殊的类型 – 单元类型(unit type), 其值被称为单元值 (unit value).
数组
数组声明如:
1 2 3
| fn main() { let a = [1, 2, 3, 4, 5]; }
|
其长度固定. (vector 可变)
声明中包含类型和长度:
1
| let a: [i32; 5] = [1, 2, 3, 4, 5];
|
包含初始值和长度:
访问如:
函数
main
函数是 Rust 程序的入口点.
Rust 中的函数和变量名都遵循 snake case
规范, 即所有字母都是小写且用下划线分隔.
传递参数:
1 2 3
| fn test_paras(num: i32) { println!("The value of the num is {}", num); }
|
语句和表达式
Rust 是基于表达式的语言.
区分语句和表达式:
- 语句, 执行操作但不返回
- 表达式, 计算并产生一个值
常见的, 函数调用是表达式, 宏调用也是表达式. 大括号创建的新的块作用域也是表达式.
如:
1 2 3 4 5 6 7 8
| fn main() { let x = 5;
let y = { let x = 3; x + 1 }; }
|
注意这里 x+1
之后没有分号 ;
, 加上则变成语句了.
函数返回
用 ->
指定返回值的类型.
函数返回值为函数体中最后一个表达式的值, 也可以用 return
提前返回.
如:
控制流
if
表达式
如:
1 2 3 4 5 6 7 8 9 10 11 12
| fn main() { let number = 3;
if number < 5 { println!("condition was true"); } else if number == 6 { println!("Hello") } else { println!("condition was false"); } }
|
注意这里可以不加括号. 且条件结果必须为 bool
类型.
在 let
语句中使用 if
由于 if
是表达式, 因此可以有:
1 2 3 4 5 6 7 8 9 10
| fn main() { let condition = true; let number = if condition { 5 } else { 6 };
println!("The value of number is: {}", number); }
|
但需要注意, 每个分支的返回值需要为同种类型.
循环
loop
循环
可以用 break
关键字返回值:
1 2 3 4 5 6 7 8 9 10 11 12
| fn main() { let mut counter = 0;
let result = loop { counter += 1;
if counter == 10 { break counter * 2; } }; println!("The result is {}", result); }
|
while
循环
1 2 3 4 5 6 7 8 9 10 11
| fn main() { let mut number = 3;
while number != 0 { println!("{}!", number);
number = number - 1; }
println!("LIFTOFF!!!"); }
|
for
循环
1 2 3 4 5 6 7
| fn main() { let a = [10, 20, 30, 40, 50];
for element in a.iter() { println!("the value is: {}", element); } }
|
所有权 (ownership)
所有权的存在让 Rust 无需垃圾回收即可保障内存安全.
Rust 的编译器在编译时会根据一系列的规则进行检查, 在运行中, 所有权系统的任何功能都不会减慢程序.
什么是所有权
栈 (stack) 与堆 (Heap)
栈中的所有数据都必须占用已知且固定的大小, 大小未知或者大小可能变化的数据, 要改为存储在堆上.
在堆上分配内存的过程为: 请求一定大小的空间, 内存分配器在堆的某处找到一块足够大的空位, 将其标记为已使用, 并返回一个表示该位置地址的指针.
入栈比在堆上分配内存要快, 因为入栈时分配器无需为存储新数据去搜索内存空间, 其位置总是在栈顶.
访问堆上的数据比访问栈上的数据慢.
而当代码调用函数时, 传递给函数的值和函数的局部变量都是被压入栈中, 当函数结束时, 这些值被移出栈.
所有权的存在就是为了管理堆数据:
- 追踪哪部分代码正在使用堆上的哪些数据
- 减少堆上的重复数据的数量
- 清理堆上不再使用的数据
所有权规则
- Rust 中的每一个值都有一个被称为其 所有者 (owner) 的变量
- 值在任意时刻都有且只有一个所有者
- 当所有者离开作用域时, 这个值将被丢弃
String
类型
字符串字面值的一个不方便之处在于不可变.
String
是 Rust 的第二个字符串类型, 其可以管理被分配到堆上的数据.
可以用 from
函数基于字符串字面值创建 String
:
1
| let mut s = String::from("hello");
|
此时可以修改 s
字符串:
1 2 3
| s.push_str(", world");
println!("{}", s);
|
内存与分配
字符串字面值, 由于在编译时就知道其内容, 所以其会被直接硬编码进最终的可执行文件.
String
类型用 String::from
请求内存. 而在拥有这个内存的变量离开作用域时自动释放. (Rust 会自动调用 drop
函数)
移动
对于 Rust 而言, 以下代码的行为为:
1 2
| let s1 = String::From("hello"); let s2 = s1;
|
s1
会被释放.
这种操作被称为 移动
克隆
1 2
| let s1 = String::from("hello"); let s2 = s1.clone();
|
这里的 clone()
是一个通用函数.
此时数据被复制, s1
不会被释放.
拷贝
此时 x
不会被释放.
原因为, 像整型这样的在编译时一直大小的类型被存储在栈上, 所以其拷贝是很快速的. 因此设计为可以直接拷贝.
究其根本在于: Rust 有一个叫做 Copy
trait 的特殊注解, 可以用在类似整型这样的存储在栈上的类型上.
如果一个类型实现了 Copy
trait, 那么一个旧的变量在将其赋值给其他变量后仍然可用.
Rust 不允许自身或其任何部分实现了 Drop
trait 的类型使用 Copy
trait.
一个通用的规则:
- 任何一组简单标量值的组合都可以实现
Copy
- 任何不需要分配内存或某种形式资源的类型都可以实现
Copy
所有权与函数
如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| fn main() { let s = String::from("hello");
takes_ownership(s);
let x = 5; makes_copy(x); }
fn takes_ownership(some_string: String) { println!("{}", some_string); }
fn makes_copy(some_integer: i32) { println!("{}", some_integer); }
|
也就是说: 所有权在传递给函数时会转移.
返回值与所有权
返回值也可以转移所有权:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| fn main() { let s1 = gives_ownership(); let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2); }
fn gives_ownership() -> String { let some_string = String::from("hello"); some_string }
fn takes_and_gives_back(a_string: String) -> String { a_string }
|
引用与借用
在使用引用时, 允许使用值但不获取其所有权:
1 2 3 4 5 6 7 8 9 10 11
| fn main() { let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len); }
fn calculate_length(s: &String) -> usize { s.len() }
|
&
用于取引用. *
用于解引用.
Rust 将创建一个引用的行为称为 借用.
可变引用
1 2 3 4 5 6 7 8 9
| fn main() { let mut s = String::from("hello");
change(&mut s); }
fn change(some_string: &mut String) { some_string.push_str(", world"); }
|
一个限制, 同一时间只能有一个对某一特定数据的可变引用:
1 2 3 4 5 6
| let mut s = String::from("hello");
let r1 = &mut s; let r2 = &mut s;
println!("{}, {}", r1, r2);
|
会报错.
可以:
1 2 3 4 5 6 7
| let mut s = String::from("hello");
{ let r1 = &mut s; }
let r2 = &mut s;
|
Slice 类型
slice
类型没有所有权. 其为 String
中一部分值的引用.
如:
1 2 3 4 5 6 7
| let s = String::from("Hello World");
let hello = &s[0..5]; let world = &s[6..11];
let hello2 = &s[..5]; let world2 = &s[6..];
|
字符串字面值就是 slice
这里 s
的类型是 &str
, 是一个不可变引用.
其他类型的 slice
引用数组的一部分:
1 2 3
| let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
|
使用结构体组织相关联的数据
定义并实例化结构体
结构体和元组类似, 每一部分都可以是不同类型. 但结构体需要命名各部分数据以便清楚的表明其值的意义.
如:
1 2 3 4 5 6
| struct User { username: String, email: String, sign_in_count: u64, active: bool, }
|
每一部分称为 字段 (field).
创建实例:
1 2 3 4 5 6
| let user1 = User { email: String::from("someone@example.com"), username: String::from("someusername123"), active: true, sign_in_count: 1, }
|
获取值:
1
| println!("{}", user1.email);
|
注意 Rust 不允许仅仅是 struct 中的某一个字段可变, 而是全可变.
简写写法
如:
1 2 3 4 5 6 7 8
| fn build_user(email: String, username: String) -> User { User { email: email, username:username, active: true, sign_in_count: 1, } }
|
可以简写为:
1 2 3 4 5 6 7 8
| fn build_user(email: String, username: String) -> User { User { email, username, active: true, sign_in_count: 1, } }
|
(需要变量名和字段名相同才行).
结构体更新语法
用旧实例中的大部分值但改变部分值来创建一个新的结构体实例.
1 2 3 4 5 6
| let user2 = User { active: user1.active, username: user1.username, email: String::from("another@example.com"), sign_in_count: user1.sign_in_count, };
|
等价于:
1 2 3 4
| let user2 = User { email: String::from("another@example.com"), ..user1 };
|
注意这里移动了数据, 导致 user1
无效了.
元组结构体
没有具体的字段名, 只有字段的类型:
1 2 3 4 5
| struct Color(i32, i32, i32); struct Point(i32, i32, i32);
let black = Color(0, 0, 0); let origin = Point(0, 0, 0);
|
类单元结构体
没有任何字段:
1 2 3
| struct AlwaysEqual;
let subject = AlwaysEqual;
|
通过派生 trait 增加实用功能
为结构体添加 Debug
trait, 可以用于打印调试信息:
1 2 3 4 5 6 7 8 9 10
| #[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50 };
println!("rect1 is {:?}", rect1); }
|
{:?}
的含义为使用 Debug
的输出格式. 也可以用 {:#?}
.
也可以用 dbg!
宏, 其会输出文件和行号, 以及表达式的结果, 并返回该值的所有权. 其会打印至 stderr
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| #[derive(Debug)] struct Rectangle { width: u32, height: u32, }
fn main() { let scale = 2; let rect1 = Rectangle { width: dbg!(30 * scale), height: 50, };
dbg!(&rect1); }
|
方法语法
方法和函数不同, 其定义在结构体上下文中.
如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| #[derive(Debug)] struct Rectangle { width: u32, height: u32, }
impl Rectangle { fn area(&self) -> u32 { self.width * self.height } }
fn main() { let rect1 = Rectangle { width: 30, height: 50 };
println!( "The area of the rectangle is {} square pixels.", rect1.area() ) }
|
impl
(implementation) 块, 表明其中所有内容都将与 Rectangle
类型相关联. self
指调用者本身.
这里的 &self
是 self: &Self
的缩写.
在 impl
块中, Self
类型就是其绑定类型的别名.
方法的第一个参数必须有一个名为 self
的 Self
类型的参数.
若需要改动调用者, 则传入 &mut self
可以定义一个与结构中字段名称相同的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| impl Rectangle { fn width(&self) -> bool { self.width > 0 } } fn main() { let rect1 = Rectangle { width: 30, height: 50, };
if rect1.width() { println!("The rectangle has a nonzero width; it is {}", rect1.width); } }
|
自动引用和解引用
如使用 object.something()
调用方法时, Rust 会自动为 object
添加 &
, &mut
或 *
以便使 object
与方法签名匹配.
以下代码等价:
1 2
| p1.distance(&p2); (&p1).distance(&p2);
|
关联函数
所有在 impl
块中定义的函数都被称为 关联函数 (associated function).
可以定义不以 self
为第一参数的关联函数 (此时就不是方法).
如用作一个构造函数:
1 2 3 4 5
| impl Rectangle { fn square(size: u32) -> Rectangle { Rectangle { width: size, height: size } } }
|
使用结构体名以及 ::
即可调用关联函数. 如 let sq = Rectangle::square(3);
(这个函数位于 Rectangle
的命名空间中)
多个 impl
块
每个结构体都允许拥有多个 impl
块:
1 2 3 4 5 6 7 8 9 10 11
| impl Rectangle { fn area(&self) -> u32 { self.width * self.height } }
impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } }
|
枚举和模式匹配