RUST学习日记之数据类型

变量的声明

rust的变量分为可变变量与不可变变量,声明变量使用let关键字,类型可以不显式声明,可变变量用mut关键字。

1
2
3
4
5
let x:i32 = 42; //显式告知编译器变量类型
let guess = 42; // 编译器推断 guess 是 i32 类型
let pi = 3.14; // 编译器推断 pi 是 f64 类型
let active = true; // 编译器推断 active 是 bool 类型
let mut y = 10; // 声明一个可变变量 y 并赋值为 10

变量的遮蔽

在 Rust 中,变量遮蔽指的是你可以声明一个与之前变量同名的新变量。这个新变量会“遮盖”(或“隐藏”)之前声明的同名变量,使得在当前作用域内,对这个名字的引用会指向新的变量,而不是旧的。

1
2
3
4
5
6
7
8
9
10
fn main() {
let x = 5; // 第一次声明 x
println!("The initial value of x is: {}", x); // 输出 5

let x = x + 1; // 第二次声明 x,它遮蔽了之前的 x
println!("The new value of x is: {}", x); // 输出 6

let x = x * 2; // 第三次声明 x,再次遮蔽
println!("The final value of x is: {}", x); // 输出 12
}

乍一看,变量遮蔽可能与使用 mut 关键字的可变变量(Mutable Variables)有些相似,但它们之间存在根本的区别:

  1. 内存位置

    • 可变变量 (let mut):修改的是同一个内存位置的值。当你说 let mut x = 5; x = 6; 时,x 的值从 5 变成了 6,存储 x 的那块内存区域中的内容发生了变化。
    • 变量遮蔽 (let):是创建了一个全新的变量。每次你使用 let 关键字并带上一个已有的变量名时,Rust 实际上是在内存中分配了一个新的空间来存储新变量的值,而不是修改旧变量的值。旧变量可能仍然存在于内存中,只是你无法通过它的名字访问它了。
  2. 类型改变

    • 可变变量 (let mut):不能改变变量的类型。如果 x 最初是 i32,那么它永远是 i32
    • 变量遮蔽 (let):可以改变变量的类型。这是遮蔽的一个强大之处,也是它与可变变量最明显的区别之一。
    1
    2
    3
    4
    5
    6
    7
    fn main() {
    let spaces = " "; // spaces 是一个 &str 类型
    println!("Type of spaces (string): {:?}", spaces);

    let spaces = spaces.len(); // 新的 spaces 是一个 usize 类型,旧的 &str 被遮蔽了
    println!("Type of spaces (number): {:?}", spaces);
    }

变量遮蔽在 Rust 中被视为一个有用的特性,主要有以下几个原因:

  1. 类型转换或数据转换:当你需要对一个变量进行转换操作(例如从字符串解析数字,或者进行某种计算),并希望用同一个有意义的变量名来表示转换后的结果时,遮蔽非常方便。这样可以避免创建 x_strx_int 这样一系列冗余的变量名。
  2. 避免意外修改:因为遮蔽是创建新变量,而不是修改旧变量,所以它强制你重新使用 let 关键字。这有助于避免无意中修改了某个远处的变量,因为你需要明确地重新声明它。
  3. 提高可读性:在某些情况下,使用相同的变量名可以使代码更具可读性,因为它清楚地表明你正在处理同一个逻辑概念的不同“阶段”或“表示”。

变量遮蔽只在当前作用域内有效。当代码块结束时,被遮蔽的变量(如果它仍然在作用域内)可能会重新变得可见,或者新的遮蔽变量会消失。

1
2
3
4
5
6
7
8
9
10
fn main() {
let s = String::from("hello"); // s1

{ // 这是一个新的内部作用域
let s = String::from("world"); // s2,遮蔽了外部的 s1
println!("Inside inner scope: {}", s); // 输出 "world"
} // 内部作用域结束,s2 被丢弃

println!("Outside inner scope: {}", s); // 输出 "hello" (s1 重新可见)
}

基本数据类型

rust中基本数据类型包括以下几种

  1. 整形(u32、i32)
  2. 浮点型(f32,f64)
  3. 布尔型(bool)
  4. 字符型(char)
1
2
3
4
5
6
fn main() {
let x:i32 = 42;
let y:u64 = 200;
let pi:f64 = 3.14159;
let is_snowing: bool = true;
}

跟C语言没什么大区别,就不详细说了


复合数据类型

复合类型Compound types)可以将多个值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。

元组(tuple):

元组是一个将多个不同类型的值组合进一个复合类型的主要方式。元组长度固定:一旦声明,其长度不会增大或缩小。

