# Rust 学习笔记

# 变量声明:

struct Struct{
    e:i32
}
fn main() {
    // 若不使用变量时,需要在变量名前面写一个下划线,表示该变量不会被使用,这样才不会报错
    // let mut _x = 5;
    // _x = 6;
    // println!("The value of x is: {}", _x);
    
    //let (a,mut b):(bool,bool)=(true,false);
    //println!("a is {:?},b is {:?}",a,b);
    //b=true;
    //assert_eq!(a,b);// 用于检查两个值是否相等。如果它们不相等,程序会 panic! 并终止运行
    
    let (a, b, c, d, e);
    (a, b) = (1, 2);
    //_ 代表匹配一个值,但是我们不关心具体的值是什么,因此没有使用一个变量名而是使用了 _
    [c, .., d, _] = [1, 2, 3, 4, 5];
    Struct { e, .. } = Struct { e: 5 };
    println!("a is {}, b is {}, c is {}, d is {}, e is {}", a, b, c, d, e);
    assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]);
    // 常量:
    const MAX_POINTS: u32 = 100_000;
    println!("The value of MAX_POINTS is: {}", MAX_POINTS);
    let space = "    ";
    println!("空格的数量是{}", space.len());
    let guess:i32 = "42".parse().expect("Not a number!");
 //or:let guess = "42".parse::<i32>().expect("Not a number!");
    
}

# 数值类型:

# 整数类型:

rust 内置的整数类型

image-20250328200043773

整形字面量可以用下表的形式书写:

image-20250328200031377

fn main() {
    let a:u8 = 255;
    let b= a.wrapping_add(20); // 相当于加法,给溢出了
    println!("{}",b); //19
}

# 浮点型:

两种:f32 和 f64–默认

由于精度的原因,我们的 0.1+0.2 不会完全等于 0.3(类似于 python)image-20250328200743786

image-20250328200810396

我们在 rust 可以用:

(0.1_f64 + 0.2 - 0.3).abs() < 0.00001

fn main() {
let abc: (f32, f32, f32) = (0.1, 0.2, 0.3);
let xyz: (f64, f64, f64) = (0.1, 0.2, 0.3);

println!("abc (f32)");
println!("   0.1 + 0.2: {:x}", (abc.0 + abc.1).to_bits());
println!("         0.3: {:x}", (abc.2).to_bits());
println!();
println!("xyz (f64)");
println!("   0.1 + 0.2: {:x}", (xyz.0 + xyz.1).to_bits());
println!("         0.3: {:x}", (xyz.2).to_bits());
println!();
assert!(abc.0 + abc.1 == abc.2);
assert!(xyz.0 + xyz.1 == xyz.2);}

结果:

abc (f32)
   0.1 + 0.2: 3e99999a
         0.3: 3e99999a

xyz (f64)
   0.1 + 0.2: 3fd3333333333334
         0.3: 3fd3333333333333


thread 'main' panicked at main.rs:16:5:
assertion failed: xyz.0 + xyz.1 == xyz.2
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

f32 类型做加法时, 0.1 + 0.2 的结果是 3e99999a0.3 也是 3e99999a ,因此 f32 下的 0.1 + 0.2 == 0.3 通过测试,但是到了 f64 类型时,结果就不一样了,因为 f64 精度高很多,因此在小数点非常后面发生了一点微小的变化, 0.1 + 0.24 结尾,但是 0.33 结尾,这个细微区别导致 f64 下的测试失败了,并且抛出了异常。

# 运算:

±*/ %(求余)

# 位运算:

image-20250328201221251

fn main() {
    // 无符号 8 位整数,二进制为 00000010
    let a: u8 = 2; // 也可以写 let a: u8 = 0b_0000_0010;
    // 二进制为 00000011
    let b: u8 = 3;
    // {:08b}:左高右低输出二进制 01,不足 8 位则高位补 0
    println!("a value is        {:08b}", a);
    println!("b value is        {:08b}", b);
    println!("(a & b) value is  {:08b}", a & b);
    println!("(a | b) value is  {:08b}", a | b);
    println!("(a ^ b) value is  {:08b}", a ^ b);
    println!("(!b) value is     {:08b}", !b);
    println!("(a << b) value is {:08b}", a << b);
    println!("(a >> b) value is {:08b}", a >> b);
    let mut a = a;
    // 注意这些计算符除了!之外都可以加上 = 进行赋值 (因为!= 要用来判断不等于)
    a <<= b;
    println!("(a << b) value is {:08b}", a);
}

位运算要用 u8,结果:

a value is        00000010
b value is        00000011
(a & b) value is  00000010
(a | b) value is  00000011
(a ^ b) value is  00000001
(!b) value is     11111100
(a << b) value is 00010000
(a >> b) value is 00000000
(a << b) value is 00010000

# 序列(***):

我们可以用 1…5 这个来生成 1-5 这几个连续数值,包括 5

for i in 1..=5{
println!("{}",i);
}
1
2
3
4
5

还能用于字符类型,源于他们的连续型(猜测应该是 ascii 的连续性)

# AS 完成类型转换:

fn main() {
   let a = 3.1 as i8;
   let b = 100_i8 as i32;
   let c = 'a' as u8; // 将字符 'a' 转换为整数,97
   println!("{},{},{}",a,b,c)
}

有理数和复数:

use num::complex::Complex;
 fn main() {
   let a = Complex { re: 2.1, im: -1.2 };
   let b = Complex::new(11.1, 22.2);
   let result = a + b;
   println!("{} + {}i", result.re, result.im)
 }

结果:

13.2 + 21i

# 字符,布尔,单元类型:

单元类型:

单元类型就是 () ,对,你没看错,就是 () ,唯一的值也是 () ,一些读者读到这里可能就不愿意了,你也太敷衍了吧,管这叫类型?

只能说,再不起眼的东西,都有其用途,在目前为止的学习过程中,大家已经看到过很多次 fn main() 函数的使用吧?那么这个函数返回什么呢?

