Rust 初体验
安装
打开官网,下载 rustup-init.exe, 选择缺省模式(1)安装。
国内源设置
在 .Cargo
目录下新建 config 文件,添加如下内容:
text
[source.crates-io]
registry = "https://github.com/rust-lang/crates.io-index"
# 指定镜像
replace-with = 'tuna'
# 清华大学
[source.tuna]
registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"
[registries.rsproxy]
index = "https://rsproxy.cn/crates.io-index"
[net]
git-fetch-with-cli = true
第一个程序
使用任意代码编辑器,编写如下代码,保存为后缀是.rs的源文件,如 hello.rs。
rust
fn main()
{
println!("Hello World!");
}
了解C语言的同学,会发现这个程序和C语言的hello.c非常相似。不同之处在于,fn
表示函数,main
表示程序的入口,println!
表示输出。!
表示宏,即 println
是个宏,而不是函数。
在源文件文件夹里,运行 rustc hello.rs
进行编译,生成 hello.exe。
运行 hello.exe,输出:
Hello World!
使用 Cargo 管理
cargo new project_name
创建一个新项目
cd project_name
进入项目
cargo build
编译生成Debug版本
cargo build --release
编译并生成可执行文件
cargo run
运行
cargo check
检查错误(不编译)
cargo 常用命令
text
build, b Compile the current package
check, c Analyze the current package and report errors, but don't build object files
clean Remove the target directory
doc, d Build this package's and its dependencies' documentation
new Create a new cargo package
init Create a new cargo package in an existing directory
add Add dependencies to a manifest file
remove Remove dependencies from a manifest file
run, r Run a binary or example of the local package
test, t Run the tests
bench Run the benchmarks
update Update dependencies listed in Cargo.lock
search Search registry for crates
publish Package and upload this package to the registry
install Install a Rust binary. Default location is $HOME/.cargo/bin
uninstall Uninstall a Rust binary
变量
Rust 是强类型语言,使用关键字 let 声明变量后,具有自动判断变量类型的能力,如:
let a = 123;
则默认 a 为整型数字,且精度不允许变化,即 a 的值不可改变,可以理解为"只读"。
这与 C 语言中的 const 修饰符起到的效果一样。在 C 语言中 int const a = 10;
a 的值也不允许改变。
Rust 中如果要让变量可变,需要用 mut 关键词:
rust
let mut a = 123;
a = 456;
指定数据类型
rust
let a: u64 = 123; // 后面跟数据类型u64,表示无符号64位整型变量。
let y: f32 = 3.0; // 后面跟数据类型f32,表示32位浮点型变量。
Shadowing(影射)
变量名可以被重新使用,如:
rust
let x = 5;
let x = x + 1;
let x = x * 2;
最终 x 的值为12。从语法上说, 变量 x 可以作为右值。
注释
可以使用 C/C++, Java 注释。
另外,可用 ///
表示文档开头注释
函数
定义函数,如需要参数,必须声明参数名称、类型。
rust
fn another_function(x: i32, y: i32) { }
可在{}
包括的块里,编写较为复杂的表达式,即所谓的函数体表达式。注:函数体表达式,并不能等同于函数体,不能使用 return 关键字。
rust
let y = {
let x = 3;
x + 1
};
返回值类型声明
在参数声明之后用->
来声明函数返回值的类型。 注:Rust 不支持自动返回值类型判断。
rust
fn add(a: i32, b: i32) -> i32 {
return a + b;
}
条件语句
rust
if a > 0 {
b = 1;
}
else if a < 0 {
b = -1;
}
else {
b = 0;
}
println!("b is {}", b);
if 后面不需要小括号,{}
必用。右括号 }
后不加 ;
。
条件表达式必须是 bool
类型,不同于 C/C++ 的非 0 即真。
可以使用 if-else 结构实现类似于三元条件运算表达式 (A ? B : C)
。如:
rust
let number = if a > 0 { 1 } else { -1 };
注:此处编译报错,检测出来其中 0 为 i32,代码未指定数据类型。故将0
改为&0
:
rust
let number = if a > &0 { 1 } else { -1 };
for 循环
最常用的循环结构。
rust
let a = [10, 20, 30, 40, 50];
for i in a.iter() {
println!("值为 : {}", i);
}
a.iter() 代表 a 的迭代器(iterator)
也可以通过下标来访问数组。如:
rust
let a = [10, 20, 30, 40, 50];
for i in 0..5 {
println!("a[{}] = {}", i, a[i]);
}
以上代码会报错。正确写法是:
rust
const a: [i32; 5] = [10, 20, 30, 40, 50];
for i in 0..5 {
println!("a[{}] = {}", i, a[i]);
}
可见,代码检查更严格。
while 循环
rust
let mut number = 1; // mut 表示number 在循环体内可以改变。
while number != 4 {
println!("{}", number);
number += 1;
}
无限循环结构 loop
可以通过 break 关键字,起到类似 return 一样的作用,使整个循环退出并给予外部一个返回值。
rust
let mut i = 0;
let location = loop {
let ch = s[i];
if ch == '*' {
break i;
}
i += 1;
};
以上代码会报错,可以改为:
rust
fn main() {
let s = "hello world";
let mut i = 0;
let location = loop {
let ch = s.chars().nth(i).unwrap();
if ch == ' ' {
break i;
}
i += 1;
};
println!("{}", location);
}
变量与数据交互
方式有移动(Move)和克隆(Clone)两种,前者不保存移动前的变量,后者继续保留移动前的变量。
rust
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // 错误!s1 已经失效
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2); //ok
引用(Reference)
& 运算符可以取变量的"引用"。当一个变量的值被引用时,变量本身不会被认定无效。注:C++中的概念。
引用不会获得值的所有权。引用只能租借(Borrow)值的所有权。
引用本身也是一个类型并具有一个值,这个值记录的是别的值所在的位置,但引用不具有所指值的所有权。
&mut 修饰可变的引用类型。
可变引用不允许多重引用,但不可变引用可以。
以上机制设计,主要出于对并发状态下发生数据访问碰撞的考虑,使得编译阶段就避免发生。
垂悬引用(Dangling References)
Rust 语言里不允许出现,如果有,编译器会发现它。
更多内容参看: https://www.runoob.com/rust/rust-ownership.html
字符串切片(String Slice)
rust
let s = String::from("broadcast");
let part1 = &s[0..5];
let part2 = &s[5..9];
println!("{}={}+{}", s, part1, part2);
在 Rust 中有两种常用的字符串类型:str 和 String。
str 是 Rust 核心语言类型,凡用双引号包括的字符串常量整体的类型性质都是 &str。
String 类型是 Rust 标准公共库提供的一种数据类型,其功能更完善。String 和 str 都支持切片,切片的结果是 &str 类型的数据。
注:切片结果必须是引用类型,但开发者必须明示:
rust
let slice = &s[0..3];
除了字符串,其他一些线性数据结构也支持切片操作,如:
rust
let arr = [1, 3, 5, 7, 9];
let part = &arr[0..3];
for i in part.iter() {
println!("{}", i);
}
结构体
定义结构体:
rust
struct Site {
domain: String,
name: String,
nation: String,
found: u32
}
注:Rust 里 struct 语句仅用来定义,不能声明实例,结尾不需要;
符号,且每个字段定义之后用 ,
分隔。
Rust 很多地方受 JavaScript 影响,在实例化结构体的时候用 JSON 对象的 key: value 语法来实现定义:
rust
let runoob = Site {
domain: String::from("www.runoob.com"),
name: String::from("RUNOOB"),
nation: String::from("China"),
found: 2013
};
如果正在实例化的结构体,有字段名称与现存变量名称一样,可以简化书写。
rust
let domain = String::from("www.runoob.com");
let name = String::from("RUNOOB");
let runoob = Site {
domain, // 等同于 domain : domain,
name, // 等同于 name : name,
nation: String::from("China"),
traffic: 2013
};
新建一个结构体实例,如果其中大部分属性需要被设置成与现存的一个结构体属性一样,仅需更改其中一两个字段的值,可以使用结构体更新语法:
rust
let site = Site {
domain: String::from("www.runoob.com"),
name: String::from("RUNOOB"),
..runoob
};
注:...runoob 后面不可以有逗号。这种语法不允许一成不变的复制另一个结构体实例,至少重新设定一个字段的值才能引用其他实例的值。
元组结构体
rust
struct Color(u8, u8, u8);
struct Point(f64, f64);
let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);
元组结构体对象的使用方式和元组一样,通过 .
和下标来进行访问:
rust
struct Color(u8, u8, u8);
struct Point(f64, f64);
let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);
println!("black = ({}, {}, {})", black.0, black.1, lack.2);
println!("origin = ({}, {})", origin.0, origin.1);
结构体必须掌握字段值所有权,因为结构体失效的时候会释放所有字段。
结构体输出
导入调试库 #[derive(Debug)]
在 println 和 print 宏中可以用 {:?}
占位符输出一整个结构体;结构体如果属性较多,可使用另一个占位符 {:#?}
。
结构体方法
结构体方法的第一个参数必须是 &self
,不需声明类型。
rust
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!("rect1's area is {}", rect1.area());
}
这里与Python中的Class方法有神似。
结构体关联函数
不依赖实例,使用时需要声明是在哪个 impl 块中的。String::from
函数就是一个"关联函数"。
rust
# [derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn create(width: u32, height: u32) -> Rectangle {
Rectangle { width, height }
}
}
fn main() {
let rect = Rectangle::create(30, 50);
println!("{:?}", rect);
}
这里的Rectangle中的create函数,即是结构体关联函数。这个概念与 C++语言里面的类成员函数有些相似。不同之处在于, C++ 类成员函数是在类的内部定义,并且使用 this 指针来访问类的实例。
单元结构体
结构体可以只作为一种象征而无需任何成员:
rust
struct UnitStruct;
枚举类
rust
# [derive(Debug)]
enum Book {
Papery, Electronic
}
fn main() {
let book = Book::Papery;
println!("{:?}", book);
}
为枚举类成员添加元组属性描述
rust
enum Book {
Papery(u32),
Electronic(String),
}
let book = Book::Papery(1001);
let ebook = Book::Electronic(String::from("url://..."));
为属性命名,可以用结构体语法:
rust
enum Book {
Papery { index: u32 },
Electronic { url: String },
}
let book = Book::Papery{index: 1001};
match 语法
枚举的目的是对某一类事物的分类,分类的目的是为了对不同的情况进行描述。Rust 通过 match 语句来实现分支结构。
rust
fn main() {
enum Book {
Papery {index: u32},
Electronic {url: String},
}
let book = Book::Papery{index: 1001};
let ebook = Book::Electronic{url: String::from("url...")};
match book {
Book::Papery { index } => {
println!("Papery book {}", index);
},
Book::Electronic { url } => {
println!("E-book {}", url);
}
}
}
注意其中的,号与;
号。
rust
match 枚举类实例 {
分类1 => 返回值表达式,
分类2 => 返回值表达式,
...
}
对非枚举类进行分支选择时必须注意处理例外情况,即使在例外情况下没有任何要做的事 . 例外情况用下划线 _ 表示:
rust
fn main() {
let t = "abc";
match t {
"abc" => println!("Yes"),
_ => {},
}
}
Option 枚举类
Rust 标准库中的枚举类,用于填补 Rust 不支持 null 引用的空白。Rust 在语言层面彻底不允许空值 null 的存在,但null 可以高效地解决少量的问题,所以 Rust 引入了 Option 枚举类:
rust
enum Option<T> {
Some(T),
None,
}
如果想定义一个可以为空值的类,可以这样:
rust
let opt = Option::Some("Hello");
如果想针对 opt 执行某些操作,必须先判断它是否是 Option::None:
rust
fn main() {
let opt = Option::Some("Hello");
match opt {
Option::Some(something) => {
println!("{}", something);
},
Option::None => {
println!("opt is nothing");
}
}
}
初始值为空的 Option 必须明确类型:
rust
fn main() {
let opt: Option<&str> = Option::None;
match opt {
Option::Some(something) => {
println!("{}", something);
},
Option::None => {
println!("opt is nothing");
}
}
}
这种设计会让空值编程变得不容易,但这正是构建一个稳定高效的系统所需要的。由于 Option 是 Rust 编译器默认引入的,在使用时可以省略 Option:: 直接写 None 或者 Some()。
rust
fn main() {
let t = Some(64);
match t {
Some(64) => println!("Yes"),
_ => println!("No"),
}
}
又:
rust
let i = 0;
match i {
0 => println!("zero"),
_ => {},
}
用 if let 语法缩短这段代码:
rust
let i = 0;
if let 0 = i {
println!("zero");
}
如:
rust
fn main() {
enum Book {
Papery(u32),
Electronic(String)
}
let book = Book::Electronic(String::from("url"));
if let Book::Papery(index) = book {
println!("Papery {}", index);
} else {
println!("Not papery book");
}
}
注意其中 if 语句后的 = 号。
组织管理
Rust 中有三个重要的组织概念:箱、包、模块。
箱(Crate)
"箱"是二进制程序文件或者库文件,存在于"包"中。树状结构,树根是编译器开始运行时编译的源文件所编译的程序。
注意:"二进制程序文件"不一定是"二进制可执行文件",只能确定是是包含目标机器语言的文件,文件格式随编译环境的不同而不同。
包(Package)
当使用 Cargo 执行 new 命令创建 Rust 工程时,工程目录下会建立一个 Cargo.toml 文件。工程的实质就是一个包,包必须由一个 Cargo.toml 文件来管理,该文件描述了包的基本信息以及依赖项。
一个包最多包含一个库"箱",可以包含任意数量的二进制"箱",但是至少包含一个"箱"(不管是库还是二进制"箱")。
当使用 cargo new 命令创建完包之后,src 目录下会生成一个 main.rs 源文件,Cargo 默认这个文件为二进制箱的根,编译之后的二进制箱将与包名相同。
模块(Module)
对于一个软件工程来说,往往按照所使用的编程语言的组织规范来进行组织,组织模块的主要结构往往是树。Java 组织功能模块的主要单位是类,而 JavaScript 组织模块的主要方式是 function。Rust 中的组织单位是模块(Module)。
这些先进的语言的组织单位可以层层包含,就像文件系统的目录结构一样。Rust 中的组织单位是模块(Module)。
路径分为绝对路径和相对路径。绝对路径从 crate 关键字开始描述。相对路径从 self 或 super 关键字或一个标识符开始描述。Rust 中的路径分隔符是 ::
。如:
crate::nation::government::govern();
是描述 govern 函数的绝对路径,相对路径可以表示为:
nation::government::govern();
访问权限
Rust 中有两种简单的访问权:公共(public)和私有(private)。
默认情况下,如果不加修饰符,模块中的成员访问权将是私有的。
如果想使用公共权限,需要使用 pub
关键字。
对于私有的模块,只有在与其平级的位置或下级的位置才能访问,不能从其外部访问。
rust
mod nation {
pub mod government {
pub fn govern() {}
}
mod congress {
pub fn legislate() {}
}
mod court {
fn judicial() {
super::congress::legislate();
}
}
}
fn main() {
nation::government::govern();
}
这段程序是能通过编译的。请注意观察 court 模块中 super 的访问方法。更多的信息请参考:
https://www.runoob.com/rust/rust-project-management.html
use 关键字能够将模块标识符引入当前作用域
rust
mod nation {
pub mod government {
pub fn govern() {}
}
}
use crate::nation::government::govern; // 解决局部模块路径过长的问题。
fn main() {
govern();
}
重命名
rust
mod nation {
pub mod government {
pub fn govern() {}
}
pub fn govern() {}
}
use crate::nation::government::govern;
use crate::nation::govern as nation_govern; // 解决重名问题
fn main() {
nation_govern();
govern();
}
还可以与 pub
关键字配合使用:
rust
mod nation {
pub mod government {
pub fn govern() {}
}
pub use government::govern;
}
fn main() {
nation::govern();
}
所有系统库模块都是被默认导入的,所以在使用的时候只需要使用 use 关键字简化路径即可使用。
在 Rust 中没有 Exception。对可恢复错误用 Result<T, E>
类来处理,对不可恢复错误使用 panic!
宏来处理。
不可恢复错误
rust
fn main() {
panic!("error occured"); // 程序将在此处中断,不执行后续语句
println!("Hello, Rust");
}
回溯是不可恢复错误的另一种处理方式,它会展开运行的栈并输出所有的信息,然后程序依然会退出。上面的省略号省略了大量的输出信息,我们可以找到我们编写的 panic! 宏触发的错误。
可恢复的错误
Rust 通过 Result<T, E>
枚举类作返回值来进行异常表达。
rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
match f {
Ok(file) => {
println!("File opened successfully.");
},
Err(err) => {
println!("Failed to open the file.");
}
}
}
可用 if let 语法对简化 match 语法块:
rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
if let Ok(file) = f {
println!("File opened successfully.");
} else {
println!("Failed to open the file.");
}
}
Rust 中可以在 Result 对象后添加 ?
操作符将同类的 Err 直接传递出去.
rust
fn f(i: i32) -> Result<i32, bool> {
if i >= 0 { Ok(i) }
else { Err(false) }
}
fn g(i: i32) -> Result<i32, bool> {
let t = f(i)?;
Ok(t) // 因为确定 t 不是 Err, t 在这里已经是 i32 类型
}
fn main() {
let r = g(10000);
if let Ok(v) = r {
println!("Ok: g(10000) = {}", v);
} else {
println!("Err");
}
}
?
符的实际作用是将 Result 类非异常的值直接取出,如果有异常就将异常 Result 返回出去。所以,?
符仅用于返回值类型为 Result<T, E>
的函数,其中 E 类型必须和 ?
所处理的 Result 的 E 类型一致。
判断 Result 的 Err 类型,用函数 kind()。
rust
use std::io;
use std::io::Read;
use std::fs::File;
fn read_text_from_file(path: &str) -> Result<String, io::Error> {
let mut f = File::open(path)?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
fn main() {
let str_file = read_text_from_file("hello.txt");
match str_file {
Ok(s) => println!("{}", s),
Err(e) => {
match e.kind() {
io::ErrorKind::NotFound => {
println!("No such file");
},
_ => {
println!("Cannot read the file");
}
}
}
}
}