我们使用包含在圆括号中的逗号分隔的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。这个例子中使用了可选的类型注解:

1
2
3
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}

tup 变量绑定到整个元组上,因为元组是一个单独的复合元素。为了从元组中获取单个值,可以使用模式匹配(pattern matching)来解构(destructure)元组值,像这样:

1
2
3
4
5
6
7
fn main() {
let tup = (500, 6.4, 1);

let (x, y, z) = tup;

println!("The value of y is: {y}");
}

程序首先创建了一个元组并绑定到 tup 变量上。接着使用了 let 和一个模式将 tup 分成了三个不同的变量,xyz。这叫做 解构destructuring),因为它将一个元组拆成了三个部分。最后,程序打印出了 y 的值,也就是 6.4

我们也可以使用点号(.)后跟值的索引来直接访问所需的元组元素。例如:

1
2
3
4
5
6
7
8
9
fn main() {
let x: (i32, f64, u8) = (500, 6.4, 1);

let five_hundred = x.0;

let six_point_four = x.1;

let one = x.2;
}

这个程序创建了一个元组,x,然后使用其各自的索引访问元组中的每个元素。跟大多数编程语言一样,元组的第一个索引值是 0。

不带任何值的元组有个特殊的名称,叫做 单元(unit) 元组。这种值以及对应的类型都写作 (),表示空值或空的返回类型。如果表达式不返回任何其他值,则会隐式返回单元值。

数组(array):

另一个包含多个值的方式是 数组array)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,Rust 中的数组长度是固定的。

我们将数组的值写成在方括号内,用逗号分隔的列表:

1
2
3
fn main() {
let numbers:[i32;5] = [1,2,3,4,5];
}

当你想要在栈(stack)而不是在堆(heap)上为数据分配空间,或者是想要确保总是有固定数量的元素时,数组非常有用。但是数组并不如 vector 类型灵活。vector 类型是标准库提供的一个 允许 增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,那么很可能应该使用 vector。

然而,当你确定元素个数不会改变时,数组会更有用。例如,当你在一个程序中使用月份名字时,你更应趋向于使用数组而不是 vector,因为你确定只会有 12 个元素。

1
2
let months = ["January", "February", "March", "April", "May", "June", "July",
"August", "September", "October", "November", "December"];

可以像这样编写数组的类型:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量。

1
let a: [i32; 5] = [1, 2, 3, 4, 5];

这里,i32 是每个元素的类型。分号之后,数字 5 表明该数组包含五个元素。

你还可以通过在方括号中指定初始值加分号再加元素个数的方式来创建一个每个元素都为相同值的数组:

1
let a = [3; 5];

变量名为 a 的数组将包含 5 个元素,这些元素的值最初都将被设置为 3。这种写法与 let a = [3, 3, 3, 3, 3]; 效果相同,但更简洁。

数组是可以在栈 (stack) 上分配的已知固定大小的单个内存块。可以使用索引来访问数组的元素,像这样:

1
2
3
4
5
6
fn main() {
let a = [1, 2, 3, 4, 5];

let first = a[0];
let second = a[1];
}

在这个例子中,叫做 first 的变量的值是 1,因为它是数组索引 [0] 的值。变量 second 将会是数组索引 [1] 的值 2


引用类型与切片(slices)

引用类型分为可变引用以及不可变引用。

可变引用可以理解为一种更加安全的指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let mut s = String::from("hello"); // 原始变量必须是可变的

// s 的可变引用被传递给 change 函数
change(&mut s);

println!("{}", s); // 输出 "hello, world"
}

// 这个函数接收一个 String 的可变引用
fn change(some_string: &mut String) {
some_string.push_str(", world");
}

不可变引用可以理解为类似于C语言中const int * 的类型,可以通过指针访问引用的值,但是不能修改。注意引用的对象是可以发生改变的

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let a = 10;
let b = 20;

let mut r = &a; // r 引用 a
println!("r points to a: {}", r); // 输出 10

r = &b; // 重新绑定 r,让它引用 b
println!("r now points to b: {}", r); // 输出 20
}

Rust 的借用检查器(borrow checker) 是一个在编译时运行的工具,它会严格执行以下引用规则,确保内存安全:

  1. 没有空引用: Rust 的引用保证总是指向有效的数据。你无法创建空引用,这避免了其他语言中常见的空指针解引用错误。
  2. 没有悬垂引用: 引用指向的数据在其生命周期内必须始终有效。借用检查器会确保你不会引用一块已经被释放的内存。
  3. 读写互斥规则: 这是最关键的规则,它防止了数据竞争(data races):
    • 在任何给定时间,你只能拥有一个可变引用 (&mut T)。这意味着当数据被修改时,不能有其他任何引用(无论是可变还是不可变)指向它。
    • 你可以拥有任意数量的不可变引用 (&T)。这意味着数据可以被多次同时读取。
    • 但是,当存在不可变引用时,就不能有任何可变引用。

