rust学习之一:基本语法
Rust语法学习
rust 安装
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Rust 组织管理
Rust 中有三和重要的组织概念:箱、包、模块。
箱(Crate)
"箱"是二进制程序文件或者库文件,存在于"包"中。
"箱"是树状结构的,它的树根是编译器开始运行时编译的源文件所编译的程序。
注意:"二进制程序文件"不一定是"二进制可执行文件",只能确定是是包含目标机器语言的文件,文件格式随编译环境的不同而不同。
包(Package)
当我们使用 Cargo 执行 new 命令创建 Rust 工程时,工程目录下会建立一个 Cargo.toml 文件。工程的实质就是一个包,包必须由一个 Cargo.toml 文件来管理,该文件描述了包的基本信息以及依赖项。
cargo的使用
1、新工程:cargo new hellow
2、编译: cargo build
3、运行: cargo run
模块(Module)
mod nation {
mod government {
fn govern() {}
}
mod congress {
fn legislate() {}
}
mod court {
fn judicial() {}
}
}
crate::nation::government::govern();
访问权限
Rust 中有两种简单的访问权:公共(public)和私有(private)。
如果想使用公共权限,需要使用 pub 关键字。
对于私有的模块,只有在与其平级的位置或下级的位置才能访问,不能从其外部访问
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();
}
use 关键字
use 关键字能够将模块标识符引入当前作用域:
use 关键字能够将模块标识符引入当前作用域:
实例
mod nation {
pub mod government {
pub fn govern() {}
}
}
use crate::nation::government::govern;
fn main() {
govern();
}
这段程序能够通过编译。
因为 use 关键字把 govern 标识符导入到了当前的模块下,可以直接使用。
这样就解决了局部模块路径过长的问题。
当然,有些情况下存在两个相同的名称,且同样需要导入,我们可以使用 as 关键字为标识符添加别名
mod nation {
pub mod government {
pub fn govern() {}
}
pub fn govern() {}
}
use crate::nation::government::govern;
use crate::nation::govern as nation_govern;
基本语法
Rust 输出文字的方式主要有两种:println!() 和 print!()。
Rust 中格式字符串中的占位符不是"% + 字母"的形式,而是一对 {}
变量
如果要声明变量,需要使用 let 关键字。例如:
let a = 123;
变量变得"可变"(mutable)只需一个 mut 关键字。
let mut a = 123;
a = 456;
Rust 有自动判断类型的功能,但有些情况下声明类型更加方便:
let a: u64 = 123;
但是如果 a 是常量就不合法:
const a: i32 = 123;
let a = 456;
重影(Shadowing)
重影就是指变量的名称可以被重新使用的机制:
重影是指用同一个名字重新代表另一个变量实体,其类型、可变属性和值都可以变化。但可变变量赋值仅能发生值的变化。
数据类型
位长度 |
有符号 |
无符号 |
8-bit |
i8 |
u8 |
16-bit |
i16 |
u16 |
32-bit |
i32 |
u32 |
64-bit |
i64 |
u64 |
128-bit |
i128 |
u128 |
arch |
isize |
usize |
isize 和 usize 两种整数类型是用来衡量数据大小的,它们的位长度取决于所运行的目标平台,如果是 32
浮点数型(Floating-Point
let x = 2.0; // f64
let y: f32 = 3.0; // f32
注意:Rust 不支持 ++ 和 --
复合类型
元组用一对 ( ) 包括的一组数据,可以包含不同种类的数据:
实例
let tup: (i32, f64, u8) = (500, 6.4, 1);
// tup.0 等于 500
// tup.1 等于 6.4
// tup.2 等于 1
let (x, y, z) = tup;
// y 等于 6.4
数组用一对 [ ] 包括的同类型数据。
实例
let a = [1, 2, 3, 4, 5];
// a 是一个长度为 5 的整型数组
a[0] = 123; // 错误:数组 a 不可变
let mut a = [1, 2, 3];
a[0] = 4; // 正确
Rust 注释
Rust 中的注释方式与其它语言(C、Java)一样,支持两种注释方式:
实例
// 这是第一种注释方式
/* 这是第二种注释方式 */
/*
* 多行注释
* 多行注释
* 多行注释
*/
在这种规则下,三个反斜杠 /// 依然是合法的注释开始。所以 Rust 可以用 /// 作为说明文档注释的开头:
实例
/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let x = add(1, 2);
///
/// ```
函数体的语句和表达式
fn main() {
let x = 5;
let y = { // 函数体表达式,注意末尾不能有分号
let x = 3;
x + 1
};
println!("x 的值为 : {}", x);
println!("y 的值为 : {}", y);
}
{
let x = 3;
x + 1 // x + 1 之后没有分号,否则它将变成一条语句
};
Rust 条件语句
Rust 中的 if 不存在单语句不用加 {} 的规则,不允许使用一个语句代替一个块
if a > 0 {
b = 1;
}
else if a < 0 {
b = -1;
}
else {
b = 0;
}
let number = if a > 0 { 1 } else { -1 }; 运算表达式 (A ? B : C)
Rust 循环
while number != 4 {
println!("{}", number);
number += 1;
}
在 C 语言中 for 循环使用三元语句控制循环,但是 Rust 中没有这种用法,需要用 while 循环来代替:
int i;
for (i = 0; i < 10; i++) {
// 循环体
}
Rust
let mut i = 0;
while i < 10 {
// 循环体
i += 1;
}
let a = [10, 20, 30, 40, 50];
for i in a.iter() {
let a = [10, 20, 30, 40, 50];
for i in 0..5 {
println!("a[{}] = {}", i, a[i]);
}
loop 循环
loop {
let ch = s[i];
if ch == 'O' {
break;
}
Rust 所有权
1)变量超出作用域会自动释放。对于简单值类型的栈内存(如int,struct)超出作用域后自动释放,这个逻辑在各个语言都有实现。而对于 new 出来的堆内存,在c/c++中是要手动释放的,在java和dotnet中要委托垃圾回收释放或手动写 dispose 语句释放。而垃圾回收不是实时的,会影响性能。而释放语句总会有懒人忘记写的。而 Rust 对栈内存和堆内存一视同仁,超出作用域一律自动释放。Rust 的这个特点在兼顾性能的情况下、有效的减少了代码量和内存泄漏隐患。
(2) “所有权” :某段内存只能被最后的变量名所有,前面声明过的变量都作废,这有效的避免被多个变量释放的问题,而且该操作是在编译期就可以检查到的,这策略可在编译期就能有效的避免空指针问题。
内存和分配
移动
多个变量可以在 Rust 中以不同的方式与相同的数据交互:
let x = 5;
let y = x;
,仅在栈中的数据的"移动"方式是直接复制
但如果发生交互的数据在堆中就是另外一种情况:
为了确保安全,在给 s2 赋值时 s1 已经无效了。没错,在把 s1 的值赋给 s2 以后 s1 将不可以再被使用。下面这段程序是错的:
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // 错误!s1 已经失效
变量定义不用一定在函数开头
引用与租借
引用(Reference)是 C++ 开发者较为熟悉的概念。
如果你熟悉指针的概念,你可以把它看作一种指针。
实质上"引用"是变量的间接访问方式。
实例
fn main() {
let s1 = String::from("hello");
let s2 = &s1;
println!("s1 is {}, s2 is {}", s1, s2);
}
运行结果:
s1 is hello, s2 is hello
实例
fn main() {
let s1 = String::from("hello");
let s2 = &s1;
let s3 = s1;
println!("{}", s2);
}
这段程序不正确:因为 s2 租借的 s1 已经将所有权移动到 s3,所以 s2 将无法继续租借使用 s1 的所有权。如果需要使用 s2 使用该值,必须重新租借:
实例
fn main() {
let s1 = String::from("hello");
let mut s2 = &s1;
let s3 = s2;
s2 = &s3; // 重新从 s3 租借所有权
println!("{}", s2);
}
这段程序是正确的。
既然引用不具有所有权,即使它租借了所有权,它也只享有使用权(这跟租房子是一个道理)。
如果尝试利用租借来的权利来修改数据会被阻止:
Rust Slice(切片)类型
字符串
fn main() {
let mut s = String::from("runoob");
let slice = &s[0..3];
s.push_str("yes!"); // 错误 s 被部分引用,禁止更改其值。
println!("slice = {}", slice);
}
非字符串切片
除了字符串以外,其他一些线性数据结构也支持切片操作,例如数组:
实例
fn main() {
let arr = [1, 3, 5, 7, 9];
let part = &arr[0..3];
for i in part.iter() {
println!("{}", i);
}
}
结构体定义
这是一个结构体定义:
struct Site {
domain: String,
name: String,
nation: String,
found: u32
}
Rust 很多地方受 JavaScript 影响,在实例化结构体的时候用 JSON 对象的 key: value 语法来实现定义:
实例
let runoob = Site {
domain: String::from("www.runoob.com"),
name: String::from("RUNOOB"),
nation: String::from("China"),
found: 2013
};
你想要新建一个结构体的实例,其中大部分属性需要被设置成与现存的一个结构体属性一样,仅需更改其中的一两个字段的值,可以使用结构体更新语法:S
let site = Site {
domain: String::from("www.runoob.com"),
name: String::from("RUNOOB"),
..runoob
};
元组结构体
有一种更简单的定义和使用结构体的方式:元组结构体
元组是不同类型的值的集合
struct Color(u8, u8, u8);
struct Point(f64, f64);
let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);
struct Rectangle {
width: u32,
height: u32,
length: u16,
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 , length:12};
println!("rect1 is {:?}", rect1);
}
rect1 is Rectangle { width: 30, height: 50, length: 12 }
println!("rect1 is {:#?}”, rect1);
rect1 is Rectangle {
width: 30,
height: 50,
length: 12,
}
结构体方法
方法(Method)和函数(Function)类似,只不过它是用来操作结构体实例的。
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn wider(&self, rect: &Rectangle) -> bool {
self.width > rect.width
}
}
fn main() {
let rect1 = Rectangle { width: 30, height: 50 };
println!("rect1's area is {}", rect1.area());
}
结构体关联函数
之所以"结构体方法"不叫"结构体函数"是因为"函数"这个名字留给了这种函数:它在 impl 块中却没有 &self 参数。
#[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);
}
枚举
enum Book {
Papery(u32),
Electronic(String),
}
enum Book {
Papery { index: u32 },
Electronic { url: String },
}
let book = Book::Papery{index: 1001};
match语法 替代switch case
enum Book {
Papery(u32),
Electronic {url: String},
}
let book = Book::Papery(1001);
match book {
Book::Papery(i) => {
println!("{}", i);
},
Book::Electronic { url } => {
println!("{}", url);
}
}
Option 枚举类
Option 是 Rust 标准库中的枚举类,这个类用于填补 Rust 不支持 null 引用的空白。
fn main() {
let opt = Option::Some("Hello");
match opt {
Option::Some(something) => {
println!("{}", something);
},
Option::None => {
println!("opt is nothing");
}
}
}
let opt: Option<&str> = Option::None;
if let 语法
实例
let i = 0;
match i {
0 => println!("zero"),
_ => {},
}
引用标准库
Rust 官方标准库字典:https://doc.rust-lang.org/stable/std/all.html
在学习了本章的概念之后,我们可以轻松的导入系统库来方便的开发程序了:
实例
use std::f64::consts::PI;
fn main() {
println!("{}", (PI / 2.0).sin());
}
运行结果:
1
错误处理
程序中一般会出现两种错误:可恢复错误和不可恢复错误。
可恢复错误的典型案例是文件访问错误,如果访问一个文件失败,有可能是因为它正在被占用,是正常的,我们可以通过等待来解决。
但还有一种错误是由编程中无法解决的逻辑错误导致的,例如访问数组末尾以外的位置。
大多数编程语言不区分这两种错误,并用 Exception (异常)类来表示错误。在 Rust 中没有 Exception。
对于可恢复错误用 Result<T, E> 类来处理,对于不可恢复错误使用 panic! 宏来处理。
fn main() {
panic!("error occured");
println!("Hello, Rust");
}
可恢复的错误
enum Result<T, E> {
Ok(T),
Err(E),
}
如果想使一个可恢复错误按不可恢复错误处理,Result 类提供了两个办法:unwrap() 和 expect(message: &str) :
kind 方法
到此为止,Rust 似乎没有像 try 块一样可以令任何位置发生的同类异常都直接得到相同的解决的语法,但这样并不意味着 Rust 实现不了:我们完全可以把 try 块在独立的函数中实现,将所有的异常都传递出去解决。实际上这才是一个分化良好的程序应当遵循的编程方法:应该注重独立功能的完整性。
但是这样需要判断 Result 的 Err 类型,获取 Err 类型的函数是 kind()。
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");
}
}
}
}
}
Rust 泛型与特性
fn max<T>(array: &[T]) -> T {
let mut max_index = 0;
let mut i = 1;
while i < array.len() {
if array[i] > array[max_index] {
max_index = i;
}
i += 1;
}
array[max_index]
}
结构体与枚举类中的泛型
在之前我们学习的 Option 和 Result 枚举类就是泛型的。
Rust 中的结构体和枚举类都可以实现泛型机制。
struct Point<T> {
x: T,
y: T
}
但不允许出现类型不匹配的情况如下:
let p = Point {x: 1, y: 2.0};
特性
特性(trait)概念接近于 Java 中的接口(Interface),但两者不完全相同。特性与接口相同的地方在于它们都是一种行为规范,可以用于标识哪些类有哪些方法。
特性在 Rust 中用 trait 表示:
trait Descriptive {
fn describe(&self) -> String;
}
Rust 集合与字符串
集合(Collection)是数据结构中最普遍的数据存放形式,Rust 标准库中提供了丰富的集合类型帮助开发者处理数据结构的操作。
向量
向量(Vector)是一个存放多值的单数据结构,该结构将相同类型的值线性的存放在内存中。
向量是线性表,在 Rust 中的表示是 Vec<T>。
向量的使用方式类似于列表(List),我们可以通过这种方式创建指定类型的向量:
let vector: Vec<i32> = Vec::new(); // 创建类型为 i32 的空向量
let vector = vec![1, 2, 4, 8]; // 通过数组创建向量
我们使用线性表常常会用到追加的操作,但是追加和栈的 push 操作本质是一样的,所以向量只有 push 方法来追加单个元素:
append 方法用于将一个向量拼接到另一个向量的尾部:
实例
fn main() {
let mut v1: Vec<i32> = vec![1, 2, 4, 8];
let mut v2: Vec<i32> = vec![16, 32, 64];
v1.append(&mut v2);
println!("{:?}", v1);
}
字符串
let one = 1.to_string(); // 整数到字符串
let float = 1.3.to_string(); // 浮点数到字符串
let slice = "slice".to_string(); // 字符串切片到字符串
字符串追加:
let mut s = String::from("run");
s.push_str("oob"); // 追加字符串切片
s.push('!'); // 追加字符
用 + 号拼接字符串:
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;
这个语法也可以包含字符串切片:
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;
使用 format! 宏:
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);
字符串长度:
let s = "hello";
let len = s.len();
映射表
映射表(Map)在其他语言中广泛存在。其中应用最普遍的就是键值散列映射表(Hash Map)。
新建一个散列值映射表:
实例
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert("color", "red");
map.insert("size", "10 m^2");
println!("{}", map.get("color").unwrap());
}
如果已经存在相同的键,会直接覆盖对应的值
就是在确认当前不存在某个键时才执行的插入动作,可以这样:
map.entry("color").or_insert("red");
在已经确定有某个键的情况下如果想直接修改对应的值,有更快的办法:
实例
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, "a");
if let Some(x) = map.get_mut(&1) {
*x = "b";
}
}