没错, main 函数就返回这个单元类型 () ,你不能说 main 函数无返回值,因为没有返回值的函数在 Rust 中是有单独的定义的: 发散函数( diverge function ) ,顾名思义,无法收敛的函数。

例如常见的 println!() 的返回值也是单元类型 ()

再比如,你可以用 () 作为 map 的值,表示我们不关注具体的值,只关注 key 。 这种用法和 Go 语言的 struct{} 类似,可以作为一个值用来占位,但是完全不占用任何内存。

# 语句与表达式:

表达式会进行求值,然后返回一个值。例如 5 + 6 ,在求值后,返回值 11 ,因此它就是一条表达式。

表达式可以成为语句的一部分,例如 let y = 6 中, 6 就是一个表达式,它在求值后返回一个值 6 (有些反直觉,但是确实是表达式)。

调用一个函数是表达式,因为会返回一个值,调用宏也是表达式,用花括号包裹最终返回一个值的语句块也是表达式,总之,能返回值,它就是表达式:

fn main(){
let y ={let x  =3;x+1};
println!("The value o y is:{}",y);
}

语句块:

{
let x  =3;
x+1
}

这里的 x+1 就是返回的值

若在表达式里面加上;就是代表不会返回值

fn main() {
    assert_eq!(ret_unit_type(), ())
}
fn ret_unit_type() {
    let x = 1;
    //if 语句块也是一个表达式,因此可以用于赋值,也可以直接返回
    // 类似三元运算符,在 Rust 里我们可以这样写
    let y = if x % 2 == 1 {
        "odd"
    } else {
        "even"
    };
    // 或者写成一行
    let z = if x % 2 == 1 { "odd" } else { "even" };
}

# 函数:

例子:

fn add(i:i32,j:i32)->i32{
i+j
}

image-20250328204037066

# 所有权与借用:

# 所有权:

所有的程序都必须和计算机内存打交道,如何从内存中申请空间来存放程序的运行内容,如何在不需要的时候释放这些空间,成了重中之重,也是所有编程语言设计的难点之一。在计算机语言不断演变过程中,出现了三种流派:

  • 垃圾回收机制 (GC),在程序运行时不断寻找不再使用的内存,典型代表:Java、Go
  • 手动管理内存的分配和释放,在程序中,通过函数调用的方式来申请和释放内存,典型代表:C++
  • 通过所有权来管理内存,编译器在编译时会根据一系列规则进行检查

其中 Rust 选择了第三种,最妙的是,这种检查只发生在编译期,因此对于程序运行期,不会有任何性能上的损失。

由于所有权是一个新概念,因此读者需要花费一些时间来掌握它,一旦掌握,海阔天空任你飞跃,在本章,我们将通过 字符串 来引导讲解所有权的相关知识。

int* foo() {
    int a;          // 变量 a 的作用域开始
    a = 100;
    char *c = "xyz";   // 变量 c 的作用域开始
    return &a;
}                   // 变量 a 和 c 的作用域结束

这段代码虽然可以编译通过,但是其实非常糟糕,变量 a 是在函数内进行的定义分配的内存,现在我返回这个的指针,但是我分配时在这个函数块里面,这样的话过了这个函数 a 就会被自动释放,这个 a 变量的指针会被释放无效,或是被其他的函数调用或局部变量覆盖,从而造成了 悬空指针(Dangling Pointer) 的问题。这是一个非常典型的内存安全问题,虽然编译可以通过,但是运行的时候会出现错误,很多编程语言都存在。

再来看变量 cc 的值是常量字符串,存储于常量区,可能这个函数我们只调用了一次,也可能我们不再会使用这个字符串,但 "xyz" 只有当整个程序结束后系统才能回收这片内存。

所以内存安全问题,一直都是程序员非常头疼的问题,好在,在 Rust 中这些问题即将成为历史,因为 Rust 在编译的时候就可以帮助我们发现内存不安全的问题,那 Rust 如何做到这一点呢?

在正式进入主题前,先来一个预热知识。

# 栈与堆:

栈和堆的核心目标就是

为程序在运行时提供可供使用的内存空间。

# 栈:

(栈中的数据必须是占用已知且固定大小的内存空间)先进后出,加数据是进栈,移出数据叫出栈

# 堆:

(堆中的数据都是大小未知或者可能变化的数据)

当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针,该过程被称为在堆上分配内存,有时简称为 “分配”(allocating)。

接着该指针会被推入栈中中,因为指针的大小是一直且固定的,后续使用过程中可以通过这个指针访问堆来访问这指针对应的数据

# 性能区别:
在栈上分配内存比在堆上分配内存要快,因为入栈时操作系统无需进行函数调用(或更慢的系统调用)来分配新的空间,只需要将新数据放入栈顶即可。相比之下,在堆上分配内存则需要更多的工作,这是因为操作系统必须首先找到一块足够存放数据的内存空间,接着做一些记录为下一次分配做准备,如果当前进程分配的内存页不足时,还需要进行系统调用来申请更多内存。 因此,处理器在栈上分配数据会比在堆上分配数据更加高效。
# 所有权与堆栈

当你的代码调用一个函数时,传递给函数的参数(包括可能指向堆上数据的指针和函数的局部变量)依次被压入栈中,当函数调用结束时,这些值将被从栈中按照相反的顺序依次移除。

因为堆上的数据缺乏组织,因此跟踪这些数据何时分配和释放是非常重要的,否则堆上的数据将产生内存泄漏 —— 这些数据将永远无法被回收。这就是 Rust 所有权系统为我们提供的强大保障。

对于其他很多编程语言,你确实无需理解堆栈的原理,但是在 Rust 中,明白堆栈的原理,对于我们理解所有权的工作原理会有很大的帮助

# 所有权原则:

规则:

1.rust中每一个值都被一个变量所拥有,该变量被称为值的所有这
2.一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
3.当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)

例如字符串:

之前的语言都是直接给一个字符串值,但是不能改变了,这中叫做是被硬编码进程序里的字符串值(类型为 & str)

又两个缺点:

不能变,不是什么时候都是可以在编写代码时后就可以得知的