切片是 Rust 中一种特殊的引用类型,它允许你引用集合(如数组、VecString)中连续、特定范围的元素序列。切片本身也是一种引用,不拥有数据。可以类比为指向数组的更安全的指针。

  • 泛型切片 (&[T]):用于引用数组或 Vec 的部分或全部。

    1
    2
    let numbers = [1, 2, 3, 4, 5];
    let middle = &numbers[1..4]; // middle 是 &[i32],引用 [2, 3, 4]
  • 字符串切片 (&str):用于引用 String 的部分或全部,或直接表示字符串字面量。

    1
    2
    3
    let s = String::from("hello world");
    let word = &s[0..5]; // word 是 &str,引用 "hello"
    let literal = "Rust is great"; // 字符串字面量本身就是 &str

String与String slices

在rust中 String是一个结构体,是封装好的可进行修改的字符串。

String slices是字符串切片,无法修改,即不可变引用。

&mut String 和字符串切片 (&str) 之间存在一种非常重要的关系,但它们是两个截然不同且用途互补的概念。

1. &mut String:可变且拥有数据的引用

&mut String 是对一个 String 类型变量的可变引用

  • 所有权和可变性:
    • String 类型本身是拥有数据的,并且其内容是可变的(存储在堆上,可以增长、收缩和修改)。
    • &mut String 允许你通过这个引用来修改原始的 String 数据
  • 用途: 当你需要在函数内部改变一个 String 变量的内容,但又不希望函数获取这个 String 的所有权时,你会传递 &mut String
  • 独占性: 遵循 Rust 的借用规则,当一个 &mut String 存在时,不能有其他任何引用(无论是 &mut String 还是 &String)指向同一个 String 实例。这保证了数据在修改时的唯一访问,从而防止了数据竞争。
1
2
3
4
5
6
7
fn modify_my_string(s: &mut String) {
s.push_str(" world!"); // 通过可变引用修改原始 String
}

let mut my_string = String::from("Hello");
modify_my_string(&mut my_string); // 传递可变引用
println!("{}", my_string); // Output: Hello world!

2. 字符串切片 (&str):不可变且不拥有数据的引用

&str(字符串切片)是对一个 UTF-8 编码的字符串数据段的不可变引用

  • 所有权和不可变性:
    • &str 不拥有数据;它只是“借用”了另一段内存中的字符串数据。
    • &str 自身是不可变的。你不能通过 &str 来修改它所引用的字符串内容。
  • 来源: &str 可以来自多种地方:
    • 字符串字面量("hello")本身就是 &'static str 类型。
    • String 类型可以通过 &my_string[..]&my_string 自动强制转换(deref coercion)为 &str
  • 用途: 当你只需要读取字符串内容,或函数需要接受任何形式的字符串(无论是 String 还是字符串字面量)作为参数时,通常会使用 &str
  • 共享性: 由于是不可变的,你可以创建多个 &str 引用指向同一段数据。
1
2
3
4
5
6
7
8
9
10
11
fn print_my_string(s: &str) {
println!("{}", s); // 只能读取,不能修改
// s.push_str("!"); // 错误!&str 是不可变的
}

let my_string = String::from("Rust");
let literal_string: &str = "Programming";

print_my_string(&my_string); // 将 String 借用为 &str
print_my_string(literal_string); // 直接使用字符串字面量
print_my_string(&my_string[0..2]); // 借用 String 的一部分作为 &str

它们之间的关系和区别总结

特性 &mut String &str
所有权 不拥有数据,是对 String借用 不拥有数据,是对 UTF-8 字符串数据借用
可变性 可变:允许通过引用修改原始 String 的内容 不可变:不允许通过引用修改所引用的字符串内容
指向目标 总是指向一个完整的 String 实例 可以指向 String 的一部分,也可以是整个 String,或者是字符串字面量
独占性 独占:同一时间只能有一个 &mut String 存在 共享:可以同时存在多个 &str 引用
用途 需要在函数中修改 String 的场景 需要读取字符串数据,或作为通用字符串参数的场景

