本文介绍Rust的标识符、关键字、操作符、常量、变量、指针等基础概念。
安装
# install, auto: echo "source $HOME/.cargo/env" >> ~/.bash_profile
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# update
rustup update
# uninstall
rustup self uninstall
$ rustc --version
rustc 1.62.0 (a8314ef7d 2022-06-27)
格式:rustc <version> (<commit hash> YYYY-mm-DD)
$ rustup doc
开发工具
vscode
+ rust-analyzer
/Rust Syntax
插件Clion
+ Rust 插件
Hello World
fn main() {
println!("Hello, world!");
}
说明:
$ rustc hello_world.rs
$ ls
$ ./hello_world
Hello, world!
约定
- Rust 的函数名、变量名、文件名一般采用
snake case
命名规范:所有字母均小写,单词使用下划线 _
分隔 - Rust 的代码包称为
crate
,可以在 https://crates.io/ 查询,分类:
基本元素
关键字
as
类型转化,let x: usize = 10; let y = x as i32;
break
enum
声明枚举类型let
声明变量match
控制流运算符fn
声明函数use
导入路径到当前工作空间as
为导入的路径指定本地别名loop
while
for
struct
定义结构体super
用来访问父模块路径中的内容,类似于文件系统的 ..
type
类型别名return
返回值&
取引用,如:&String
pub
公有trait
定义trait,类似于其他语言的接口move
所有权移交unsafe
不安全代码块extern
调动外部代码
区别:
let
vs const
let
初始化时不确定,且声明与赋值可以分开,初始化无类型推导,可以加类型注释const
必须用来声明常量,初始化后不可修改
shadow
(隐藏,后面有介绍) vs let
- let 声明的非
mut
同名新变量,是不可变变量,重新赋值会报编译错误 - let 声明的同名新变量(shadowing)可以改变类型
常量
Rust 使用 const
声明常量(constant
),规则如下:
- 常量使用
const
声明,不需要加mut
(区别与不可变变量),常量声明后不可变 - 常量声明必须标注类型
- 常量可以在任意作用域声明,如全局
- 常量使用全大写,多个单词间使用
_
分隔,示例如下:
const MAX_CPU: u32 = 10_000; // 值为1万,下划线增强可读
常量 vs 不可变静态变量
- 静态变量,声明:
static [mut] SCRESMING_SNAKE_CASE: &str = "hello world!";
- 有固定的的内存地址,使用它的值总会访问相同的数据
- 可以是可变的,访问和修改静态可变变量是不安全(unsafe)的
- 常量
变量
Rust 使用 let
声明变量,规则如下:
- 默认情况下为不可变(immutable)变量,如:
let foo = 1;
let mut
声明的可变(mutable)变量,如:let mut bar = 2;
- Rust 中可以使用相同的名字声明新变量,且新变量会
Shadowing(隐藏)
之前声明的同名变量 - 以下划线
_
开头命名的变量,编译器不会警告未被使用
示例如下:
fn main() {
// 下面声明的变量是 mutable (可变)的
let mut foo = 1;
println!("{}", foo);
foo = 2;
println!("{}", foo);
let foo = "abc";
println!("foo is {}", foo);
// shadowing var
let foo = foo.len();
println!("foo len is {}", foo);
}
其中,{}
表示占位符,与 python format 类似。
数据类型
Rust 是静态编译语言,编译时需要确定所有变量的类型
- 根据值,编译器可以推断出变量类型
- 若值的类型可能比较多,必须添加类型标注,否则编译报错,如:
let num: i32 = "10".parse().expect("Not a number");
- 标量类型,共有4个:
- 整数类型,默认为 i32
- i8/i16/i32/i64/i128/isize 有符号整数类型,范围:$-(2^n - 1)$ ~ ($2^{n-1} - 1)$
- u8/u16/u32/u64/u128/usize 无符号整数类型,非负,范围:0 ~ $2^n - 1$
- size 由操作系统架构决定,32或64
- 字面值/量,下面除 byte 类型外,其他数值允许使用类型后缀,如:
24u16
表示 24
为 u16
类型- 十进制 Decimal,如:10_000
- 十六进制 Hex,如:0x1a
- 八进制 Octal,如:0o11
- 二进制 Binary,如:0b0001
- 字节 Byte(u8 only),如:b’A'
- 整数溢出,存在两种情况。以 u8(范围 0~255)类型为例,如果将 u8 变量的值设置为 256:
- 调试模式下:Rust 检查到溢出,程序运行时会 panic
- 发布模式下(
--release
)编译:Rust 不检查可能的 panic 溢出,若发生溢出,Rust 会执行 环绕
操作,即对 256 取余,256 转化为 0,257 转化为 1 …。程序不会 panic
- 浮点类型,默认为 f64,采用 IEEE-754 标准表达
- 布尔类型,占用一个字节
- 字符类型,占用4个字节,字面值使用单引号,Unicode 变量值,可以用来表示拼音、中日韩文字、emoj表情等,但 Unicode 中并没有
字符
的概念
- 复合类型:将多个值放在一个类型里,Rust 有两种复合类型:
- 元组(Tuple):将多个类型的多个值放在一个类型里
- 特点:长度固定,一旦声明,不能修改
- 声明:在小括号里将值分开,每个位置对应一个类型,且个元素的类型不必相同。
- 示例:
let foo: (i32, f64) = (1, 2.3);
- 获取:使用模式匹配来解构(destructure)Tuple 来获取元素的值,如:
let (x, y) = foo;
- 访问:Tuple 变量可以使用点标记法+索引获取
.
,访问 foo
时使用 foo.0
和 foo.1
分别查看 i32 和 f64 类型对应的值
- 数组:将多个相同类型的值放在一个类型里,长度固定,声明后不可修改
- 声明:中括号里,使用逗号
,
分隔 - 示例:
let a1 = [1,2,3,4];
- 数组的类型表示形式:
[类型; 长度]
,如:let a1: [i32; 4] = [1,2,3,4];
- 声明数组每个元素值都相同的特例:
let a2 = [3; 2];
相当于 let a2 = [3, 3];
- 访问数组的元素:
a1[0];
- 访问索引越界时,编译(可能)、运行时会报 panic 错误(
index out of bounds: the len is x but the index is y
)
- Vector:和数组类型,有标准库提供,长度可变
操作符
- 算术运算符:+,-,*,/(5/2=2),%(5%2=1)
注释
//
注释一行,可以在行开头;语句或表达式后面/* */
注释多行
/* comment main info */
/*
some info
*/
fn main() {
// main fn
println!("Hello, world!"); // print info
}
String 类型
- 组成:以下3部分组成,内容存放在 stack 上
- 指针 ptr,指向存放字符串内容的内存指针,内容存放在 heap 上
- 长度 len
- 容量 capacity
- 在 heap 上分配存储空间,支持存储编译时未知数量的文本
- 一般使用
from
函数从字符串字母值创建 String
类型,如:let str = String::from("hello string")
::
表示 from
是 String
类型下的函数
fn main() {
let mut str = String::from("hello string");
str.push_str("...");
println!("{}", str)
}
移动(move)
当变量离开作用域时,会调用 drop 函数,并将 String 使用的 heap 内容释放,下面示例中s1、s2字符串指向的内存相同,会触发二次释放(double free)bug。因此,Rust 设计采用 移动(move)
机制,使 s1
失效,当离开 s1、s2 作用域时仅释放一次内存。这也体现了 rust 内存安全性。
let s1 = String::from("hello");
let s2 = s1;
// 以后使用 s1 报错:borrow of moved value | value borrowed here after move
移动(move)
与下面的拷贝实现不同点在使原变量实现- 浅拷贝(shallow copy)
- 深拷贝(deep copy)
克隆(Clone)
对 String 上指针指向的数据(heap)进行深度拷贝,需要使用 clone 方法,示例:
let s1 = String::from("hello");
let s2 = s1.clone();
// 以下 s1/s2 都有效
Copy trait
copy trait
可以用于像整数这样完全存放在 stack 上的类型- 若一个类型实现了
copy trait
,那么旧的变量在复制后仍然可用 - 若一个类型或者该类型的一部分实现了
drop trait
,那么 Rust 不允许让它再去实现 copy trait
- 拥有
copy trait
的类型:- 任何简单
标量的组合类型
都可以 Copy 的 - 任何需要分配内存或某种资源的都不是 Copy 的
- 拥有
copy trait
的类型:- 所有整数类型,如u8
- bool
- char
- 所有浮点类型,如f32
- Tuple(元组),如果所有的字段都是 Copy 的
- (i32, i32) 是
- (i32, String) 不是
语句 vs 表达式
Rust 是一种基于表达式的语言,需要区分下语句和表达式的异同:
- 语句是执行一系列的动作指令,语句没有返回值(或默认的返回值为空tuple,即
()
) - 表达式会计算产生一个值
- 函数的定义也是语句
- 表达式(expression)没有后面的
;
,语句以 ;
结尾
fn main() {
let bar = {
let x = 1;
x + 10
};
println!("bar is {}", bar)
}
流程控制
条件判断 if else
if
表达式:根据条件(bool
类型)执行不同的代码分支- 与条件关联的代码块称为分支(
arm
) - 后面可以跟
else if
或 else
,如果有多个 else if
时,推荐使用 match
重构
fn main() {
let n = 2;
// if demo
if n > 5 {
println!("{} > 5", n)
} else if n == 5 {
println!("{} = 5", n)
} else {
println!("{} < 5", n)
}
// use match instead of if
match n.cmp(&5) {
std::cmp::Ordering::Greater => println!("{} > 5", n),
std::cmp::Ordering::Equal => println!("{} = 5", n),
std::cmp::Ordering::Less => println!("{} < 5", n),
};
// if
let b = if n == 2 {true} else {false};
println!("n==2 is {}", b)
}
循环
Rust 有3中循环实现:loop
、while
、for
loop
loop
:循环执行一块代码,一般使用 break
关键字停止程序循环,break
也可以指定返回值
fn main() {
let mut c = 0;
let r = loop {
c += 1;
if c == 5 {
break c;
}
};
println!("result is {}", r)
}
while
while
:每次执行循环前,判断一次循环条件,满足就执行
fn main() {
let mut c = 0;
while c != 5 {
println!("current number is {}", c);
c += 1;
}
}
for
for
:迭代指定的次数,由于其安全、简捷,比 while
和 loop
更常用
fn main() {
let arr = [1, 2, 3, 4, 5];
for num in arr.iter() {
println!("{}!", num);
}
for (index, value) in arr.iter().enumerate() {
println!("index: {}, value: {}", index, value)
}
}
for
一般和标准库提供的 Range
一起使用,Range 需要指定范围 [start, end)
,且可以使用 rev
翻转,翻转遍历 1~9 示例:
fn main() {
// not include 10
for num in (1..10).rev() {
println!("{}!", num);
}
// include 10
for num in (1..=10).rev() {
println!("- {}!", num);
}
}
其他
as 别名
as
关键字为导入的路径指定本地别名,示例:
use std::fmt::Result as fmtResult;
super
fn abc() {}
mod some_mod {
fn call_abc() {
super::abc(); // 相对路径调用
crate::abc(); // 决定路径调用
}
}