02-基本数据类型及打印
本文内容摘编自: 快速入门:Rust中有哪些你不得不了解的基础语法?(著作权归AI悦创所有) Rust语言圣经(Rust Course)-字符串与切片
0. Hello, World
创建项目后,编写代码打印出“Hello, World!”字样
fn main() {
println!("Hello, world!");
}
计算机传统⬆️
使用 cargo build
来编译。
$ cargo build
Compiling helloworld v0.1.0 (/home/mike/works/classes/helloworld)
Finished dev [unoptimized + debuginfo] target(s) in 1.57s
使用 cargo run
命令可以直接运行程序。
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `target/debug/helloworld`
Hello, world!
可以看到,最后终端打印出了 Hello, world。我们成功地执行了第一个 Rust 程序。
命名
Rust中合法的标识符命名(包括变量名、函数名、triat名等)必须由数字、字母、下划线组成,而且不能以数字开头。这个和很多语言都是一样的。
Rust将来也会允许**其他Unicode**字符作为标识符
1.数据类型
在声明一个单一类型变量(标量、字符串、数组)的时候,不用声明它的类型,rust的会根据输入的内容自动分配一个类型,如果不想要他分配的默认类型,可以在生变量后使用冒号加数据类型的形式标明该数据的类型。
Rust 的数据类型主要分为两大类:标量(Scalar)类型和复合(Compound)类型。以下是一个概览表格:
类别 | 数据类型 | 描述 |
---|---|---|
标量 | 整型 | 包括 i8 、i16 、i32 、i64 、i128 、isize (有符号整型)和 u8 、u16 、u32 、u64 、u128 、usize (无符号整型) |
标量 | 浮点型 | 包括 f32 和 f64 |
标量 | 布尔型 | bool 类型,它的值可以是 true 或 false |
标量 | 字符型 | char 类型,表示单个 Unicode 字符; |
特殊 | [字符串类型](#_5. 字符串String ) | 如 srting 它是一个**动态大小的字符串类型(必需都是**字符型) |
复合 | 切片 | 切片(Slice)类型,引用集合的一部分数据; 如 &str 是一个总是指向有效 UTF-8 序列的字符串切片 |
复合 | 数组 | 数组(Array)类型,所有元素都必须是相同类型 |
特殊 | 向量(**动态**数组) | 向量(Vector)类型,所有元素都必须是相同类型,非固定值 用Vec<T> 或Vec! 宏声明 |
复合 | 哈希表 | 哈希表(HashMap)类型,拥有索引“关键字”Key-words 和其对应的“值”Value |
复合 | 元组 | 元组(Tuple)类型,可以包含多种类型的几个值,固定值 |
复合 | 结构体 | 自定义数据类型,允许命名和包装多个相关值 |
复合 | 枚举 | 枚举(Enum)类型,用于定义通过多个具体变量来表示的类型 |
复合 | 引用 | 引用(Reference) 类型,借用数据,但是不变更所有权 |
特殊 | 指针 | 包括裸指针 *const T 和 *mut T ,不建议直接使用。注意和智能指针如Arc<T> 等区分 |
特殊 | 函数 | 函数也是一种类型,可以通过函数签名来指定 除函数外,函数式编程 将介绍其他类似函数的结构,如**闭包** |
特殊 | 单元类型 | () ,它是一个空**元组**,也称为单元类型 |
- 切片(Slice):引用一个数组的部分数据。与数组不同,切片的长度不需要在编译时确定,提供了一种灵活处理数组的方法。
- 引用(Reference):引用允许你以不拥有数据的方式借用值,它们本身并不拥有它们所指向的值。在 Rust 中,引用的使用遵循严格的借用规则,以确保内存安全。
- 指针(pointer):允许直接地址访问,但绕过了 Rust 的所有权和借用规则,因此使用它们可能会造成内存安全问题。它们主要用于与 C 语言库的交互或其他高级场景。相比之下,普通的引用(
&T
和&mut T
)和智能指针(如Box<T>
、Rc<T>
和Arc<T>
)在 Rust 中的使用更加安全和常见。
First. 标量
1. 整数
与一些动态语言不同,Rust 中的数字类型是区分位数的。
举例
fn main() {
let a = 1;
let b: i32 =-15;
let c: i8 = -15;
}
注意
类型写在变量名的后面
可以先不赋值,但是此时必须声明类型
let x: i32;
// println!("x has the Value {}", x); !`x` isn't initialized `
2. 浮点数
浮点数有两种类型:f32 和 f64,分别代表 32 位浮点数类型和 64 位浮点数类型。它们也可以跟在字面量的后面,用来指定浮点数值的类型,比如
let a: f32 = 10.0
let a = 10.0f32
该语句定义了一个变量 a,初始化成 32 位浮点数类型,值为 10.0。
不定义则默认f64类型
3. 布尔类型
Rust 中的布尔类型为bool
,它只有两个值,true
和 false
。
let a = true;
let b: bool = false;
4. 字符
Rust 中的字符类型是 char,值用单引号括起来。
fn main() {
let c = 'c';
let z: char = 'ℤ';
let kristalball = '🔮';
let t = '中';
}
Rust 的 char 类型是 Unicode 散列值。,在内存中总是占用 4 个字节大小。这一点与 C 语言或其他某些语言中的 char 有很大不同。 这意味着它可以表达各种符号,比如中文符号、emoji 符号等。
可以看到,char
在rust中只看字符占位,不看字节多少。char的内存占用甚至超过了的内存占用
Second. UTF-8编码的字符串
Rust 中有两种字符串类型:String
和 &str
。
虽然中文表述上,字符串只比前面的字符类型多了一个串字,但它们内部存储结构完全不同。String 内部存储的是 Unicode 字符串的 UTF8 编码,而 char 直接存的是 Unicode Scalar Value
。也就是说 String 不是 char 的数组,这点与 C 语言也有很大区别。
5. 字符串String
String
赋值方式是
x = String::from() //注意大写
通过下面示例我们可以看到,Rust 字符串对 Unicode 字符集有着良好的支持。
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שָׁלוֹם");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
let hello = String::from("你好");
字符串切片&str
&str
本质是一种切片Slice
。切片详见 04-切片与引用。其中字符也由UTF-8编码。它既单独输出一些内容,也可以截取String
中的内容。
- 语法
let hello = ""
或者:截取
let s = String::from("可爱的人");
let slice = &s[0..2];
字符串的转义
与 C 语言一样,Rust 中转义符号也是反斜杠 \
,可用来转义各种字符。
fn main() {
/* 将""号进行转义 */
let byte_escape = "I'm saying \"Hello\"";
println!("{}", byte_escape);
/* 分两行打印(转义换行符) */
let byte_escape = "I'm saying \n 你好";
println!("{}", byte_escape);
/* Windows下的换行符 */
let byte_escape = "I'm saying \r\n 你好";
println!("{}", byte_escape);
/* 打印出 \ 本身 */
let byte_escape = "I'm saying \\ Ok";
println!("{}", byte_escape);
}
/* 强行在字符串后面加个0,与C语言的字符串一致。(打印不会显示)*/
let byte_escape = "I'm saying hello.\0";
println!("{}", byte_escape);
除此之外,Rust 还支持通过 \x
输入等值的 ASCII
字符,以及通过 \u{}
输入等值的 Unicode 字符。
fn main() {
// 使用 \x 输入等值的ASCII字符(最高7位)
let byte_escape = "I'm saying hello \x7f";
println!("{}", byte_escape);
// 使用 \u{} 输入等值的Unicode字符(最高24位)
let byte_escape = "I'm saying hello \u{0065}";
println!("{}", byte_escape);
}
注:字符串转义的详细知识点,请参考 Tokens - The Rust Reference (rust-lang.org)
不转义字符串
使用 r""
或 r#""#
把字符串字面量套起来,输出原始字面量。
fn main() {
// r""
let raw_str = r"Escapes don't work here: \x3F \u{211D}";
println!("{}", raw_str);
// 我们希望在字符串字面量里面保留 "", 则使用r# #这种形式,
let quotes = r#"And then I said: "There is no escape!""#;
println!("{}", quotes);
// 如果遇到字面量里面有#号的情况,可以在r后面,前后分别增加相同数量的#号,直到比连续引用的#多
let longer_delimiter = r###"A string with "#" in it.
And even "##""###;
println!("{}", longer_delimiter);
}
一点小提示,Rust 中的字符串字面量都支持换行写,默认把换行符包含进去。
- 输出
Third(6).数组
i.字节串
很多时候,我们的字符串字面量中用不到 Unicode 字符,只需要 ASCII 字符集。对于这种情况,Rust 还有一种更紧凑的表示法:字节串。用 b“ ”
,比如 b"this is a byte string"
。这时候字符串的类型已不是字符串,而是字节的数组 [u8; N]
,N
为字节数。
fn main() {
//
let bytestring = b"this is a byte string";
// println!("A byte string: {}", bytestring);
// ^^^^^^^^^^ `[u8; 21]` cannot be formatted with the default formatter
println!("A byte string: {:?}", bytestring);
//“13“表示声明长度,数组类型必须固定长度
let escaped: &[u8; 13] = b"\x52\x75\x73\x74 as bytes";
println!("Some escaped bytes: {:?}", escaped);
// 字节串与原始字面量结合使用
let raw_bytestring = br"\u{211D} is not escaped here";
println!("{:?}", raw_bytestring);
}
字节串当中表示的字节,不能被直接打印,所以使用“{:?}”而不是“{}”
将打印这些字符的ASCII编码
字节串很有用,特别是在做系统级编程或网络协议开发的时候,经常会用到。
ii. 一般数组
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
let a = [1, 2, 3, 4, 5];
}
Rust 中的数组是固定长度的,也就是说在编译阶段就能知道它占用的字节数,并且在运行阶段,不能改变它的长度(尺寸)。
是否能在编译期计算出某个数据类型在运行过程中占用内存空间的大小,这个指标很重要,Rust 的类型系统就是按这个对类型进行分类的。后面的课程中我们会经常用到这个指标。
数组常用于开辟一个固定大小的 Buffer(缓冲区),用来接收 IO 输入输出等。也常用已知元素个数的字面量集合来初始化,比如表达一年有 12 个月。
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];
数组的访问,可以用下标索引。
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
let b = a[0];
println!("{}", b)
}
// 输出
1
我们再来看看,如果下标索引越界了会发生什么。
fn main() {
let a: [i32; 5] = [1, 2, 3, 4, 5];
let b = a[5];
println!("{}", b)
}
提示:
Compiling playground v0.0.1 (/playground)
error: this operation will panic at runtime
--> src/main.rs:3:13
|
3 | let b = a[5];
| ^^^^ index out of bounds: the length is 5 but the index is 5
这时候你可能已经发现了,Rust 在编译的时候,就给我们指出了问题,说这个操作会在运行的时候崩溃。为什么 Rust 能指出来呢?就是因为数组的长度是确定的,Rust 在编译时就分析并提取了这个数组类型占用空间长度的信息,因此直接阻止了你的越界访问。
不得不说,Rust 太贴心了。
7. 动态数组 Vec
Rust 中的动态数组类型是 Vec(Vector),也就是向量,中文翻译成动态数组。它用来存储同一类型的多个值,容量可在程序运行的过程中动态地扩大或缩小,因此叫做动态数组。
fn main() {
let v: Vec<i32> = Vec::new();
let v = vec![1, 2, 3];
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
}
动态数组可以用下标进行索引访问。
比如:
fn main() {
let s1 = String::from("superman 1");
let s2 = String::from("superman 2");
let s3 = String::from("superman 3");
let s4 = String::from("superman 4");
let v = vec![s1, s2, s3, s4];
println!("{:?}", v[0]);
}
// 输出
"superman 1"
如果我们下标越界了会发生什么?Rust 能继续帮我们提前找出问题来吗?试一试就知道了。
fn main() {
let s1 = String::from("superman 1");
let s2 = String::from("superman 2");
let s3 = String::from("superman 3");
let s4 = String::from("superman 4");
let v = vec![s1, s2, s3, s4];
// 这里下标访问越界了
println!("{:?}", v[4]);
}
运行后,出现了提示。
Compiling playground v0.0.1 (/playground)
Finished dev [unoptimized + debuginfo] target(s) in 0.62s
Running `target/debug/playground`
thread 'main' panicked at 'index out of bounds: the len is 4 but the index is 4', src/main.rs:9:22
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
可以看到,这段代码正确通过了编译,但在运行的时候出错了,并且导致了主线程的崩溃。
你可以将其与前面讲的数组 array 下标越界时的预警行为对比理解。为什么 array 的越界访问能在编译阶段检查出来,而 Vec 的越界访问不能在编译阶段检查出来呢?你可以好好想一想。
8. 哈希表
哈希表是一种常见的结构,用于存储 Key-Value
映射关系。Rust 中的哈希表类型为 HashMap。对一个 HashMap 结构来说,Key 要求是同一种类型,比如是字符串就统一用字符串,是数字就统一用数字。Value 也是一样,要求是同一种类型。Key 和 Value 的类型不需要相同。
fn main() {
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
}
9. 元组
元组是一个固定(元素)长度的列表,每个元素类型可以不一样。用小括号括起来,元素之间用逗号隔开。例如:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}
元组元素的访问:
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);
// 元组使用.运算符访问其元素,下标从0开始,注意语法
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}
数组的相同点是,它们都是固定元素个数的,在运行时不可伸缩。与数组的不同点是,元组的每个元素的类型可以不一样。元组在 Rust 中很有用,因为它可以用于函数的返回值,相当于把多个想返回的值捆绑在一起,一次性返回。
当没有任何元素的时候,元组退化成 ()
,就叫做 unit 类型
x. unit
类型
unit
类型,是 Rust 中一个非常重要的基础类型和值,unit 类型唯一的值实例就是 ()
。比如一个函数没有返回值的时候,它实际默认返回的是这个 unit 值。
11. 结构体
结构体也是一种复合类型,它由若干字段组成,每个字段的类型可以不一样。Rust 中使用 struct 关键字来定义结构体。比如下面的代码就定义了一个 User 类型。
struct User {
active: bool,
username: String,
email: String,
age: u64,
}
下面这段代码演示了结构体类型的实例化。
fn main() {
let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("someone@example.com"),
age: 1,
};
}
12. 枚举
枚举是一种自定义的数据分类。就像默认(预编译)数据类型有上述的
Rust 中使用 enum 关键字定义枚举类型。比如:
enum IpAddrKind {
V4,
V6,
}
枚举类型里面的选项叫做此枚举的变体(variants)
。变体是其所属枚举类型的一部分。
枚举使用变体进行枚举类型的实例化,比如:
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
可以看到,枚举类型也是一种复合类型。但是与结构体不同,结构体类型是里面的所有字段(所有类型)同时起作用,来产生一个具体的实例,而枚举类型是其中的一个变体起作用,来产生一个具体实例。学术上,通常把枚举叫作和类型(sum type),把结构体叫作积类型(product type)。
enum的
enum Shape {
Rectangle { width: u32, height: u32},
Triangle((u32, u32), (u32, u32), (u32, u32)),
Circle { origin: (u32, u32), radius: u32 },
}
struct Rectangle {
width: u32,
height: u32
}
enum Shape {
Rectangle(Rectangle),
// ...
}