文章目录
安装Rust
创建Rust项目
#cargo new 项目名
cargo new project
打开src/main.rs
,打开控制台
编译Rust
cargo build
运行Rust
cargo run
windows Rust编译成可执行文件(.exe)
rustc .rs文件路径
初步认识Rust
基础数据类型
对于学习过
javascript
的人来说比较好懂
let a = 123;
println!("a is {}",a)
注意:a不能是
a="123";a=12.3;a=345
- 第一行的错误在于当声明 a 是 123 以后,a 就被确定为整型数字,不能把字符串类型的值赋给它。
- 第二行的错误在于自动转换数字精度有损失,Rust 语言不允许精度有损失的自动数据类型转换。
- 第三行的错误在于 a 不是个可变变量。这就牵扯到了 Rust 语言为了高并发安全而做的设计:在语言层面尽量少的让变量的值可以改变。所以 a 的值不可变。但这不意味着 a 不是"变量"(英文中的 variable),官方文档称 a 这种变量为"不可变变量"。
使变量变得"可变"(mutable)只需一个mut
关键字
let mut a = 123;
a = 456;
println!("a is {}",a);
虽然 Rust 有自动判断类型的功能,但有些情况下声明类型更加方便
let a: u64 = 123;
整数型(Integer)
整数型简称整型,按照比特位长度和有无符号分为以下种类:
位长度 | 有符号 | 无符号 |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch isize | usize |
浮点数型(Floating-Point)
Rust 与其它语言一样支持 32 位浮点数(f32)和 64 位浮点数(f64)。默认情况下,64.0 将表示 64 位浮点数,因为现代计算机处理器对两种浮点数计算的速度几乎相同,但 64 位浮点数精度更高。
实例
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
函数
fn 函数名 ( 参数 ) 函数体{}
举例(有返回值)
//没有返回值
fn main(){
let test1:i16 = test(10,12);
}
//有返回值
fn test(a:i16,b:i16) -> i16{
return a + b;
}
c中是
/*
int testSum(int a,int b){
return a + b;
}
int main(){
int sum = testSum(10,15);
return 0;
}
*/
//函数申明
int testSum(int a,int b);
int main(){
int sum = testSum(10,15);
return 0;
}
int testSum(int a,int b){
return a + b;
}
以上的函数申明若是在Rust中会出现警告,因为在 Rust 语言中,函数名通常遵循 snake_case 命名规则,即所有字母小写,单词之间用下划线分隔。如果想取消警告可以写成
fn test_sum
Rust 条件语句
let number:u16 = 3;
if number < 3 {
println!("条件:true")
}else{
println!("条件:false")
}
else-if
let number:u16 = 3; if number < 3 { println!("条件:true") }else if number = 3{ println!("条件:false") }else{ println!("条件:123") }
Rust循环
Rust中的循环还是
while
和for
俩个关键字
while true{}
最好不要使以上写法,可以是加一个结束条件作为循环结束的一个标志
//while 循环结束条件{}
let mut number:i16 = 0;
while number < 10{
number += 1;
}
for i in 0..5 { //>=0;<5
println!("i is {}",i)
}
let a = [10, 20, 30, 40, 50];
for i1 in a.iter() {
println!("值为 : {}", i1);
}
Rust
中还有一种循环,是代替了while true
那就是loop
let mut number:i16 = 0;
loop {
number += 1;
if number > 15 {
break;
}
}
Rust 迭代器
使用
iter()
方法创建借用迭代器
fn TestWhile(){
let array: [i32; 6] = [1,2,3,4,5,6];
let iter = array.iter();
for i in iter{
println!("iter is {}",i);
if iter[2] == 3 { //x
iter[3] = 10; //x
}
}
}
会报错,原因:不可变
使用
iter_mut()
方法创建可变借用迭代器(修改其中的变量)
fn TestWhile(){
let mut array = [1,2,3,4,5,6];
let mut iter = array.iter_mut(); //iter
// 假设我们想要访问第一个元素
let first = iter.next().unwrap();
*first = 10;
println!("*first valuse is {:?}",array)
}
闭包
闭包函数声明
|参数...| { 表达式 }
参数可以有类型注解,也可以省略,Rust 编译器会根据上下文推断它们。
let add_one = |x:i16|x+1;
闭包的参数和返回值: 闭包可以有零个或多个参数,并且可以返回一个值。
let calculate = |a, b, c| a * b + c;
println!("calculate value is {}",calculate(1,2,3));
从上面的println!()
可以知道闭包在 Rust 中类似于匿名函数,可以在代码中以 {}
语法块的形式定义,使用 ||
符号来表示参数列表
闭包可以捕获周围环境中的变量,这意味着它可以访问定义闭包时所在作用域中的变量。
let a:i16 = 15;
let sum = |x:i16|x+a;
println!("sum value is {}",sum(1)); //16
移动与借用
借用变量:默认情况下,闭包会借用它捕获的环境中的变量,这意味着闭包可以使用这些变量,但不能改变它们的所有权。这种情况下,闭包和外部作用域都可以使用这些变量。
let x = 10;
let add_x = |y| x + y;
println!("{}", add_x(5)); // 输出 15
println!("{}", x); // 仍然可以使用 x
获取所有权:通过在闭包前添加
move
关键字,闭包会获取它捕获的环境变量的所有权。这意味着这些变量的所有权会从外部作用域转移到闭包内部,外部作用域将无法再使用这些变量。
let s = String::from("hello");
let print_s = move || println!("{}", s);
print_s(); // 输出 "hello"
// println!("{}", s); // 这行代码将会报错,因为 s 的所有权已经被转移给了闭包
Rust 所有权
所有权规则
所有权有以下三条规则:
- Rust 中的每个值都有一个变量,称为其所有者。
- 一次只能有一个所有者。
- 当所有者不在程序运行范围时,该值将被删除。
变量范围
{
// 在声明以前,变量 s 无效
let s = "runoob";
// 这里是变量 s 的可用范围
}
// 变量范围已经结束,变量 s 无效
内存和分配
Rust 中没有调用 free 函数来释放字符串 s 的资源(我知道这样在 C 语言中是不正确的写法,因为 “runoob” 不在堆中,这里假设它在)。Rust 之所以没有明示释放的步骤是因为在变量范围结束的时候,Rust 编译器自动添加了调用释放资源函数的步骤。
这种机制看似很简单了:它不过是帮助程序员在适当的地方添加了一个释放资源的函数调用而已。但这种简单的机制可以有效地解决一个史上最令程序员头疼的编程问题。
变量与数据交互的方式
变量与数据交互方式主要有移动(Move) 和 克隆(Clone) 俩种
移动
多个变量可以在 Rust 中以不同的方式与相同的数据交互
- 所有整数类型,例如
i32
、u32
、i64
等。 - 布尔类型 bool,值为
true
或false
。 - 所有浮点类型,
f32
和f64
。 - 字符类型
char
。 - 仅包含以上类型数据的元组(Tuples)。
但如果发生交互的数据在堆中就是另外一种情况:
let s1 = String::from("hello");
let s2 = s1;
第一步产生一个 String 对象,值为 “hello”。其中 “hello” 可以认为是类似于长度不确定的数据,需要在堆中存储。
第二步的情况略有不同(这不是完全真的,仅用来对比参考):
如图所示:两个 String 对象在栈中,每个 String 对象都有一个指针指向堆中的 “hello” 字符串。在给 s2 赋值时,只有栈中的数据被复制了,堆中的字符串依然还是原来的字符串。
前面我们说过,当变量超出范围时,Rust 自动调用释放资源函数并清理该变量的堆内存。但是 s1 和 s2 都被释放的话堆区中的 “hello” 被释放两次,这是不被系统允许的。为了确保安全,在给 s2 赋值时 s1 已经无效了。没错,在把 s1 的值赋给 s2 以后 s1 将不可以再被使用。下面这段程序是错的:
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // 错误!s1 已经失效
所以实际情况是:
s1 名存实亡。
克隆
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
}
涉及函数的所有权机制
对于变量来说这是最复杂的情况了。
如果将一个变量当作函数的参数传给其他函数,怎样安全的处理所有权呢?
下面这段程序描述了这种情况下所有权机制的运行原理:
fn main() {
let s = String::from("hello");
// s 被声明有效
takes_ownership(s);
// s 的值被当作参数传入函数
// 所以可以当作 s 已经被移动,从这里开始已经无效
let x = 5;
// x 被声明有效
makes_copy(x);
// x 的值被当作参数传入函数
// 但 x 是基本类型,依然有效
// 在这里依然可以使用 x 却不能使用 s
} // 函数结束, x 无效, 然后是 s. 但 s 已被移动, 所以不用被释放
fn takes_ownership(some_string: String) {
// 一个 String 参数 some_string 传入,有效
println!("{}", some_string);
} // 函数结束, 参数 some_string 在这里释放
fn makes_copy(some_integer: i32) {
// 一个 i32 参数 some_integer 传入,有效
println!("{}", some_integer);
} // 函数结束, 参数 some_integer 是基本类型, 无需释放
Rust Slice(切片)类型
切片(Slice)是对数据值的部分引用。
切片这个名字往往出现在生物课上,我们做样本玻片的时候要从生物体上获取切片,以供在显微镜上观察。在 Rust 中,切片的意思大致也是这样,只不过它从数据取材引用。
字符串切片
fn testSlice(){
let s = String::from("myslice");
let s1 = &s[0..2]; //<=&>的范围内。也就是0<=&>2的范围内(0,1)
let s2 = &s[2..7];
println!("{}={}+{}",s,s1,s2);
}
注意:到目前为止,尽量不要在字符串中使用非英文字符,因为编码的问题。具体原因会在"字符串"章节叙述。
被切片引用的字符串禁止更改其值:
let s = String::from("myslice");
let s1 = &s[0..2];
let s2 = &s[2..7];
s.push_str("hello");//错误
s 被部分引用,禁止更改其值。
实际上,到目前为止你一定疑惑为什么每一次使用字符串都要这样写String::from(“runoob”) ,直接写 “runoob” 不行吗?
事已至此我们必须分辨这两者概念的区别了。在 Rust 中有两种常用的字符串类型:str 和 String。str 是 Rust 核心语言类型,就是本章一直在讲的字符串切片(String Slice),常常以引用的形式出现(&str)。
凡是用双引号包括的字符串常量整体的类型性质都是 &str
let s = "hello";//let s:&str = "hello";
有一个快速的办法可以将 String 转换成 &str
let s1 = String::from("hello");
let s2 = &s1[..];
非字符串切片
let arr = [1, 3, 5, 7, 9];
let part = &arr[0..3];
for i in part.iter() {
println!("{}", i);
}
结构体
结构体类名 {
字段名 : 字段值,
...
}
struct TestUser {
uid: i32,
name:String,
sex:bool,
detail:String,
}
pub fn TestStruct(){
let user = TestUser {
uid:0,
name:String::from("admin"),
sex:false,
detail: String::from("管理员")
};
println!("user name:{}",user.name)
}
如果正在实例化的结构体有字段名称和现存变量名称一样的,可以简化书写
let uid:i32 = 0;
let name = String::from("admin")
let user = TestUser {
uid,// 等同于 uid: uid,
name,// 等同于 name : name,
sex:false,
detail: String::from("管理员")
};
有这样一种情况:你想要新建一个结构体的实例,其中大部分属性需要被设置成与现存的一个结构体属性一样,仅需更改其中的一两个字段的值,可以使用结构体更新语法
let uid:i32 = 0;
let name = String::from("admin")
let user = TestUser {
uid,// 等同于 uid: uid,
name,// 等同于 name : name,
sex:false,
detail: String::from("管理员")
};
let user1 = TestUser{
uid:1,
name:String::from("张三"),
..user //注意:注意:..user 后面不可以有逗号。这种语法不允许一成不变的复制另一个结构体实例,意思就是说至少重新设定一个字段的值才能引用其他实例的值
};
查看输出(引用)
println!("user name:{};user1 detail:{}",user.name,user1.detail)
元组结构体
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, black.2);
println!("origin = ({}, {})", origin.0, origin.1);
输出结构体
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("rect1 is {:?}", rect1);
}
如第一行所示:一定要导入调试库 #[derive(Debug)]
,之后在 println
和 print
宏中就可以用 {:?}
占位符输出一整个结构体。如果属性较多的话可以使用另一个占位符 {:#?}
结构体方法
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());
}
请注意,在调用结构体方法的时候不需要填写 self
,这是出于对使用方便性的考虑。其他语言可以理解为this
结构体关联函数
之所以结构体方法不叫结构体函数是因为函数这个名字留给了这种函数:它在 impl 块中却没有 &self 参数。
这种函数不依赖实例,但是使用它需要声明是在哪个 impl 块中的。
#[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);
}
单元结构体
结构体可以只作为一种象征而无需任何成员。我们称这种没有身体的结构体为单元结构体(Unit Struct)
struct UnitStruct;
Rust 枚举类
#[derive(Debug)]
enum Book {
Papery, Electronic
}
fn main() {
let book = Book::Papery;
println!("{:?}", book);
}
你可以为枚举类成员添加元组属性描述
enum Book {
Papery(u32),
Electronic(String),
}
let book = Book::Papery(1001);
let ebook = Book::Electronic(String::from("url://..."));
如果你想为属性命名,可以用结构体语法
enum Book {
Papery { index: u32 },
Electronic { url: String },
}
let book = Book::Papery{index: 1001};
match 语法
Rust 通过
match
语句来实现分支结构。先认识一下如何用 match 处理枚举类
错误语法
enum Book{
Papery { index: u32 },
Electronic { url: String },
}
pub fn TestBook(){
let book = Book::Papery { index: 1001 };
let ebook = Book::Electronic { url: String::from("https://....") };
match book{
Book::Papery { index } => {
println!("Paper book {}",index)
},
Book::Electronic { url } => {
println!("E-book {}",url)
}
}
}
输出结果
match 块也可以当作函数表达式来对待,它也是可以有返回值的
match 枚举类实例 {
分类1 => 返回值表达式,
分类2 => 返回值表达式,
...
}
但是如下图所示,enum
中的分类是不能少的
match 除了能够对枚举类进行分支选择以外,还可以对整数、浮点数、字符和字符串切片引用(&str)类型的数据进行分支选择。其中,浮点数类型被分支选择虽然合法,但不推荐这样使用,因为精度问题可能会导致分支错误。
对非枚举类进行分支选择时必须注意处理例外情况,即使在例外情况下没有任何要做的事 . 例外情况用下划线_
表示
fn main() {
let t = "abc";
match t {
"abc" => println!("Yes"),
_ => {},
}
}
Option 枚举类
Option 是 Rust 标准库中的枚举类,这个类用于填补 Rust 不支持 null 引用的空白
Java 默认支持 null,但可以通过 @NotNull 注解限制出现 null,这是一种应付的办法。
Rust 在语言层面彻底不允许空值 null 的存在,但无奈null 可以高效地解决少量的问题,所以 Rust 引入了 Option 枚举类
enum Option<T> {
Some(T),
None,
}
如果你想定义一个可以为空值的类,你可以这样
let opt = Option::Some("Hello");
如果你想针对 opt 执行某些操作,你必须先判断它是否是 Option::None
fn main() {
let opt = Option::Some("Hello");
match opt {
Option::Some(something) => {
println!("{}", something);
},
Option::None => {
println!("opt is nothing");
}
}
}
if let 语法
let i = 0;
match i {
0 => println!("zero"),
_ => {},
}
这段程序的目的是判断 i 是否是数字 0,如果是就打印 zero
现在用 if let 语法缩短这段代码
let i = 0;
if let 0 = i {
println!("zero");
}
if let 语法格式
if let 匹配值 = 源变量 {
语句块
}
Rust 组织管理
Rust 中有三个重要的组织概念:
箱
、包
、模块
箱(Crate)
箱是二进制程序文件或者库文件,存在于"包"中。
"箱"是树状结构的,它的树根是编译器开始运行时编译的源文件所编译的程序。
注意:“二进制程序文件"不一定是"二进制可执行文件”,只能确定是是包含目标机器语言的文件,文件格式随编译环境的不同而不同
包(Package)
当我们使用 Cargo 执行 new 命令创建 Rust 工程时,工程目录下会建立一个 Cargo.toml 文件。工程的实质就是一个包,包必须由一个 Cargo.toml 文件来管理,该文件描述了包的基本信息以及依赖项。
一个包最多包含一个库"箱",可以包含任意数量的二进制"箱",但是至少包含一个"箱"(不管是库还是二进制"箱")。
当使用
cargo new
命令创建完包之后,src 目录下会生成一个 main.rs 源文件,Cargo 默认这个文件为二进制箱的根,编译之后的二进制箱将与包名相同。
模块(Module)
对于一个软件工程来说,我们往往按照所使用的编程语言的组织规范来进行组织,组织模块的主要结构往往是树。Java 组织功能模块的主要单位是类,而 JavaScript 组织模块的主要方式是 function。
这些先进的语言的组织单位可以层层包含,就像文件系统的目录结构一样。
Rust中的组织单位是模块(Module)
访问权限
Rust 中有两种简单的访问权:
公共(public)
和私有(private)
私有
fn 函数名(形参:参数类型){}
公有
pub fn 函数名(形参:参数类型){}
共有的可以被
mod
调用的rs
文件中调用并访问
Rust 错误处理
Rust 有一套独特的处理异常情况的机制,它并不像其它语言中的 try 机制那样简单。
首先,程序中一般会出现两种错误:可恢复错误
和不可恢复错误
。
不可恢复错误
我们已经使用过
println!
同样我们可以是使用panic!
宏的使用方法
fn main() {
panic!("error occured");
println!("Hello, Rust");
}
- 第一行输出了 panic! 宏调用的位置以及其输出的错误信息。
- 第二行是一句提示,翻译成中文就是"通过
RUST_BACKTRACE=1
环境变量运行以显示回溯"。接下来我们将介绍回溯(backtrace)。
新建命令行
如果在 Windows 7 及以上的 Windows 系统版本中,默认使用的终端命令行是 Powershell,请使用以下命令:
$env:RUST_BACKTRACE=1 ; cargo run
如果你使用的是 Linux 或 macOS 等 UNIX 系统,一般情况下默认使用的是 bash 命令行,请使用以下命令:
RUST_BACKTRACE=1 cargo run
可恢复错误
此概念十分类似于 Java 编程语言中的
异常
。实际上在 C 语言中我们就常常将函数返回值设置成整数来表达函数遇到的错误,在 Rust 中通过Result<T, E>
枚举类作返回值来进行异常表达
enum Result<T, E> {
Ok(T),
Err(E),
}
use std::fs::File;
fn openOne(fileName:String) {
let f = File::open(fileName);
match f {
Ok(file) => {
println!("File opened successfully.");
},
Err(err) => {
println!("Failed to open the file.");
}
}
}
fn main(){
openOne(String::from("file.txt"));
}
没有文件则会输出
Failed to open the file.
可恢复的错误的传递
之前所讲的是接收到错误的处理方式,但是如果我们自己编写一个函数在遇到错误时想传递出去怎么办呢?
//定义一个Result<i32, bool>为返回值的函数
fn f(i: i32) -> Result<i32, bool> {
if i >= 0 { Ok(i) }
else { Err(false) }
}
fn main() {
let r = f(10000);
if let Ok(v) = r {
println!("Ok: f(-1) = {}", v);
} else {
println!("Err");
}
}
引入的rs文件
//test.rs
pub fn test1(a:i16, b:i16) -> i16{
return a + b;
}
//在src目录下的rs文件
mod test; //model_name.rs
fn main(){
let sum:i16 = test::test1(10,25);
}
注意:一般
mod
调用只允许出现在main.rs
或lib.rs
中,其他rs文件想要使用则需要把文件放入src/utils
文件中,并调用时得使用use utils::模块rs文件名
。注意这个时候的lib.rs
和main.rs
中就不能调用为mod utils::open_file
或use utils::open_file
了
//当前文件: /src/test.rs
// /src/utils/open_file.rs
use utils::open_file;
pub fn TestError(){
// princ!("error occured");
enum Result<T, E> {
Ok(T),
Err(E),
}
open_file::openOne("file.txt")
}
使用外部的
// 在 Cargo.toml 中添加依赖
[dependencies]
serde = "1.0"
// 在代码中引入
use serde::{Serialize, Deserialize};
创建属于你的库
cargo new my_library --lib