变量
在 Rust 中, 变量声明使用的是 let 关键字。
fn main() {
let msg: &str = "Hello, world!";
println!("{}", msg);
}
我们不需要每个变量都专门标注类型, 编译器会自动推断出变量的类型。 只有当编译器无法推断出变量类型时, 才需要手动标注类型。
因此以上代码可以更改为:
fn main() {
let msg = "Hello, world!";
println!("{}", msg);
}
我们的 CLI 用于记录 Todo 事项, 因此我们需要可以输入内容。
Rust 官方提供了标准库 std::env 用来获取环境信息。它提供了一个 args 函数, 允许获取命令行参数。
更改 main.rs 为如下内容:
fn main() {
// 因为 args 函数返回的是一个迭代器, 我们需要将其收集为一个数据集合
// 用户输入是未知的, 因此我们需要指定收集的输入类型, 即 String
let args: Vec<String> = std::env::args().collect();
println!("{:#?}", args);
}
命令行运行 cargo run -- a b, 结果如下:
[
"target\\debug\\cli.exe",
"a",
"b",
]
可以看见, 我们获得的输入是一个数组格式, 它的第一项是我们的可执行文件路径。
我们需要的是输入的内容, 即 a 和 b
fn main() {
let args: Vec<String> = std::env::args().collect();
let title = args[1];
let content = args[2];
println!("todo title: {}, content: {}", title, content);
}
运行 cargo run -- a b。会发现有报错:
error[E0507]: cannot move out of index of `Vec<String>`
--> src\main.rs:6:15
|
6 | let title = args[1];
| ^^^^^^^ move occurs because value has type `String`, which does not implement the `Copy` trait
|
help: consider borrowing here
|
6 | let title = &args[1];
| +
help: consider cloning the value if the performance cost is acceptable
|
6 | let title = args[1].clone();
| ++++++++
error[E0507]: cannot move out of index of `Vec<String>`
--> src\main.rs:7:17
|
7 | let content = args[2];
| ^^^^^^^ move occurs because value has type `String`, which does not implement the `Copy` trait
|
help: consider borrowing here
|
7 | let content = &args[2];
| +
help: consider cloning the value if the performance cost is acceptable
|
7 | let content = args[2].clone();
| ++++++++
所有权
以上报错的关键如下:
cannot move out of index of `Vec<String>`
move occurs because value has type `String`, which does not implement the `Copy` trait
它的意思是: 无法从 Vec<String> 中取出值, 因为 String 类型没有实现 Copy 特征, 无法被隐式复制。
在前面有提及到:
Rust 它从语言设计层面 “不信任开发者”, 认为 “你总有一天会犯错”。
因此, Rust 引入了所有权系统、借用检查和生命周期机制。以求在编译阶段就将那些 “未来可能出问题的代码” 拒之门外。
这里就是因为 Rust 引入的所有权系统导致的问题。
根据 Rust 所有权规则:
- 每个值都有一个所有者。
- 每个值同时只能有一个所有者。
- 当所有者离开作用域时, 这个值将被丢弃。
以上报错就很好理解了。
我们试图从 Vec<String> 这个类型中取出值, 但是根据所有权原则, 每个值都只能有一个所有者。
因此 Vec<String> 拥有它内部所有 String 元素的所有权。
当我们使用 args[1] 这样的方式访问时, 实际上是尝试将该元素的所有权 “移动” 到另一个变量。这就违反了所有权规则,
因为 args 还可能在后续被使用, 如果移动了元素所有权, 那么会导致它内部状态不一致, 甚至出现悬垂指针、重复释放等问题。
引用和借用
Rust 在其语言设计层面上“不信任开发者”, 因此它采用了所有权系统来强制保障内存安全。 也因此编译器非常“智能”, 它不仅会告诉你哪里出错了, 还会提供修复建议。
比如, 下面的编译错误信息中就给出了两种可能的解决方式:
help: consider borrowing here
|
6 | let title = &args[1];
| +
help: consider cloning the value if the performance cost is acceptable
|
6 | let title = args[1].clone();
| ++++++++
第一种方法是 let title = &args[1];, 它表示借用 args[1] 的值, 而不移动它的所有权。
这种方式高效, 不会复制数据, 但是变量的类型将变为 &String, 表示这个变量是一个 String 值的引用。
因此它将受到引用对象的限制。当 args 失效, 那么它的引用也将失效。
而第二种方法是 let title = args[1].clone();, 它表示克隆 args[1] 的值,
并将这个值移动到 title 变量中, 这样 args 失效时, 也不会影响 title 的使用。
因此, 我们选择使用第二种方式, 显式调用 clone 方法, 克隆一份 args[1] 的值。
创建一个引用的行为叫做借用。引用则是借用这个行为的结果。
我们再次运行 cargo run -- a b, 可以发现编译通过了。
可变变量
在当前实现中, 每次运行程序都需要输入两个参数(标题和内容), 否则程序会因索引越界而报错。 为了提升程序的健壮性, 我们可以为缺失的参数设置默认值。
修改代码:
fn main() {
let args: Vec<String> = std::env::args().collect();
let len = args.len();
let title = args[1].clone();
let content = String::from("default content");
if len > 2{
content = args[2].clone();
}
println!("todo title: {}, content: {}", title, content);
}
以上代码中, 我们对输入参数做了检查, 一旦参数数量大于 2 个, 就会使用第三个参数作为内容。否则就会使用默认值。
执行 cargo run -- a, 发现又有报错了。
error[E0384]: cannot assign twice to immutable variable `content`
--> src\main.rs:10:5
|
7 | let content = String::from("default content");
| ------- first assignment to `content`
...
10 | content = args[2].clone();
| ^^^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
7 | let mut content = String::from("default content");
| +++
这是因为 Rust 出于安全性和可读性考虑, 默认所有变量都是不可变的。
这段报错的意思是: 不能对不可变变量 content 进行二次赋值, 除非将它声明为可变的。
编译器已经为我们提示了。在 let 后面增加 mut 关键字即可。
fn main() {
let args: Vec<String> = std::env::args().collect();
let len = args.len();
let title = args[1].clone();
let mut content = String::from("default content");
if len > 2{
content = args[2].clone();
}
println!("todo title: {}, content: {}", title, content);
}
再次执行 cargo run -- a, 成功运行。
变量类型
Rust 是一门强类型的语言, 这意味着变量在编译时必须要有明确的类型。
类型确定方式有两种, 分别是显式声明和隐式推断。
显式声明, 在变量名称后面使用 : 指定类型。
例如: let args: Vec<String> = std::env::args().collect();。将变量 args 的类型指定为 Vec<String>。
隐式推断, 编译器根据变量的值和上下文推断变量的类型。 而 Rust 有着强大的类型推断机制, 使得我们在大多数情况下, 不需要手动标注类型。 编译器会自动推断类型, 只有当编译器无法推断类型时才需要手动标注。
例如以下代码中, 我们并未显式声明 len, title 或 content 的类型, 但它们的类型仍然是确定的:
let args: Vec<String> = std::env::args().collect();
let len /* usize */ = args.len();
let title /* String */ = args[1].clone();
let mut content /* String */ = String::from("default content");
Rust 支持常见的基本类型:
- 整型:
i8,i16,i32,i64,i128,isize - 无符号整型:
u8,u16,u32,u64,u128,usize - 浮点数:
f32,f64 - 布尔值:
bool - 字符:
char
需要注意的是, Rust 中, "xxx" 是一个字符串字面量切片, 类型为 &str, 是在编译时就固定不可变的。
而 String 是一个字符串类型, 编译时动态分配, 可变长度。
我们在前面使用的 args 是 Vec<String>, 就是一个动态字符串的集合。