所以,rust 提供动态字符串类型:String, 该类型被分配到堆上,因此可以动态伸缩,也就能存储在编译时大小位置的文本

可以用下面的方法基于字符串字面量来创建 String 类型:

let s =String::from("hello");

:: 是一种调用操作符,这里表示调用 String 类型中的 from 关联函数,由于 String 类型存储在堆上,因此它是动态的,可以修改:

let mut s =String::from("hello ");
s.push_str(",world");
println!("{}",s);

用 push_str 函数就可以加进去了

# 变量绑定背后的数据交互:
let s1= String::from("hello");
let s2=s1;

String 类型是一个复杂类型,又存储在栈中的堆指针,字符串长度,字符串容量,其中堆指针是最重要的,容量是堆内存分配空间的大小,长度是目前已经使用的大小。

Rust 这样解决问题:s1 被赋予 s2 后,Rust 认为 s1 不再有效,因此也无需在 s1 离开作用域后 drop 任何东西,这就是把所有权从 s1 转移给了 s2s1 在被赋予 s2 后就马上失效了

s1 的引用便会无效了

这样就解决了我们之前的问题, s1 不再指向任何数据,只有 s2 是有效的,当 s2 离开作用域,它就会释放内存。 相信此刻,你应该明白了,为什么 Rust 称呼 let a = b变量绑定了吧?

# 引用与解引用

# 不可变引用:
fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
    s.len()
}
这个函数fn calculate_length(s: &String) -> usize {
    s.len()
}我没有对这个函数进行任何操作,所以什么也不会发生

但是有时候我们有需要对引用进来的值进行操作

这时候就要用 mut 来可变一下了

# 可变引用:
fn main() {
    let mut s = String::from("hello");
    change(&mut s);
}
fn change(some_string: &mut String) {
    some_string.push_str(", world");
}
# 这两者的区别与联系:
1.可变引用只能在一个作用域里面有一个
2.可变引用与不可变引用只能有一个
3.只有不可变引用时,可以有多个不可变引用

# 悬垂引用 (Dangling References)

悬垂引用也叫做悬垂指针,意思为指针指向某个值后,这个值被释放掉了,而指针仍然存在,其指向的内存可能不存在任何值或已被其它变量重新使用。在 Rust 中编译器可以确保引用永远也不会变成悬垂状态:当你获取数据的引用后,编译器可以确保数据不会在引用结束前被释放,要想释放数据,必须先停止其引用的使用。

让我们尝试创建一个悬垂引用,Rust 会抛出一个编译时错误:

fn main() {
    let reference_to_nothing = dangle();
}
fn dangle() -> &String {
    let s = String::from("hello");
    &s
}

这里是错误:

error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
5 | fn dangle() -> &'static String {
  |                ~~~~~~~~

错误信息引用了一个我们还未介绍的功能:生命周期 (lifetimes)。不过,即使你不理解生命周期,也可以通过错误信息知道这段代码错误的关键信息:

this function's return type contains a borrowed value, but there is no value for it to be borrowed from.
该函数返回了一个借用的值,但是已经找不到它所借用值的来源

仔细看看 dangle 代码的每一步到底发生了什么:

fn dangle() -> &String { //dangle 返回一个字符串的引用
    let s = String::from("hello"); //s 是一个新字符串
    &s // 返回字符串 s 的引用
} // 这里 s 离开作用域并被丢弃。其内存被释放。
  // 危险!

因为 s 是在 dangle 函数内创建的,当 dangle 的代码执行完毕后, s 将被释放,但是此时我们又尝试去返回它的引用。这意味着这个引用会指向一个无效的 String ,这可不对!

其中一个很好的解决方法是直接返回 String

fn no_dangle() -> String {
    let s = String::from("hello");
    s
}

这样就没有任何错误了,最终 String所有权被转移给外面的调用者

# 借用规则总结
  • 总的来说,借用规则如下:
    
    - 同一时刻,你只能拥有要么一个可变引用,要么任意多个不可变引用
    - 引用必须总是有效的
    

# 复合类型:

# 字符串与切片:

# 字符串:

&str 是一个不可变引用(字符串字面量)

String 是可以用来可变引用的

# 切片:

对字符串而言,切片就是对 String 类型中某一部分的引用

let s = String::from("hello world");
let hello = &s[0..5];
let world = &s[6..11];

hello 就是从 0 索引开始,到索引 5 之前截止(0-4,不包括)

world 同理

image-20250330192216268

切片想要包含 String 最后一个字节:

let s = String::from("hello");
let len = s.len();
let slice = &s[4..len];
let slice = &s[4..];

在对字符串使用切片语法时需要格外小心,切片的索引必须落在字符之间的边界位置,也就是 UTF-8 字符的边界,例如中文在 UTF-8 中占用三个字节,下面的代码就会崩溃:

let s = "中国人";
 let a = &s[0..2];
 println!("{}",a);

什么是字符串:字符组成的连续集合

Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的 (1 - 4) Rust 在语言级别,只有一种字符串类型: str ,它通常是以引用类型出现 &str ,也就是上文提到的字符串切片。虽然语言级别只有上述的 str 类型,但是在标准库里,还有多种不同用途的字符串类型,其中使用最广的即是 String 类型

str 类型是硬编码进可执行文件,也无法被修改,但是 String 则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串,当 Rust 用户提到字符串时,往往指的就是 String 类型和 &str 字符串切片类型,这两个类型都是 UTF-8 编码

除了 String 类型的字符串,Rust 的标准库还提供了其他类型的字符串,例如 OsStringOsStrCsStringCsStr 等,注意到这些名字都以 String 或者 Str 结尾了吗?它们分别对应的是具有所有权和被借用的变量。

# String 与 & str 的转换:

在之前的代码中,已经见到好几种从 &str 类型生成 String 类型的操作:

  • String::from("hello,world")
  • "hello,world".to_string()

那么如何将 String 类型转为 &str 类型呢?答案很简单,取引用即可:

fn main() {
    let s = String::from("hello,world!");
    say_hello(&s);
    say_hello(&s[..]);
    say_hello(s.as_str());
}
fn say_hello(s: &str) {
    println!("{}",s);
}

实际上这种灵活用法是因为 deref 隐式强制转换,具体我们会在 Deref 特征进行详细讲解。

# 字符串索引

在其它语言中,使用索引的方式访问字符串的某个字符或者子串是很正常的行为,但是在 Rust 中就会报错:

let s1 = String::from("hello");
   let h = s1[0];

该代码会产生如下错误:

3 |     let h = s1[0];
  |             ^^^^^ `String` cannot be indexed by `{integer}`
  |
  = help: the trait `Index<{integer}>` is not implemented for `String`

# 深入字符串内部

字符串的底层的数据存储格式实际上是 [ u8 ],一个字节数组。对于 let hello = String::from("Hola"); 这行代码来说, Hola 的长度是 4 个字节,因为 "Hola" 中的每个字母在 UTF-8 编码中仅占用 1 个字节,但是对于下面的代码呢?

let hello = String::from("中国人");

如果问你该字符串多长,你可能会说 3 ,但是实际上是 9 个字节的长度,因为大部分常用汉字在 UTF-8 中的长度是 3 个字节,因此这种情况下对 hello 进行索引,访问 &hello[0] 没有任何意义,因为你取不到 这个字符,而是取到了这个字符三个字节中的第一个字节,这是一个非常奇怪而且难以理解的返回值。

# 字符串的不同表现形式

现在看一下用梵文写的字符串 “नमस्ते” , 它底层的字节数组如下形式:

[224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164,
224, 165, 135]

长度是 18 个字节,这也是计算机最终存储该字符串的形式。如果从字符的形式去看,则是:

['न', 'म', 'स', '्', 'त', 'े']

但是这种形式下,第四和六两个字母根本就不存在,没有任何意义,接着再从字母串的形式去看:

["न", "म", "स्", "ते"]

所以,可以看出来 Rust 提供了不同的字符串展现方式,这样程序可以挑选自己想要的方式去使用,而无需去管字符串从人类语言角度看长什么样。

还有一个原因导致了 Rust 不允许去索引字符串:因为索引操作,我们总是期望它的性能表现是 O (1),然而对于 String 类型来说,无法保证这一点,因为 Rust 可能需要从 0 开始去遍历字符串来定位合法的字符。

# 字符串切片

前文提到过,字符串切片是非常危险的操作,因为切片的索引是通过字节来进行,但是字符串又是 UTF-8 编码,因此你无法保证索引的字节刚好落在字符的边界上,例如:

let hello = "中国人";
let s = &hello[0..2];

运行上面的程序,会直接造成崩溃:

thread 'main' panicked at 'byte index 2 is not a char boundary; it is inside '中' (bytes 0..3) of `中国人`', src/main.rs:4:14
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

这里提示的很清楚,我们索引的字节落在了 字符的内部,这种返回没有任何意义。

因此在通过索引区间来访问字符串时,需要格外的小心,一不注意,就会导致你程序的崩溃!

# 字符串操作(***):

# 追加 (Push)

在字符串尾部可以使用 push() 方法追加字符 char ,也可以使用 push_str() 方法追加字符串字面量。这两个方法都是在原有的字符串上追加,并不会返回新的字符串。由于字符串追加操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰

示例代码如下:

fn main() {
    let mut s = String::from("Hello ");
    s.push_str("rust");
    println!("追加字符串 push_str() -> {}", s);
    s.push('!');
    println!("追加字符 push() -> {}", s);
}

代码运行结果:

追加字符串 push_str() -> Hello rust
追加字符 push() -> Hello rust!

# 插入 (Insert)

可以使用 insert() 方法插入单个字符 char ,也可以使用 insert_str() 方法插入字符串字面量,与 push() 方法不同,这俩方法需要传入两个参数,第一个参数是字符(串)插入位置的索引,第二个参数是要插入的字符(串),索引从 0 开始计数,如果越界则会发生错误。由于字符串插入操作要修改原来的字符串,则该字符串必须是可变的,即字符串变量必须由 mut 关键字修饰

示例代码如下:

fn main() {
    let mut s = String::from("Hello rust!");
    s.insert(5, ',');
    println!("插入字符 insert() -> {}", s);
    s.insert_str(6, " I like");
    println!("插入字符串 insert_str() -> {}", s);
}

代码运行结果:

插入字符 insert() -> Hello, rust!
插入字符串 insert_str() -> Hello, I like rust!

# 替换 (Replace)

如果想要把字符串中的某个字符串替换成其它的字符串,那可以使用 replace() 方法。与替换有关的方法有三个。

1、 replace

该方法可适用于 String&str 类型。 replace() 方法接收两个参数,第一个参数是要被替换的字符串,第二个参数是新的字符串。该方法会替换所有匹配到的字符串。该方法是返回一个新的字符串,而不是操作原来的字符串

示例代码如下:

fn main() {
    let string_replace = String::from("I like rust. Learning rust is my favorite!");
    let new_string_replace = string_replace.replace("rust", "RUST");
    dbg!(new_string_replace);
}

代码运行结果:

new_string_replace = "I like RUST. Learning RUST is my favorite!"

2、 replacen

该方法可适用于 String&str 类型。 replacen() 方法接收三个参数,前两个参数与 replace() 方法一样,第三个参数则表示替换的个数。该方法是返回一个新的字符串,而不是操作原来的字符串

示例代码如下:

fn main() {
    let string_replace = "I like rust. Learning rust is my favorite!";
    let new_string_replacen = string_replace.replacen("rust", "RUST", 1);
    dbg!(new_string_replacen);
}

代码运行结果:

new_string_replacen = "I like RUST. Learning rust is my favorite!"

3、 replace_range

该方法仅适用于 String 类型。 replace_range 接收两个参数,第一个参数是要替换字符串的范围(Range),第二个参数是新的字符串。该方法是直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰

示例代码如下:

fn main() {
    let mut string_replace_range = String::from("I like rust!");
    string_replace_range.replace_range(7..8, "R");
    dbg!(string_replace_range);
}

代码运行结果:

string_replace_range = "I like Rust!"

# 删除 (Delete)

与字符串删除相关的方法有 4 个,它们分别是 pop()remove()truncate()clear() 。这四个方法仅适用于 String 类型。

1、 pop —— 删除并返回字符串的最后一个字符

该方法是直接操作原来的字符串。但是存在返回值,其返回值是一个 Option 类型,如果字符串为空,则返回 None

示例代码如下:

fn main() {
    let mut string_pop = String::from("rust pop 中文!");
    let p1 = string_pop.pop();
    let p2 = string_pop.pop();
    dbg!(p1);
    dbg!(p2);
    dbg!(string_pop);
}

代码运行结果:

p1 = Some(
   '!',
)
p2 = Some(
   '文',
)
string_pop = "rust pop 中"

2、 remove —— 删除并返回字符串中指定位置的字符

该方法是直接操作原来的字符串。但是存在返回值,其返回值是删除位置的字符串,只接收一个参数,表示该字符起始索引位置。 remove() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。

示例代码如下:

fn main() {
    let mut string_remove = String::from("测试remove方法");
    println!(
        "string_remove 占 {} 个字节",
        std::mem::size_of_val(string_remove.as_str())
    );
    // 删除第一个汉字
    string_remove.remove(0);
    // 下面代码会发生错误
    // string_remove.remove(1);
    // 直接删除第二个汉字
    // string_remove.remove(3);
    dbg!(string_remove);
}

代码运行结果:

string_remove 占 18 个字节
string_remove = "试remove方法"

3、 truncate —— 删除字符串中从指定位置开始到结尾的全部字符

该方法是直接操作原来的字符串。无返回值。该方法 truncate() 方法是按照字节来处理字符串的,如果参数所给的位置不是合法的字符边界,则会发生错误。

示例代码如下:

fn main() {
    let mut string_truncate = String::from("测试truncate");
    string_truncate.truncate(3);
    dbg!(string_truncate);
}

代码运行结果:

string_truncate = "测"

4、 clear —— 清空字符串

该方法是直接操作原来的字符串。调用后,删除字符串中的所有字符,相当于 truncate() 方法参数为 0 的时候。

示例代码如下:

fn main() {
    let mut string_clear = String::from("string clear");
    string_clear.clear();
    dbg!(string_clear);
}

代码运行结果:

string_clear = ""

# 连接 (Concatenate)

1、使用 + 或者 += 连接字符串

使用 + 或者 += 连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型。其实当调用 + 的操作符时,相当于调用了 std::string 标准库中的 add() 方法,这里 add() 方法的第二个参数是一个引用的类型。因此我们在使用 + 时, 必须传递切片引用类型。不能直接传递 String 类型。 + 是返回一个新的字符串,所以变量声明可以不需要 mut 关键字修饰

示例代码如下:

fn main() {
    let string_append = String::from("hello ");
    let string_rust = String::from("rust");
    // &string_rust 会自动解引用为 & amp;str
    let result = string_append + &string_rust;
    let mut result = result + "!"; // `result +"!"`中的`result` 是不可变的
    result += "!!!";
    println!("连接字符串 + -> {}", result);
}

代码运行结果:

连接字符串 + -> hello rust!!!!

add() 方法的定义:

fn add(self, s: &str) -> String

因为该方法涉及到更复杂的特征功能,因此我们这里简单说明下:

fn main() {
    let s1 = String::from("hello,");
    let s2 = String::from("world!");
    // 在下句中,s1 的所有权被转移走了,因此后面不能再使用 s1
    let s3 = s1 + &s2;
    assert_eq!(s3,"hello,world!");
    // 下面的语句如果去掉注释,就会报错
    // println!("{}",s1);
}

selfString 类型的字符串 s1 ,该函数说明,只能将 &str 类型的字符串切片添加到 String 类型的 s1 上,然后返回一个新的 String 类型,所以 let s3 = s1 + &s2; 就很好解释了,将 String 类型的 s1&str 类型的 s2 进行相加,最终得到 String 类型的 s3

由此可推,以下代码也是合法的:

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
// String = String + &str + &str + &str + &str
let s = s1 + "-" + &s2 + "-" + &s3;

String + &str 返回一个 String ,然后再继续跟一个 &str 进行 + 操作,返回一个 String 类型,不断循环,最终生成一个 s ,也是 String 类型。

s1 这个变量通过调用 add() 方法后,所有权被转移到 add() 方法里面, add() 方法调用后就被释放了,同时 s1 也被释放了。再使用 s1 就会发生错误。这里涉及到所有权转移(Move)的相关知识。

2、使用 format! 连接字符串

format! 这种方式适用于 String&strformat! 的用法与 print! 的用法类似,详见格式化输出

示例代码如下:

fn main() {
    let s1 = "hello";
    let s2 = String::from("rust");
    let s = format!("{} {}!", s1, s2);
    println!("{}", s);
}

代码运行结果:

hello rust!

# 字符串转义:

我们可以通过转义的方式 \ 输出 ASCII 和 Unicode 字符。

fn main() {
    // 通过 \ + 字符的十六进制表示,转义输出一个字符
    let byte_escape = "I'm writing \x52\x75\x73\x74!";
    println!("What are you doing\x3F (\\x3F means ?) {}", byte_escape);
    // \u 可以输出一个 unicode 字符
    let unicode_codepoint = "\u{211D}";
    let character_name = "\"DOUBLE-STRUCK CAPITAL R\"";
    println!(
        "Unicode character {} (U+211D) is called {}",
        unicode_codepoint, character_name
    );
    // 换行了也会保持之前的字符串格式
    // 使用 \ 忽略换行符
    let long_string = "String literals
                        can span multiple lines.
                        The linebreak and indentation here ->\
                        <- can be escaped too!";
    println!("{}", long_string);
}

在 \ 加上一个 \ 转义这个就好了

