语法
# 变量
// 不可变变量
let foo = 1;
// 可变变量:mut关键字为可变变量,没有则为不可变变量
let mut guess = String::new(); // :: 相当于调用静态方法
// 隐藏
foo = 2; // 会报错
let foo = 2; // 不报错,相当于重新声明一个foo变量,并将之前的隐藏
let foo = "Two"; // 新声明的变量可以是其他类型。
// 指定类型,如不指定类型则使用类型推断,rust可在编译时根据下文推到。
let x: u32 = 1;
# 常量
const MAX_POINTS = 100_0000;
- 可声明在任何作用域
- 名字命名规范全大写,下划线分割单词
- 数值可使用下划线分割增加可读性
# 数据类型 - 标量类型
Note
存储在 Stack
# 整数
类型长度
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
isize
、usize
的位数由程序运行的计算机的架构所决定,如 64 位计算机则为 64 位- 默认类型为
i32
定义(字面值)
Number Literals | Example |
---|---|
Decimal | 98_222 |
Hex | 0xff |
Octal | 0o77 |
Binary | 0b1111_0000 |
Byte(u8 only) | b'A' |
- 除了 Byte 类型(只能是 u8)以外,其他数值字面值可使用类型后缀,如
57u8
# 浮点数
- f32,32 位,单精度
- f64,64,双精度,默认类型
数值运算
let sum = 5 + 10;
let difference = 95.5 - 4.3;
let product = 4 * 30;
let guotient = 56.7 / 32.2;
let reminder = 54 % 5;
# 布尔
let t = true;
let f: bool = false;
# 字符
let x = 'z';
let y: char = "N";
# 数据类型 - 复合类型
存储在 Heap 上
# 元组 - Tuple
// 定义一个元素依次为i32、f64、u8的元组
let tup: (i32, f64, u8) = (500, 6.4 ,1);
// 读取值:解构(与ES的解构赋值类似)
let (x, y, z) = tup;
println!("{}, {}, {}", x, y, z);
// 读取值:索引读取
println!("{}, {}, {}", tup.0, tup.1, tup.2);
- 长度固定,一旦声明就无法改变
- 元素类型不必相等
# 静态数组
let months = [
"January",
"……",
"Decmber",
];
// 定义i32类型长度5的数组
let a: [i32;5] = [1,2,3,4,5];
// 另一种初始化方式:指定数组初始化值
let b = [3; 5];
// 相当于
let b = [3,3,3,3,3];
// 读取
let first = months[0];
- 数组声明后,长度不可变
# 动态数组 - Vector
todo
# 字符串 - String
- 字符串字面值:
- 程序里手写的那些字符串值。它们是不可变的
- 在编译时已知道内容,其文本直接被硬编码到最终的可执行文件里
- 速度快、高效,是因为其不可变性
- String 类型:
- 为了支持可变性,需要在 heap 上分配内存来保存编译时未知的文本内容:
- 操作系统必须在运行时请求内存,这步通过调用
String::from
来实现
// 字符串字面值
let s = "Hello";
// 可变字符串(String类型)
let mut s = String::from("Hello"); // 调用String类型下的from函数创建一个String类型
s.push_str(", World");
println!("{}",s);
# 切片
let a = [1,2,3,4,5]
let slice = &a[1..3]
# 函数
# 结构
fn fn_name(x: i32, y: i64) { // 入参必须定义参数类型
println!("the value og x is : {}", x);
}
# 表达式
fn fn2() {
let x = 5;
let y = {
let x = 1;
x + 3 // 注意这里是没有;的,有就返回`()`了
};
println!("y={}", y); // y=4
}
# 返回值
- 在
->
符合后面声明函数返回值的类型,但是不可以为返回值命名 - 函数体最后一个表达式的值为返回值。大多数函数都是默认使用最后一个表达式为返回值
- 若要提前返回,需要使用
return
关键字,并指定一个值;
fn five() -> i32 {
5 // 返回5,主要这里没有;
}
# 注释
与 Java 类似
//
/* */
# 控制流
# if
let number = 3;
if number < 5 { // 必须是布尔类型,有别于js
....
} else if number > 100 {
} else {
....
}
因为 if
是一个表达式,所以可以将它放在 let 语句中等号的右边:
fn fn3() {
let condition = true;
let number = if condition { 5 } else { 6 }; // 类似于三元表达式
println!("The value of number is: {}", number);
}
# loop
死循环,直到遇到 break
loop {
……
}
fn fn4() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2;
}
};
println!("The result is {}", result);
}
# while
let mut numer = 3;
while numer != 0 {
println!("{}", numer);
numer = numer - 1;
}
println!("LIFTOFF!!!")
# for
# 遍历数组
let a = [1, 2, 3, 4, 5];
for element in a {
println!("{}", element);
}
# 数值遍历
for number in (1..4) {
// 遍历从1-3的值,不包括4
}
for number in (1..4).rev {
// 遍历从3-1的值,不包括4
}
# 所有权
# Stack 和 Heap
- Stack,栈,后进先出(LIFO)
- 存储在 Stack 上的数据必须拥有已知的固定大小,反之则必须存储在 heap 上
# 数据存储
- heap
- 性能较差一些
- 把数据插入 heap 时,会请求一定数量的空间
- 操作系统在 heap 里找到一块足够大的空间,把它标记为在用,并返回一个指针,也就是这个空间的地址
- 这个过程叫做在 heap 上进行分配
- stack
- 存储已知大小的数据
- 把值压入 stack 上不叫分配
- 指针是已知固定大小的,可以存放在 stack
- 把数据压到 stack 上要比在 heap 上分配快得多:因为操作系统不需要寻找用来存储新数据的空间,那个位置永远都在 stack 的顶端。
- 在 heap 上分配空间需要做更多的工作:操作系统首先需要找到一个足够大的空间来存放数据,然后要做好记录方便下次分配。
# 数据访问
- stack 可直接读取
- heap 的数据需要先在 stack 找到指针才能找到对应的 heap,因此会慢一些
# 所有权存在的原因
- 追踪代码的哪些部分正在使用 heap 的哪些数据
- 最小化 heap 上的重复数据量
- 清理 heap 上未使用的数据以避免空间不足
# 所有权规则
- 每个值都有一个变量,这个变量是该值的所有者
- 每个值同时只能有一个所有者
- 当所有者超出作用域(scope)时,该值将被删除
# 变量作用域
fn main(){
// s 不可用
let s = "hello"; // s 可用
// 可以对 s 进行相关操作
} // s 作用域到此结束,s 不再可用
# 内存和分配
以 String 类型为例,当用完 String 之后,需要使用某种方式将内存返回给操作系统,这步:
- 在拥有 GC 的语言中,GC 会跟踪并清理不再使用的内存,
- 在没有 GC 的语言中,就需要我们去识别内存何时不再使用,并调用代码手动将它返回:
- 如果忘了,那就浪费内存
- 如果提前做了,变量就会非法
- 如果做了两次,也是 Bug。必须一次分配对应一次释放
- Rust 采用不同的方式:对于某个值来说,当拥有它的变量走出作用范围时,会自动调用
drop
函数,内存会立即自动的交换给操作系统。
# 变量和数据交互的方式
# 移动 - 复杂类型(类浅拷贝)
有别于其他语言的浅拷贝,Rust 采用一种名为移动(Move)的方式,如下文,变量 s1
赋值到 s2
后,s1
就失效了。
let s1 = String::from("Hello");
let s2 = s1;
println!("{}", s1); // 这里会报错,s1已经移动到s2
- 为了保证内存安全:
- Rust 没有尝试复制被分配的内存
- Rust 让 s1 失效:当 s1 离开作用域的时候,Rust 不需要释放任何东西
- 隐含的一个设计原则:Rust 不会自动创建数据的深拷贝
# 复制 - 标量类型
let x = 5;
let y = x;
// 整数时已知且固定大小的简单的值,这两个5被压到了stack中
println!("{}, {}", x, y); // 不会报错
由于标量类型只作用在 Stack,因此 “移动” 并不会带来明显性能提升,因此采用了复制的方式处理。
Note
这个特性跟 Java 类似,基本类型传递值 a1 != b1
,对象类型传递的是对象地址 a2 == b2
,但 Rust 在传递对象地址时,会导致原变量失效。
# 克隆(深拷贝)
- 即深拷贝
- 只用实现了
Copy
trait 的类型才可用 - 如果一个类型或该类型的一部分实现了 Drop trait,那么 Rust 不允许它再实现 Copy trait
let s1 = String::from("Hello");
let s2 = s1.clone();
println!("{}, {}", s1, s2); // 正常输出
Copy trait
- 任何简单标量的组合类型都可以是 Copy 的
- 任何需要分配内存或某种资源的都不是 Copy 的
- 一些拥有 Copy trait 的类型:
- 所有的整数类型:如 u32
- bool
- char
- 所有的浮点类型
- Tuple,如果其所有的字段都是 Copy 的
(i32,i32)
是(i32,String)
不是
# 函数
在语义上,将值传递给函数和把值赋给变量是类似的,同样会发生移动或复制
# 传递
fn main(){
let s = String::from("Hello, world!");
take_ownership(s);
// println!("{}", s); // 这一行会报错,因为 `s` 的所有权已经被转移到 `take_ownership` 函数中。
let x = 5;
make_copy(x);
println!("{}", x); // 这一行不会报错,因为 `x` 是一个整数,它的值被复制到 `make_copy` 函数中,而不是转移所有权。
}
fn take_ownership(s: String) {
println!("{}", s);
} // `s` 在这里被丢弃,内存被释放。
fn make_copy(x: i32) {
println!("{}", x);
} // `x` 在这里被丢弃,但它的值已经被复制,所以不会影响原来的 `x`。
# 返回值
一个变量的所有权总是遵循同样的模式:
- 把一个值赋给其他变量时就会发生移动
- 当一个包含 heap 数据的变量离开作用域时,它的值就会被 drop 函数清理,除非数据的所有权移动到另一个变量上
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 // 这个函数接收一个字符串并将其返回给调用者
}
# 引用与借用
如何让函数使用某个值,但不获得其所有权?
&
符号表示引用:允许你引用某个值而不取得其所有权- 把引用作为函数参数的行为叫做借用
- 默认情况,借用变量不可修改
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()
}
可修改的借用
fn main(){
let s1 = String::from("hello");
let len = calculate_length(&mut s1);
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &mut String) -> usize {
s.push_str(", world!");
s.len()
}
可变引用
在特定作用域内,对某一块数据,只能有一个可变的引用,因此可在编译时防止数据竞争。
以下 3 种行为下会发生数据竞争:
- 两个或多个指针同事访问同一个数据
- 至少有一个指针用于写入数据
- 没有任何机制来同步对数据的访问
不可同时存在可变引用和不可变引用
fn main(){
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2); // no problem
let r3 = &mut s; // BIG PROBLEM
println!("{}", r3); // BIG PROBLEM
}
悬空引用 Dangling References
- 悬空指针(Dangling Pointer):一个指针引用了内存中的某个地址,而这块内存可能已经释放并分配给其他人使用了。
- 在 Rust 里,编译器可保证引用永远都不是悬空引用:编译器将保证在引用离开作用域之前数据不会离开作用域。
# 字符串切片
- 类型为
&str
- 字符串字面值本质上是切片,是一个指向二进制程序特定位置的切片
- 由于
&str
是不可变引用,所以字符串字面值也是不可变的 - 形式:
[开始索引..结束索引]
,边界值为[开始索引,结束索引)
fn main(){
let mut s = String::from("Hello World");
let hello = &s[0..5]; // or let hello = &s[..5];
let world = &s[6..11]; // or let world = &s[6..];
println!("{} {}", hello, world);
}
fn main(){
let mut s = String::from("Hello World");
let wordIndex = first_word(&s);
// s.clear(); // s不能再可变
println!("The first word is at index: {}", wordIndex);
}
fn first_word(s: &String) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i]; // 返回到第一个空格的字符串切片
}
}
&s[..] // 返回整个字符串切片
}
作为参数传递
- 采用
&str
作为参数类型,可以同时接收String
和&str
类型的参数 - 使用字符串切片 (
&str
) 代替字符串引用 (&String
) 使 API 更加通用,且不会损失任何功能
fn main(){
// This is a string slice
let mut my_string = String::from("Hello world");
let wordIndex = first_word(&my_string[..]);
// This is a string literal
let my_string_literal = "Hello world";
let wordIndexLiteral = first_word(my_string_literal);
}
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[..]
}
# 结构体 - struct
# 含义
- 自定义的数据类型
- 为相关联的值命名,打包 =》 有意义的组合
# 定义
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool, // 最后一个属性结尾也有逗号
}
# 实例化
// email = String::from("[email protected]");
let user1 = User {
// 属性顺序无需跟定义的顺序相同;必须声明所有属性
email: String::from("[email protected]")
// email, // 与es6类似,当存在一个字段同名变量时,可简写
username: String::from("Michael")
active: true,
sign_in_count: 556,
}
# 读写
- 一旦 struct 实例是可变的,那么实例中所有的字段都是可变的。【这里有别于 Java 的 Class】
let mut user1 = User { ... }
user.email = String::from("[email protected]")
# 更新(实例局部克隆)
let user2 = User {
username: String::from("user2"),
email: String::from("[email protected]"),
active: user1.active,
sign_in_count: user1.sign_in_count,
};
优化写法:
let user2 = User {
username: String::from("user2"),
email: String::from("[email protected]"),
..user1 // 剩下的字段由user1提供
};
# Tuple struct
相当于给某个格式的元组 (tuple) 起名
struct Color(i32,i32,i32);
struct Point(i32,i32,i32);
let black = Color(0,0,0);
let origin = Point(0,0,0);
# Unit-Like Struct(空结构体)
- 没有定义任何字段的 struct,叫做 Unit-Like struct
- 适用于需要再某个类型上实现某个 trait,但又不想存储字段的数据
# 所有权
- struct 里定义的字段为非引用类型时,实例拥有其所有权,只要 struct 实例有效,字段数据就有效。
- struct 也可以定义引用类型,但需要使用生命周期(见下文);
- 生命周期可以保证实例是有效的,引用也是有效的。
- 如果没有使用生命周期,就会报错。
# 打印 struct
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
println!("{}", rect); // 需要实现std::fmt::Display,自定义struct没有实现
println!("{:?}", rect); // 需要实现Debug,或在结构体上添加`#[derive(Debug)]`
# 实例方法
# 定义
- 这里所说的
方法
,有别于函数
,用 Java 的概念来说,就是实例方法。 - 与
函数
相同之处:fn 关键字、名称、参数、返回值 - 与
函数
不同之处:- 方法是在 struct(或 enum、trait 对象)的上下文中定义
- 第一个参数是
self
,表示方法被调用的 struct 实例,可获得其所有权(去掉&
)或可变借用(mut
)
- 同一个 struct 的
impl
块可以有多个
impl Rectangle {
fn area(&self) -> u32 { // 入参使用引用,也可以不加&获得所有权,无限制
self.width * self.height
}
}
# 调用
- Rust 会自动引用或解引用,即无需
*obj
- 调用方式时,Rust 会根据情况自动添加
&
、&mut
、*
,以便 object 可以匹配方法的签名
// 下面两行代码等效
p1.distance(&p2);
(&p1).distance(&p2);
# 参数
可添加出 &self
以外的入参
impl Rectangle {
...
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
# 关联函数(静态方法)
- 不把
self
作为第一个参数的函数,叫关联函数(不是方法),可理解为静态方法,不是实例方法。 - 多用于作为构造器
- 例如:
String::from()
# 定义
impl Rectangle {
...
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
# 调用
- 使用
::
符号调用,::
还可用于模块创建的命名空间
let s = Rectangle::square(20);
# 枚举
# 定义
enum IpAddrKind {
V4,
V6,
}
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
enum IpAddrKind {
V4(u8, u8, u8, u8),
V6(String),
}
let four = IpAddrKind::V4(127, 0, 0, 1);
let six = IpAddrKind::V6(String::from("::1"));
# 调用
fn route(ip_kind: IpAddrKind) {
match ip_kind {
IpAddrKind::V4 => println!("IPv4"),
IpAddrKind::V6 => println!("IPv6"),
}
}
route(IpAddrKind::V4);
# 定义方法
与 struct 类似,略
# Option 枚举
- Rust 没有 Null,Rust 的作者认为空指针成本是巨大的,更优雅的方法是使用
Option<T>
来处理,值得一提的是,Java 也在 Java8 引入了这一特性。 - 它包含在 Prelude(预倒入模块中),可直接使用
enum Option<T>{
Some(T),
None,
}
// 可直接使用`Some()`无需前缀`Option::`
let some_number = Some(5);
let some_string = Some("a string");
// 由于None没有入参,无法推断类型,需要显示声明
let absent_number: Option<i32> = None;
上次更新: 2025/05/10, 02:23:42
← 入门 常见库(Crate)→