简而言之:

  • &mut String 是你想要修改一个堆上可变字符串时用的。它就像给函数一个“写权限”去操作原始的 String
  • &str 是你想要读取一个**字符串(无论来自哪里)**时用的。它就像给函数一个“只读视图”。

&str 是更通用的字符串视图类型,因为它既可以引用 String 的内容,也可以引用字符串字面量。而 &mut String 明确表示你正在操作一个底层的 String 对象,并且你有权修改它。


1
2
3
4
5
6
7
8
9
10
11
12
13
let book_slices:&[&String] = &[&"JVAV".to_string(),&"IT".to_string(),&"ZEN".to_string()];
println!("Book_Slices:{:?}",book_slices);

//String Vs String Slices(&str)
let mut stone_cold:String = String::from("Hell,");
println!("Stone Cold Says:{}",stone_cold);
stone_cold.push_str("Yeah!");
println!("Stone Cold Says:{}",stone_cold);

//B- &str(String Slice)
let string: String = String::from("Hello,World");
let slice: &str = &string[0..5];
println!("Slice Value:{}",slice);

let book_slices:&[&String] = &[&"JVAV".to_string(),&"IT".to_string(),&"ZEN".to_string()];

是一个是一个指向 &String 类型数组的切片

1
let book_slices: &[&String] = &[&"JVAV".to_string(),&"IT".to_string(),&"ZEN".to_string()];
  1. "JVAV".to_string()
    • 这一步创建了一个拥有所有权String 类型实例,存储在堆上。
  2. &"JVAV".to_string()
    • 这一步从上面创建的 String 实例中,获取了一个不可变引用。这个引用的类型是 &String
  3. [& ... , & ... , & ...]
    • 这是一个数组字面量,它在编译时被创建。这个数组的元素类型,就是它里面包含的那些引用的类型,即 &String。所以,这是一个 [&String; 3] 类型的数组。
  4. &[&String; 3]
    • 最外层的 & 运算符,是获取这个数组的引用
    • 这个引用(& 后面的部分)的类型就是 [&String; 3]
  5. let book_slices: &[&String]
    • 最后,book_slices 被明确地类型标注为 &[&String]。这是一个切片,它指向的底层数组的元素类型是 &String

关键点:没有发生 Deref Coercion

虽然 Rust 有 Deref Coercion(解引用强制转换) 的机制,允许 &String 在需要 &str 的地方自动转换。但是,这种转换发生在单个引用层面,而不会改变数组或切片中元素的实际类型

在这个特定的例子中:

  • 你明确地构建了一个包含 &String 引用的数组:[&some_string_ref_1, &some_string_ref_2]
  • 这个数组的类型就是 [&String; N]
  • 你从这个数组创建的切片,其类型也必须与其元素的实际类型匹配,即 &[&String]

如果 Rust 允许 &[&String] 自动强制转换为 &[&str],那将意味着它需要修改切片中每个元素的类型,这是 Deref Coercion 不会做的事情。Deref Coercion 是一个单个引用到单个引用的转换,而不是一个集合到另一个集合的转换。


例子:当你确实想要 &[&str]

如果你真的想拥有一个 &str 切片(即 &[&str]),你需要这样构建它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn main() {
// 方式一:直接使用字符串字面量(它们本身就是 &str)
let book_str_slices_1: &[&str] = &["JVAV", "IT", "ZEN"];
println!("Book Str Slices 1: {:?}", book_str_slices_1);

// 方式二:从 String 创建 &str,然后将 &str 放入数组
let s1 = "JVAV".to_string();
let s2 = "IT".to_string();
let s3 = "ZEN".to_string();

// 注意这里我们是将 &String 显式地转换(通过 Deref Coercion)为 &str 放入数组
// 但更常见的是直接从 String 创建 &str
let book_str_slices_2: &[&str] = &[&s1, &s2, &s3]; // &s1 (type &String) automatically derefs to &str here
println!("Book Str Slices 2: {:?}", book_str_slices_2);
}

book_str_slices_2 的例子中,当你把 &s1(类型是 &String)放入一个期望 &str 元素的数组时,Deref Coercion 就会发生,将每个 &String 自动转换为 &str


&String与&str

&[&String]&[&str] 都不能直接修改它们所指向的字符串内容,但它们之间存在重要的区别,尤其是在所有权生命周期底层内存布局方面。

核心区别概览