fn main() {
    println!("{}", "hello \\x52\\x75\\x73\\x74");
    let raw_str = r"Escapes don't work here: \x3F \u{211D}";
    println!("{}", raw_str);
    // 如果字符串包含双引号,可以在开头和结尾加 #
    let quotes = r#"And then I said: "There is no escape!""#;
    println!("{}", quotes);
    // 如果字符串中包含 # 号,可以在开头和结尾加多个 # 号,最多加 255 个,只需保证与字符串中连续 # 号的个数不超过开头和结尾的 # 号的个数即可
    let longer_delimiter = r###"A string with "# in it. And even "##!"###;
    println!("{}", longer_delimiter);
}

操作 UTF-8 字符串

前文提到了几种使用 UTF-8 字符串的方式,下面来一一说明。

# 字符

如果你想要以 Unicode 字符的方式遍历字符串,最好的办法是使用 chars 方法,例如:

for c in "中国人".chars() {
    println!("{}", c);
}

输出如下

中
国
人

# 字节

这种方式是返回字符串的底层字节数组表现形式:

for b in "中国人".bytes() {
    println!("{}", b);
}

输出如下:

228
184
173
229
155
189
228
186
186

# 获取子串

想要准确的从 UTF-8 字符串中获取子串是较为复杂的事情,例如想要从 holla中国人नमस्ते 这种变长的字符串中取出某一个子串,使用标准库你是做不到的。 你需要在 crates.io 上搜索 utf8 来寻找想要的功能。

可以考虑尝试下这个库:utf8_slice

# 字符串深度剖析

那么问题来了,为啥 String 可变,而字符串字面值 str 却不可以?

就字符串字面值来说,我们在编译时就知道其内容,最终字面值文本被直接硬编码进可执行文件中,这使得字符串字面值快速且高效,这主要得益于字符串字面值的不可变性。不幸的是,我们不能为了获得这种性能,而把每一个在编译时大小未知的文本都放进内存中(你也做不到!),因为有的字符串是在程序运行的过程中动态生成的。

对于 String 类型,为了支持一个可变、可增长的文本片段,需要在堆上分配一块在编译时未知大小的内存来存放内容,这些都是在程序运行时完成的:

  • 首先向操作系统请求内存来存放 String 对象
  • 在使用完成后,将内存释放,归还给操作系统

其中第一部分由 String::from 完成,它创建了一个全新的 String

重点来了,到了第二部分,就是百家齐放的环节,在有垃圾回收 GC 的语言中,GC 来负责标记并清除这些不再使用的内存对象,这个过程都是自动完成,无需开发者关心,非常简单好用;但是在无 GC 的语言中,需要开发者手动去释放这些内存对象,就像创建对象需要通过编写代码来完成一样,未能正确释放对象造成的后果简直不可估量。

对于 Rust 而言,安全和性能是写到骨子里的核心特性,如果使用 GC,那么会牺牲性能;如果使用手动管理内存,那么会牺牲安全,这该怎么办?为此,Rust 的开发者想出了一个无比惊艳的办法:变量在离开作用域后,就自动释放其占用的内存:

{
    let s = String::from("hello"); // 从此处起,s 是有效的
    // 使用 s
}                                  // 此作用域已结束,
                                   //s 不再有效,内存被释放

与其它系统编程语言的 free 函数相同,Rust 也提供了一个释放内存的函数: drop ,但是不同的是,其它语言要手动调用 free 来释放每一个变量占用的内存,而 Rust 则在变量离开作用域时,自动调用 drop 函数:上面代码中,Rust 在结尾的 } 处自动调用 drop

其实,在 C++ 中,也有这种概念:Resource Acquisition Is Initialization (RAII)。如果你使用过 RAII 模式的话应该对 Rust 的 drop 函数并不陌生。

这个模式对编写 Rust 代码的方式有着深远的影响,在后面章节我们会进行更深入的介绍。

# 元组:

复合类型:

# 语法

可以通过一下用法创建元组:

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

# 模式匹配:

fn main(){
let tup=(600,5.4,2);
let(x,y,z)=tup;
println!("the value of y is {}",y);
}

上述代码首先创建一个元组,然后将其绑定到 tup 上,接着使用 let (x, y, z) = tup; 来完成一次模式匹配,因为元组是 (n1, n2, n3) 形式的,因此我们用一模一样的 (x, y, z) 形式来进行匹配,元组中对应的值会绑定到变量 xyz 上。这就是解构:用同样的形式把一个复杂对象中的值匹配出来。

# 用。访问元组

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;
}

索引从 0 开始

使用实例:

fn main() {
    let s1 = String::from("hello");
    let (s2, len) = calculate_length(s1);
    println!("The length of '{}' is {}.", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); //len () 返回字符串的长度
    (s, length)
}

# 结构体

# 语法:

中间的不是用;分隔,而是用,分割

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

创建实例:

let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

有几点值得注意:

  1. 初始化实例时,每个字段都需要进行初始化
  2. 初始化时的字段顺序不需要和结构体定义时的顺序一致

# 用。访问结构体字段:

let mut user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };
    user1.email = String::from("anotheremail@example.com");

# 简化结构体创建:

fn build_user(email:String ,username:String)->User{
User{
email:email,
username:username,
active:true,
sign_in_count:1,
}
}

它接收两个字符串参数: emailusername ,然后使用它们来创建一个 User 结构体,并且返回。可以注意到这两行: email: emailusername: username ,非常的扎眼,因为实在有些啰嗦,如果你从 TypeScript 过来,肯定会鄙视 Rust 一番,不过好在,它也不是无可救药:

fn build_user(email:String,username:String)->User{
User{
email,
username,
active:true,
sign_in_count:1,
}
}

# 结构体更新语法:

用已有的 user1 来构建 user2:

let user2=User{
active:user1.active,
username:user1.username,
email:String::from("dwqdqd@example.com"),
sign_in_count:user1.sign_in_count,
};

rust 给的更新语法:

let user2=User{
email:String::from("dwqdqd@example.com");
..user1
};

