Rust 入门笔记【一】
不可变变量和常量的区别
用 let定义的变量默认就是不可变的.
let x = 1;
如果需要修改可以使用 mut关键字.
用 const定义的是常量,且必须显示的给出变量类型.
const x : u32 = 1;
let的值可以在运行时由输入或函数返回值确定.
const的值必须在编译时确定.
同时 let支持复用,本质上是新建了一个变量,并且支持更换类型.
let spaces = "123";
let spaces = spaces.len();
元组 tuple
和C++中的 std::tuple类似,可以将多个类型不同的变量绑定进一个变量中
let tup : (i32, i64, f64) = (123, 123_456_789_123, 0.123)
访问有两种方法,第一种是用模式匹配来结构,类似于C++中结构化绑定
let (x, y, z) = tup;
第二种是用 .加索引来访问
tup.0;
tup.1;
tup.2;
数组 array
数组要求元素类型必须相同
let x = [4,5,3,6,2,5,1];
也可以显示的指定类型和长度
let dx : [i64; 4] = [0, 0, 1, -1];
也可以指定初始值和长度
let a = [3; 5]; // 等价于 [3, 3, 3, 3, 3,]
访问数组元素可以用索引
a[2];
rust 不仅可以在编译时检查下标越界,在运行时也会检查下标越界,一旦发生越界就会终止执行,有些类似C++中 std::array::at。
命名规范 snake case
rust 采用 snake case 命名规范
- 单词分隔:将多个单词间的空格替换为单个下划线 (
_),如first_name、user_id等。 - 全部小写:所有字母均使用小写形式,不使用大写或混合大小写。
- 常量变体:在常量命名中,经常将字母全部大写并使用下划线分隔,称为“SCREAMING_SNAKE_CASE”(或称宏常量风格),如
MAX_BUFFER_SIZE。
函数
rust 的函数定义顺序不像C++一样有强制的要求,不需要先定义再调用,同时也不能像C++一样把定义和实现分开。和C++一样,main是程序的入口
fn main(){
f();
}
fn f(){
println!("f");
}
函数的形参,必须要显示的指定形参的类型,返回值有些类似C++的尾返回类型。关于返回值有两种写法
- 显式的用
return返回值。 - 隐式返回最后一个表达式的值作为返回值,且该表达式的末尾不能以分号
;结尾
fn f(a : i32, b : i32) -> i32 {
a + b
}
fn g(a : i32, b : i32) -> i32 {
return a + b;
}
如果一个函数没有指定返回值,或者无返回值,则函数的返回值是 (),也即空元组.同时函数也支持元组作为返回值.
fn f(x : i32, y : i32) -> (i32, i32) {
return (x - y, x + y );
}
if
rust 没有大多数时候隐式类型转换,因此if 接受表达式的返回值必须是 bool类型的. if 的格式有点类似与 python 和 C++ 的结合.
if number < 10 {
} else if number > 10 {
} else {
}
if 也可认为有返回值的, 我们可以使用隐式返回值来实现一些简单的操作比如
let x = if number % 2 == 0 {0} else {1};
注意,如果要用到返回值的情况请确保返回值类型相同,不然无法正常通过编译.
循环
while
语法依旧是类似 python 和 C++ 的结合
while number != 0 {
number -= 1;
}
同样的,循环也有 break, continue这两个关键字.
loop
loop 可以认为是没有条件的 while 循环,或者说条件是 true 的循环.
loop {
println!("again");
}
for
for 循环和python类似主要是用来遍历数组
let a = [4,5,3,6,2,5,1];
for i in a {
println!("i");
}
还有一种方法是类似python一样生成一个序列来遍历
for i in (1..5) {
println!("i");
}
这里生成了一个1到5左闭右开的区间.
所有权
首先要明白两个概念,栈内存和堆内存
| 特性 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 分配方法 | 自动:函数调用时由编译器“入栈”局部变量 | 显式:通过分配器在运行时申请,如 Box::new、Vec::new等 |
| 分配速度 | 非常快:只需移动栈指针 | 较慢:需搜索可用内存块并维护分配元信息 |
| 访问速度 | 极快:内存连续、缓存友好 | 较慢:通过指针间接访问,可能发生随机内存跳转 |
| 大小限制 | 必须在编译时已知且固定(编译器需确定偏移量) | 动态:可申请任意大小块并在运行时调整 |
| 生命周期 | 自动:函数/作用域结束时自动“出栈” | 显式:由所有权系统管理,超出作用域时调用 Drop释放 |
| 内存碎片 | 不会产生碎片 | 可能产生“空洞”(holes),取决于分配/释放顺序和分配器策略 |
我们再来看所有权的规则
- Rust 中的每一个值都有一个 所有者(owner)。
- 值在任一时刻有且只有一个所有者。
- 当所有者(变量)离开作用域,这个值将被丢弃。
下面来简单说一下什么是作用域
{
let x = 123;
}
在这个括号内,就是x的作用域,但程序执行完括号内所有代码后,对于 x来说生命周期就已经结束也就会释放内存.因为在编译时我们就已知 x是 u32,因此这个内存就是利用的栈内存.
下面再来介绍一下 String
let s = String::from("hello");
s.push_str(", world!");
println!("{s}");
这就是一个简单的 String例子,很明显s占用的内存空间在运行期间是变换的,因此s使用的就是堆内存.
好的我们来回忆一下如何释放内存.
C++ 的对象是通过析构函数来释放对象的. Java 是通过GC来识别和清理不再被使用的对象的.
rust 使用 Drop::drop函数来释放对象,并且只有编译器在值离开作用域时自动插入对该方法的调用,并且不允许直接调用 Drop::drop. 这看起来和C++ 的 RAII 机制很接近.但同时,rust 允许使用 std::mem::drop来提前释放对象,这样不就又和C++一样会造成二次析构了吗?
实际上不是的,因为当调用 std::mem::drop时,会讲对象的所有权移动到 drop中,我们来看 std::mem::drop的定义
pub fn drop<T>(_x: T) { }
虽然 drop 函数体空无一物,但编译器会在函数返回时插入对该类型实现的 Drop::drop 方法的调用,完成析构逻辑.
而如果使用C++的析构函数,则会出现这种情况.
void f(){
vector<int> a;
a.~vector<int>();
a.~vector<int>();
}
这里我们的代码是可以正常编译并运行的,但是第二个析构函数实际上是未定义行为.
接下来我们再来看
let x = 3;
let y = x;
这里x和y实际上都是 u32也就都在栈内存中,这里实际上是发生了深拷贝,也就是栈内存中有两个 u32且值都是 3
再看这个例子
let s1 = String::from("aaa");
let s2 = s1;
看起来和上面的例子一样,会发生深拷贝.但是我们要知道 String的底层是什么.一个 String包含三部分 (ptr, len, capacity),分别是指针,长度,容量这三部分都可以在编译器知道长度,因此是存在栈中的.其中 ptr指向了堆中一段长度为 capacity的内存空间,用来记录字符串的真实值.当我们执行 s2 = s1时确实会对这三部分进行深拷贝,但这样会造成一个问题就是堆内存中的一段内存被两个 ptr指向,这样就会导致上面提到的两次析构的情况.但是如果对堆上的内存也进行深拷贝的话就会对运行时的性能产生极大的影响.因此对于这里的 s2 = s1 实际上是浅拷贝,更加形象的说,实际上是移动操作.当发生移动操作后,s1就是已经失去了所有权,也就无法在进行 drop.
如果真的也要对堆上的也进行深拷贝,该如何实现?可以使用 clone().
let s2 = s1.clone();

浙公网安备 33010602011771号