rust学习笔记之基础一:安装运行、Cargo、原生数据类型
Rust 是一门注重安全(safety)、速度(speed)和并发(concurrency)的现代系统编程语言。Rust 通过内存安全来实现以上目标,但不使用垃圾回收机制(garbage collection, GC)。
安装
检查是否正确安装了 Rust
rustc --version
编写程序 main.rs
// 主函数
fn main() {
println!("Hello, world!");// println! 调用 Rust 宏
}
编译
rustc main.rs
运行生成的可执行文件
./main
Cargo
cargo 是官方的 Rust 包管理工具。 它有很多非常有用的功能来提高代码质量和开发人员的开发效率! 这些功能包括:
- 依赖管理和与 crates.io(官方 Rust 包注册服务)集成
- 方便的单元测试
- 方便的基准测试
Cargo 是 Rust 的构建系统和包管理器。
检查版本号
cargo --version
使用 Cargo 创建项目
# 二进制可执行文件
cargo new hello_cargo
# 或者库
cargo new --lib hello_cargo
我们选择创建二进制可执行文件而不是库,但所有的概念都是相同的。得到下面的目录结构:
hello_cargo
├── Cargo.toml
└── src
└── main.rs
Cargo.toml 是 Cargo 配置文件。内容如下:
- [package],是一个表块标题,表明下面的语句用来配置一个包。
- package 下面的 name 字段表明项目的名称。 如果您发布 crate,那么 crates.io 将使用此字段标明的名称。 这也是编译时输出的二进制可执行文件的名称。
- version 字段是使用语义版本控制的 crate 版本号。
- edition 字段是使用的 Rust 大版本号
- authors 字段表明发布 crate 时的作者列表。
- [dependencies] 是罗列项目依赖的表块的开始。
[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"
authors = ["mark"]
[dependencies]
clap = "2.27.1" # 来自 crates.io
rand = { git = "https://github.com/rust-lang-nursery/rand" } # 来自网上的仓库
bar = { path = "../bar" } # 来自本地文件系统的路径
Cargo 期望源文件存放在 src 目录中。项目根目录只存放说明文件(README)、许可协议(license)信息、配置文件和其他跟代码无关的文件。
main.rs 是新项目的入口源文件。
添加其它的二进制文件
foo
├── Cargo.toml
└── src
├── main.rs
└── bin
└── my_other_bin.rs
为了使得 cargo 编译或运行这个二进制可执行文件而不是默认或其他二进制可执行文件,我们只需给 cargo 增加一个参数 --bin my_other_bin
构建项目
cargo build
- 这个命令会创建一个可执行文件 target/debug/hello_cargo (在 Windows 上是 target\debug\hello_cargo.exe)。
- 首次运行 cargo build 时,也会使 Cargo 在项目根目录创建一个新文件:Cargo.lock。这个文件记录项目依赖的实际版本。
构建并运行
cargo run
快速检查代码确保其可以编译
cargo check
- 并不产生可执行文件
- 通常
cargo check要比cargo build快得多,因为它省略了生成可执行文件的步骤。
测试
在代码目录组织上,我们可以将单元测试放在需要测试的模块中,并将集成测试放在源码中 tests/ 目录中:
foo
├── Cargo.toml
├── src
│ └── main.rs
└── tests
├── my_test.rs
└── my_other_test.rs
tests 目录下的每个文件都是一个单独的集成测试。
cargo 很自然地提供了一种便捷的方法来运行所有测试!
cargo test
运行一些测试,其中名称匹配一个模式:
cargo test test_foo
发布构建
cargo build --release
- 优化编译项目,会在 target/release 而不是 target/debug 下生成可执行文件。
- 这些优化可以让 Rust 代码运行的更快,不过启用这些优化也需要消耗更长的编译时间。
构建所有本地依赖提供的文档,并在浏览器中打开
cargo doc --open
原生数据类型
Rust 的每个值都有确切的数据类型(data type),该类型告诉 Rust 数据是被指定成哪类数据,从而让 Rust 知道如何使用该数据。
Rust 是一种静态类型(statically typed)的语言,这意味着它必须在编译期知道所有变量的类型。编译器通常可以根据值和使用方式推导出我们想要使用的类型。在类型可能是多种情况时,我们必须加上一个类型标注。
let guess: u32 = "42".parse().expect("Not a number!");
标量类型
标量(scalar)类型表示单个值。Rust 有 4 个基本的标量类型:整型、浮点型、布尔型和字符。
整型
整型默认是 i32。
isize 和 usize 类型取决于程序运行的计算机体系结构,若使用 64 位架构系统则为 64 位,若使用 32 位架构系统则为 32 位。
- 无符号类型: u8、u16、u32、u64、u128、usize
- 有符号类型: i8、i16、i32、i64、i128、isize
let x1=57u8; // 可能属于多种数字类型的数字字面量允许使用类型后缀来指定类型
let x2=1_000; // _作为可视分隔符以方便读数
let x3=0xff; // 十六进制
let x4=0o77; // 八进制
let x5=0b1111_0000; // 二进制
let x6=b'A'; // 字节 (仅限于 u8)
浮点类型
浮点数(floating-point number)是带有小数点的数字,在 Rust 中浮点类型(简称浮点型)数字也有两种基本类型。Rust 的浮点型是 f32 和 f64,它们的大小分别为 32 位和 64 位。
默认浮点类型是 f64,所有浮点型都是有符号的。
let x = 2.0; // f64
let y: f32 = 3.0; // f32
字面量
整数 1、浮点数 1.2、字符 'a'、字符串 "abc"、布尔值 true 和单元类型 () 可以用数字、文字或符号之类的 “字面量”(literal)来表示。另外,通过加前缀 0x、0o、0b,数字可以用十六进制、八进制或二进制记法表示。为了改善可读性,可以在数值字面量中插入下划线,比如:1_000 等同于 1000,0.000_001 等同于 0.000001。
数字运算
let sum = 5 + 10;
let difference = 95.5 - 4.3;
let product = 4 * 30;
let quotient = 56.7 / 32.2;
let floored = 2 / 3; // 结果0 ,向下取整
let remainder = 43 % 5; // 取模
Rust 可以根据上下文来推断(infer)类型(比如一个未声明类型整数和 i64 的整数相加,则该整数会自动推断为 i64 类型。仅当根据环境无法推断时,才按默认方式取整型数值为 i32,浮点数值为 f64)。
布尔类型
let t = true;
let f: bool = false;
字符类型
Rust 的字符类型 char 采用单引号括起来,大小为 4 个字节,表示的是一个 Unicode 标量值,这意味着它可以表示的远远不止是 ASCII。标音字母,中文/日文/韩文的文字,emoji,还有零宽空格(zero width space)在 Rust 中都是合法的字符类型。
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '😻';
单元类型
单元类型(unit type)。其唯一可能的值就是 () 这个空元组
尽管单元类型的值是个元组,它却并不被认为是复合类型,因为并不包含多个值。
复合类型
复合类型(compound type)可以将多个值组合成一个类型。Rust 有两种基本的复合类型:元组(tuple)和数组(array)。
元组类型
元组是一个可以包含各种类型值的组合。元组使用括号 () 来构造(construct),而每个元组自身又是一个类型标记为 (T1, T2, ...) 的值,其中 T1、T2 是每个元素的类型。
函数可以使用元组来返回多个值,因为元组可以拥有任意多个值。
元组是将多种类型的多个值组合到一个复合类型中的一种基本方式。元组的长度是固定的:声明后,它们就无法增长或缩小。
let tup: (i32, f64, u8) = (500, 6.4, 1);
let tup = (500, 6.4, 1);
let (x, y, z) = tup; // 解构
let x=tup.0; // 使用索引访问元祖元素
let pair = (1, true);
println!("{:?}", pair);
// 创建单元素元组需要一个额外的逗号,这是为了和被括号包含的字面量作区分。
println!("one element tuple: {:?}", (5u32,));
println!("just an integer: {:?}", (5u32));
数组类型
数组(array)是一组拥有相同类型 T 的对象的集合,在内存中是连续存储的,支持通过索引快速访问。数组使用中括号 [] 来创建,且它们的大小在编译时会被确定。数组的类型标记为 [T; length](T 为元素类型,length 表示数组大小)。
数组的每个元素必须具有相同的类型。Rust 中的数组具有固定长度。
use std::mem;
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];// 定长数组(类型标记是多余的)
let a = [1, 2, 3, 4, 5];
println!("{}",a.len()); // `len` 返回数组的大小
let a = [3; 5]; // 所有元素可以初始化成相同的值,包含 5 个元素,这些元素的值为 3
let first = a[0]; // 访问数组元素
let second = a[1];
// 数组是在栈中分配的
println!("array occupies {} bytes", mem::size_of_val(&a));
}
类型系统
类型转换
Rust 不提供原生类型之间的隐式类型转换,但可以使用 as 关键字进行显式类型转换(casting)。
// 不显示类型转换产生的溢出警告。
#![allow(overflowing_literals)]
fn main() {
let decimal = 65.4321_f32;
// 错误!不提供隐式转换
// let integer: u8 = decimal;
// 可以显式转换
let integer = decimal as u8;
let character = integer as char;
println!("Casting: {} -> {} -> {}", decimal, integer, character); // Casting: 65.4321 -> 65 -> A
// 当把任何类型转换为无符号类型 T 时,会不断加上或减去 (std::T::MAX + 1)
// 直到值位于新类型 T 的范围内。
// 1000 已经在 u16 的范围内
println!("1000 as a u16 is: {}", 1000 as u16); // 1000 as a u16 is: 1000
// 1000 - 256 - 256 - 256 = 232
// 事实上的处理方式是:从最低有效位(LSB,least significant bits)开始保留
// 8 位,然后剩余位置,直到最高有效位(MSB,most significant bit)都被抛弃。
// MSB 就是二进制的最高位,LSB 就是二进制的最低位,按日常书写习惯就是最左边一位和最右边一位。
println!("1000 as a u8 is : {}", 1000 as u8); // 1000 as a u8 is : 232
// -1 + 256 = 255
println!(" -1 as a u8 is : {}", (-1i8) as u8); // -1 as a u8 is : 255
// 对正数,这就和取模一样。
println!("1000 mod 256 is : {}", 1000 % 256); // 1000 mod 256 is : 232
// 当转换到有符号类型时,(位操作的)结果就和 “先转换到对应的无符号类型,
// 如果 MSB 是 1,则该值为负” 是一样的。
// 当然如果数值已经在目标类型的范围内,就直接把它放进去。
println!(" 128 as a i16 is: {}", 128 as i16); // 128 as a i16 is: 128
// 128 转成 u8 还是 128,但转到 i8 相当于给 128 取八位的二进制补码,其值是:
println!(" 128 as a i8 is : {}", 128 as i8); // 128 as a i8 is : -128
// 重复之前的例子
// 1000 as u8 -> 232
println!("1000 as a u8 is : {}", 1000 as u8); // 1000 as a u8 is : 232
// 232 的二进制补码是 -24
println!(" 232 as a i8 is : {}", 232 as i8); // 232 as a i8 is : -24
}
字面量
对数值字面量,只要把类型作为后缀加上去,就完成了类型说明。比如指定字面量 42 的 类型是 i32,只需要写 42i32。
无后缀的数值字面量,其类型取决于怎样使用它们。如果没有限制,编译器会对整数使用 i32,对浮点数使用 f64。
fn main() {
// 带后缀的字面量,其类型在初始化时已经知道了。
let x = 1u8;
let y = 2u32;
let z = 3f32;
// 无后缀的字面量,其类型取决于如何使用它们。
let i = 1;
let f = 1.0;
// `size_of_val` 返回一个变量所占的字节数
println!("size of `x` in bytes: {}", std::mem::size_of_val(&x)); // size of `x` in bytes: 1
println!("size of `y` in bytes: {}", std::mem::size_of_val(&y)); // size of `y` in bytes: 4
println!("size of `z` in bytes: {}", std::mem::size_of_val(&z)); // size of `z` in bytes: 4
println!("size of `i` in bytes: {}", std::mem::size_of_val(&i)); // size of `i` in bytes: 4
println!("size of `f` in bytes: {}", std::mem::size_of_val(&f)); // size of `f` in bytes: 8
}
类型推断
Rust 的类型推断引擎是很聪明的,它不只是在初始化时看看右值(r-value)的 类型而已,它还会考察变量之后会怎样使用,借此推断类型。这是一个类型推导的进阶例子:
fn main() {
// 因为有类型说明,编译器知道 `elem` 的类型是 u8。
let elem = 5u8;
// 创建一个空向量(vector,即不定长的,可以增长的数组)。
let mut vec = Vec::new();
// 现在编译器还不知道 `vec` 的具体类型,只知道它是某种东西构成的向量(`Vec<_>`)
// 在向量中插入 `elem`。
vec.push(elem); // 现在编译器知道 `vec` 是 u8 的向量了(`Vec<u8>`)。
println!("{:?}", vec);
}
别名
可以用 type 语句给已有的类型取个新的名字。类型的名字必须遵循驼峰命名法(像是 CamelCase 这样),否则编译器将给出警告。原生类型是例外,比如: usize、f32,等等。
type NanoSecond = u64;
type Inch = u64;
fn main() {
let nanoseconds: NanoSecond = 5 as NanoSecond;
let inches: Inch = 2 as Inch;
println!("{} nanoseconds + {} inches = {} unit?",
nanoseconds,
inches,
nanoseconds + inches);
}
别名的主要用途是避免写出冗长的模板化代码(boilerplate code)。如 IoResult<T> 是 Result<T, IoError> 类型的别名。
浙公网安备 33010602011771号