因为 user2 仅仅在 email 上与 user1 不同,因此我们只需要对 email 进行赋值,剩下的通过结构体更新语法 ..user1 即可完成。

.. 语法表明凡是我们没有显式声明的字段,全部从 user1 中自动获取。

# 需要注意的是 ..user1 必须在结构体的尾部使用。
结构体更新语法跟赋值语句 `=` 非常相像,因此在上面代码中,`user1` 的部分字段所有权被转移到 `user2` 中:`username` 字段发生了所有权转移,作为结果,`user1` 无法再被使用。

聪明的读者肯定要发问了:明明有三个字段进行了自动赋值,为何只有 `username` 发生了所有权转移?

仔细回想一下[所有权](https://course.rs/basic/ownership/ownership.html#拷贝浅拷贝)那一节的内容,我们提到了 `Copy` 特征:实现了 `Copy` 特征的类型无需所有权转移,可以直接在赋值时进行 数据拷贝,其中 `bool` 和 `u64` 类型就实现了 `Copy` 特征,因此 `active` 和 `sign_in_count` 字段在赋值给 `user2` 时,仅仅发生了拷贝,而不是所有权转移。

值得注意的是:`username` 所有权被转移给了 `user2`,导致了 `user1` 无法再被使用,但是并不代表 `user1` 内部的其它字段不能被继续使用,例如:
let user1 = User {
    email: String::from("someone@example.com"),
    username: String::from("someusername123"),
    active: true,
    sign_in_count: 1,
};
let user2 = User {
    active: user1.active,
    username: user1.username,
    email: String::from("another@example.com"),
    sign_in_count: user1.sign_in_count,
};
println!("{}", user1.active);
// 下面这行会报错
println!("{:?}", user1);

# 结构体的内存排列:

#[derive(Debug)]
 struct File {
   name: String,
   data: Vec<u8>,
 }
 fn main() {
   let f1 = File {
     name: String::from("f1.txt"),
     data: Vec::new(),
   };
   let f1_name = &f1.name;
   let f1_length = &f1.data.len();
   println!("{:?}", f1);
   println!("{} is {} bytes long", f1_name, f1_length);
 }

file 结构体在内存中的排列如下图表示:

image-20250330200913250

这个图清晰的看出 File 结构体两个字符 name 和 data 分别拥有两个 [u8] 数组的所有权(Stringl 欸行的底层也是 [u8] 数组),通过 ptr 指针指向底层数组的内存地址,这里你可以把 ptr 指针理解为 Rust 中的引用类型

把结构体中具有所有权的字段转移出去后,将无法再访问该字段,但是可以正常访问其它的字段。

# 元组结构体:

结构体必须有名称,但是结构体的字段可以没有这个名称,这种结构体很想元组,所以称为元组结构体,例如:

struct Color(i32,i32,i32);
struct Point(i32,i32,i32);
let black=Color(0,0,0);
let origin=Point(0,0,0);

元组结构体在你希望有一个整体名称,但是又不关心里面字段的名称时将非常有用。例如上面的 Point 元组结构体,众所周知 3D 点是 (x, y, z) 形式的坐标点,因此我们无需再为内部的字段逐一命名为: x , y , z

# 单元结构体:

还记得之前讲过的基本没啥用的单元类型吧?单元结构体就跟它很像,没有任何字段和属性,但是好在,它还挺有用。

如果你定义一个类型,但是不关心该类型的内容,只关心它的行为时,就可以使用 单元结构体

struct AlwaysEqual;
let subject = AlwaysEqual;
// 我们不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为单元结构体,然后再为它实现某个特征
impl SomeTrait for AlwaysEqual {
}

使用 #[derive (Debug)] 来打印结构体的信息

结构体没有实现 Display 特征,主要原因是由于结构体的复杂性(结构名,子成员的名字,值,所有权归属不知道要输出哪一些),所以用 {} 行不通

我们可以用 {:?} 来进行打印,但是会报错,

error[E0277]: `Rectangle` doesn't implement `Debug`
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`


显示需要添加 #[derive (Debug)] 这条语句

因为,rust 不会为我们实现 Debug,为了实现,我们有两条方式进行选择:

手动实现

使用derive派生实现

后者简单得多,但是也有限制。如下使用就行:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}
fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    println!("rect1 is {:?}", rect1);
}

输出:

$ cargo run
rect1 is Rectangle { width: 30, height: 50 }

我们还可以用 dbg! 来打印

dbg! 输出到标准错误输出 stderr,而 println! 输出到标准输出 stdout。
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}
fn main() {
    let scale = 2;
    let rect1 = Rectangle {
        width: dbg!(30 * scale),
        height: 50,
    };
    dbg!(&rect1);
}
[4.rs:10:16] 30 * scale = 60
[4.rs:14:5] &rect1 = Rectangle {
    width: 60,
    height: 50,
}

这下就全都有了

# 枚举

# 枚举:

例子:

枚举 (enum 或 enumeration) 允许你通过列举可能的成员来定义一个枚举类型,例如扑克牌花色:

enum PokerSuit{
Clubs,
Spades,
Diamonds,
Hearts,
}

枚举类型是一个类型,它会包含所有可能的枚举成员,而枚举值是该类型中的具体某个成员的实例。

# 枚举值:

创建枚举类型的两个成员实例:

let heart=PokerSuit::Hearts;
let diamond=PokerSuit::Diamonds;

我们通过::操作符来访问 PokerSuit 下的具体成员,从代码可以清晰的看出,heart 和 diamond 都是 PokerSuit 枚举类型的,接着可以定义一个函数来使用他们:

#[derive(Debug)]
enum PokerSuit {
    Clubs,
    Spades,
    Diamonds,
    Hearts,
}
fn main() {
    let heart = PokerSuit::Hearts;
    let diamond = PokerSuit::Diamonds;
    print_suit(heart);
    print_suit(diamond);
}
fn print_suit(card: PokerSuit) {
    // 需要在定义 enum PokerSuit 的上面添加上 #[derive (Debug)],否则会报 card 没有实现 Debug
    println!("{:?}",card);
}

print_suit 函数的参数类型是 PokerSuit ,因此我们可以把 heartdiamond 传给它,虽然 heart 是基于 PokerSuit 下的 Hearts 成员实例化的,但是它是货真价实的 PokerSuit 枚举类型。

接下来,我们想让扑克牌变得更加实用,那么需要给每张牌赋予一个值: A (1)- K (13),这样再加上花色,就是一张真实的扑克牌了,例如红心 A。

目前来说,枚举值还不能带有值,因此先用结构体来实现:

enum PokerSuit {
    Clubs,
    Spades,
    Diamonds,
    Hearts,
}
struct PokerCard {
    suit: PokerSuit,
    value: u8
}
fn main() {
   let c1 = PokerCard {
       suit: PokerSuit::Clubs,
       value: 1,
   };
   let c2 = PokerCard {
       suit: PokerSuit::Diamonds,
       value: 12,
   };
}

还有更简单的写法:

enum PokerCard {
    Clubs(u8),
    Spades(u8),
    Diamonds(u8),
    Hearts(u8),
}
fn main() {
   let c1 = PokerCard::Spades(5);
   let c2 = PokerCard::Diamonds(13);
}

再复杂一点:

struct Ipv4Addr {
    // --snip--
}
struct Ipv6Addr {
    // --snip--
}
enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

这里面就是 Ipv4Addr,Ipv6Addr 之类的结构体来定义两种不同的 IP 数据。

这样证明:任何类型的数据都可以放入枚举成员中

增加一些挑战?先看以下代码:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}
fn main() {
    let m1 = Message::Quit;
    let m2 = Message::Move{x:1,y:1};
    let m3 = Message::ChangeColor(255,255,0);
}

该枚举类型代表一条消息,它包含四个不同的成员:

  • Quit 没有任何关联数据
  • Move 包含一个匿名结构体
  • Write 包含一个 String 字符串
  • ChangeColor 包含三个 i32

当然,我们也可以用结构体的方式来定义这些消息:

struct QuitMessage; // 单元结构体
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // 元组结构体
struct ChangeColorMessage(i32, i32, i32); // 元组结构体

由于每个结构体都有自己的类型,因此我们无法在需要同一类型的地方进行使用,例如某个函数它的功能是接受消息并进行发送,那么用枚举的方式,就可以接收不同的消息,但是用结构体,该函数无法接受 4 个不同的结构体作为参数。

而且从代码规范角度来看,枚举的实现更简洁,代码内聚性更强,不像结构体的实现,分散在各个地方。

# 同一化类型:

最后,再用一个实际项目中的简化片段,来结束枚举类型的语法学习。

例如我们有一个 WEB 服务,需要接受用户的长连接,假设连接有两种: TcpStreamTlsStream ,但是我们希望对这两个连接的处理流程相同,也就是用同一个函数来处理这两个连接,代码如下:

fn new (stream: TcpStream) {
  let mut s = stream;
  if tls {
    s = negotiate_tls(stream)
  }
  //websocket 是一个 WebSocket<TcpStream > 或者
  //   WebSocket<native_tls::TlsStream<TcpStream>> 类型
  websocket = WebSocket::from_raw_socket(
    s, ......)
}

此时,枚举类型就能帮上大忙:

enum Websocket {
  Tcp(Websocket<TcpStream>),
  Tls(Websocket<native_tls::TlsStream<TcpStream>>),
}

# option 枚举用于处理空值:

在其它编程语言中,往往都有一个 null 关键字,该关键字用于表明一个变量当前的值为空(不是零值,例如整型的零值是 0),也就是不存在值。当你对这些 null 进行操作时,例如调用一个方法,就会直接抛出 null 异常,导致程序的崩溃,因此我们在编程时需要格外的小心去处理这些 null 空值。

# Rust 的做法 —— Option<T>

Rust 直接去掉了 null ,用 Option<T> 这个枚举类型来表示 “可能有值,也可能没值” 的情况。

rust复制编辑enum Option<T> {
    Some(T), // 代表有值
    None,    // 代表没有值
}
  • Some(T) 代表有值,比如 Some(5) 就表示 5 这个数。
  • None 代表没有值,相当于 null ,但它是 Option<T> 类型的一部分,不能直接使用。

Rust 这样设计的好处是:你必须显式处理可能的空值,防止程序出错!

# 例子:定义 Option<T> 变量
rust复制编辑let some_number = Some(5);
let some_string = Some("Hello");

let absent_number: Option<i32> = None; // 需要指定类型

如果是 None ,Rust 需要知道 Option<T>T 是什么类型,比如 Option<i32> ,否则它不知道 None 代表什么类型。

# 为什么 Option<T>null 好?

因为 Option<T>T 是不同的类型,Rust 不允许直接把 Option<T> 当作普通值使用,这样能防止你意外访问 None

来看这个例子:

rust复制编辑let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y; // ❌ 不能直接相加!

错误信息:

rust


复制编辑
no implementation for `i8 + Option<i8>`

Rust 不允许直接把 Option<i8> 当作 i8 ,必须先处理 Option ,确保它有值。

# 如何取出 Option<T> 的值?

你必须显式地告诉 Rust 如何处理 None ,避免程序崩溃。可以用 match 语句来处理:

rust复制编辑fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1), // 有值时 +1
        None => None,            // 没有值,返回 None
    }
}

let five = Some(5);
let six = plus_one(five); // six 是 Some(6)
let none = plus_one(None); // 仍然是 None

这里 match 确保了:

  • 如果 x 里有值( Some(i) ),就加 1。
  • 如果 xNone ,就保持 None ,避免出错。

# 数组

两种数组:第一种是速度快但是长度固定的 array,一种是可动态增长的 Vector(动态数组)

这两个跟 & str 和 String 很像

数组的三要素:

长度固定
元素必须有相同的类型
依次线性排列
Edited on

Give me a cup of [coffee]~( ̄▽ ̄)~*

odiws WeChat Pay

WeChat Pay

odiws Alipay

Alipay

odiws PayPal

PayPal