特性 / 类型 &[&String] (指向 &String 数组的切片) &[&str] (指向 &str 数组的切片)
切片元素类型 &String (指向 String 的不可变引用) &str (字符串切片,指向 str 的不可变引用)
被引用数据的 String 类型(拥有数据,在堆上) str 类型(通常在数据段或来自 String 的部分)
内存位置 引用指向堆上的 String 对象 引用可以直接指向字符串字面量(静态区)或 String 的堆数据
底层数据所有权 切片本身不拥有数据,但它引用的 String 拥有数据 切片本身不拥有数据,它引用的 str不拥有数据
生命周期管理 String 实例的生命周期必须比 &String 引用长 str 数据源的生命周期必须比 &str 引用长
创建成本 通常涉及 String::from(),堆分配成本 直接使用字面量,无运行时成本;从 String 借用也无额外成本
灵活性 间接引用堆数据,多了一层封装 直接引用字符串数据,更灵活,可接受多种来源

1. &[&String]:指向 String 引用的切片

当你有 let book_slices: &[&String] 时:

  • 内部元素是 &String:这意味着切片中的每个元素都是一个指向 String 类型数据的引用
  • String 拥有数据:这些 String 对象本身是独立的,它们各自在**堆(heap)**上分配了内存来存储字符串数据。
  • 多层引用:你可以将其想象为“一个数组的切片,这个数组里的每个元素都是一个指针,而这些指针又指向堆上的字符串对象”。
  • 生命周期:这个 &[&String] 切片的有效性,取决于它所引用的那个数组(它是一个临时数组字面量),以及数组里面所有 &String 引用所指向的原始 String 实例的生命周期。这些 String 实例必须在 &[&String] 切片被使用期间保持有效。
    • 在你的例子中 &"JVAV".to_string(),这些 String 实例是匿名的。它们在切片被创建的那一行被创建,它们的生命周期被 Rust 自动管理。由于它们在表达式中创建并立即被引用,Rust 编译器会确保它们存活足够长的时间,以供 book_slices 使用。
  • 修改:你不能通过 &[&String] 切片修改原始的 String 内容,也不能修改切片本身(例如增加或删除元素)。

2. &[&str]:指向字符串切片的切片

当你有 let book_str_slices: &[&str] 时:

  • 内部元素是 &str:这意味着切片中的每个元素都是一个字符串切片,直接指向 UTF-8 编码的字符串数据。
  • &str 不拥有数据:这些 &str 只是视图,它们不负责内存管理。它们可以指向:
    • 静态字符串字面量(存储在程序的二进制文件中,生命周期是 'static',贯穿整个程序)。
    • 堆上 String 的一部分或全部(如果 &str 是从 String 借用而来)。
  • 单层引用:这可以看作是“一个数组的切片,这个数组里的每个元素都是一个字符串指针(胖指针),直接指向字符串数据”。
  • 生命周期:这个 &[&str] 切片的有效性,取决于它所引用的数组,以及数组里面所有 &str 引用所指向的原始字符串数据源的生命周期。这些数据源必须在 &[&str] 切片被使用期间保持有效。
  • 修改:你不能通过 &[&str] 切片修改原始的字符串内容,也不能修改切片本身。

为什么这种区别很重要?

  1. 性能与内存开销
    • &[&String]:创建时需要先创建 String 对象(堆分配),再获取引用。这会带来额外的内存分配和解分配开销。
    • &[&str]:如果元素是字符串字面量,就没有堆分配开销。如果元素是从 String 借用的 &str,那么也没有额外的堆分配。通常更轻量级。
  2. 生命周期复杂性
    • &[&String]:你引用的 String 实例必须在切片的整个生命周期内都存在。如果这些 String 是匿名临时创建的,Rust 编译器会尽力延长它们的生命周期,但这并非总是能成功。
    • &[&str]:其生命周期取决于它所引用的实际 str 数据。&'static str 最简单,可以无限制地使用。从 String 借用的 &str 则受限于 String 的生命周期。
  3. 函数通用性
    • 通常情况下,函数参数更倾向于使用 &str 而不是 &String,因为 &str 更通用,可以接受各种来源的字符串数据。
    • &[&str] 这样的类型在处理一组通用字符串视图时更为常见。

总结

虽然 &[&String]&[&str] 都表示不可修改的字符串序列,但它们在底层数据类型、所有权链、内存分配和生命周期管理上存在根本区别。

  • &[&String] 是一个切片,它的元素是指向堆上 String 对象的引用
  • &[&str] 是一个切片,它的元素是直接指向 str 数据的引用(可以是静态区或堆上 String 的一部分)。

理解这些细微差别对于编写高效、安全且符合 Rust 习惯的代码至关重要。

但是&[&mut String]是可以对包含的字符串内容进行修改的,可以类比C语言中的字符数组数组。