主页 > 互联网  > 

【Rust基础①】基本类型、所有权与借用、复合类型

【Rust基础①】基本类型、所有权与借用、复合类型

文章目录 1 基本类型1.1 数值类型1.1.1 Rust 中的内置的整数类型:1.1.2 浮点类型1.1.3 数学运算1.1.4 位运算1.1.5 序列(Range) 1.2 字符、布尔、单元类型1.3 语句和表达式1.4 函数 2 所有权与借用2.1 栈(Stack)与堆(Heap)2.2 所有权原则2.2.1 转移所有权2.2.2 克隆(深拷贝)2.2.3 拷贝(浅拷贝)2.2.4 函数传值与返回 2.3 引用与借用可变引用悬垂引用(Dangling References) 3 复合类型3.1 字符串与切片3.1.1 切片(slice)3.1.2 字符串操作追加(push)插入(insert)替换(replace)删除(delete)连接(concatenate)遍历字符串 3.2 元组用模式匹配解构元组用 `.` 来访问元组 3.3 结构体3.3.1 结构体语法定义结构体创建结构体实例访问结构体字段简化结构体创建 3.3.2 元组结构体(Tuple Struct)3.3.3 单元结构体(Unit-like Struct)3.3.4 使用 `#[derive(Debug)]` 来打印结构体的信息 3.4 枚举3.4.1 枚举的语法3.4.2 Opention枚举处理空值 3.5 数组3.5.1 创建数组3.5.2 数组越界访问3.5.3 数组切片

1 基本类型

示例, 在rust中,变量是默认不可变的。要使变量可变,要用mut修饰

// Rust 程序入口函数,跟其它语言一样,都是 main,该函数目前无返回值 fn main() { // 使用let来声明变量,进行绑定,a是不可变的 // 此处没有指定a的类型,编译器会默认根据a的值为a推断类型:i32,有符号32位整数 // 语句的末尾必须以分号结尾 let a = 10; // 主动指定b的类型为i32 let b: i32 = 20; // 这里有两点值得注意: // 1. 可以在数值中带上类型:30i32表示数值是30,类型是i32 // 2. c是可变的,mut是mutable的缩写 let mut c = 30i32; // 还能在数值和类型中间添加一个下划线,让可读性更好 let d = 30_i32; // 跟其它语言一样,可以使用一个函数的返回值来作为另一个函数的参数 let e = add(add(a, b), add(c, d)); // println!是宏调用,看起来像是函数但是它返回的是宏定义的代码块 // 该函数将指定的格式化字符串输出到标准输出中(控制台) // {}是占位符,在具体执行过程中,会把e的值代入进来 println!("( a + b ) + ( c + d ) = {}", e); } // 定义一个函数,输入两个i32类型的32位有符号整数,返回它们的和 fn add(i: i32, j: i32) -> i32 { // 返回相加值,这里可以省略return i + j } 1.1 数值类型

Rust 每个值都有其确切的数据类型,总的来说可以分为两类:基本类型和复合类型。 基本类型意味着它们往往是一个最小化原子类型,无法解构为其它类型(一般意义上来说),由以下组成:

数值类型: 有符号整数 (i8, i16, i32, i64, isize)、 无符号整数 (u8, u16, u32, u64, usize) 、浮点数 (f32, f64)、以及有理数、复数字符串:字符串字面量和字符串切片 &str布尔类型: true和false字符类型: 表示单个 Unicode 字符,存储为 4 个字节单元类型: 即 () ,其唯一的值也是 () 1.1.1 Rust 中的内置的整数类型: 长度有符号类型无符号类型8 位i8u816 位i16u1632 位i32u3264 位i64u64128 位i128u128视架构而定isizeusize

每个有符号类型规定的数字范围是 − ( 2 n − 1 ) -(2^{n-1}) −(2n−1)~ 2 n − 1 − 1 2^{n-1}-1 2n−1−1,其中 n 是该定义形式的位长度。因此 i8 可存储数字范围是 -(27) ~ 27 - 1,即 -128 ~ 127。无符号类型可以存储的数字范围是 0 ~ 2n - 1,所以 u8 能够存储的数字为 0 ~ 28 - 1,即 0 ~ 255。

此外,isize 和 usize 类型取决于程序运行的计算机 CPU 类型: 若 CPU 是 32 位的,则这两个类型是 32 位的,同理,若 CPU 是 64 位,那么它们则是 64 位。

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

数字字面量示例十进制98_222十六进制0xff八进制0o77二进制0b1111_0000字节 (仅限于 u8)b’A’ 1.1.2 浮点类型

浮点类型数字 是带有小数点的数字,在 Rust 中浮点类型数字也有两种基本类型: f32 和 f64,分别为 32 位和 64 位大小。默认浮点类型是f64,在现代的 CPU 中它的速度与 f32 几乎相同,但精度更高。

当需要使用浮点数时,需遵守以下准则:

避免在浮点数上测试相等性当结果在数学上可能存在未定义时,需要格外的小心

例如:

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 'assertion failed: xyz.0 + xyz.1 == xyz.2', ➥ch2-add-floats.rs.rs:14:5 note: run with `RUST_BACKTRACE=1` environment variable to display ➥a backtrace

对于数学上未定义的结果,例如对负数取平方根-42.1.sqrt(),会产生一个特殊的结果:Rust 的浮点数类型使用 NaN (not a number)来处理这些情况。可以使用 is_nan() 等方法,可以用来判断一个数值是否是 NaN

fn main() { let x = (-42.0_f32).sqrt(); if x.is_nan() { println!("未定义的数学行为") } } 1.1.3 数学运算 fn main() { // 编译器会进行自动推导,给予twenty i32的类型 let twenty = 20; // 类型标注 let twenty_one: i32 = 21; // 通过类型后缀的方式进行类型标注:22是i32类型 let twenty_two = 22i32; // 只有同样类型,才能运算 let addition = twenty + twenty_one + twenty_two; println!("{} + {} + {} = {}", twenty, twenty_one, twenty_two, addition); // 对于较长的数字,可以用_进行分割,提升可读性 let one_million: i64 = 1_000_000; println!("{}", one_million.pow(2)); // 定义一个f32数组,其中42.0会自动被推导为f32类型 let forty_twos = [ 42.0, 42f32, 42.0_f32, ]; // 打印数组中第一个值,并控制小数位为2位 println!("{:.2}", forty_twos[0]); } 1.1.4 位运算

Rust的运算基本上和其他语言一样

运算符说明& 位与相同位置均为1时则为1,否则为0| 位或相同位置只要有1时则为1,否则为0^ 异或相同位置不相同则为1,相同则为0! 位非把位中的0和1相互取反,即0置为1,1置为0<< 左移所有位向左移动指定位数,右位补0>> 右移所有位向右移动指定位数,带符号移动(正数补0,负数补1) 1.1.5 序列(Range)

Rust 提供了一个非常简洁的方式,用来生成连续的数值,例如 1..5,生成从 1 到 4 的连续数字,不包含 5 ;1..=5,生成从 1 到 5 的连续数字,包含 5.

序列只允许用于数字或字符类型

1.2 字符、布尔、单元类型 Rust 的字符不仅仅是 ASCII,所有的 Unicode 值都可以作为 Rust 字符,包括单个的中文、日文、韩文、emoji 表情符号等等,都是合法的字符类型。由于 Unicode 都是 4 个字节编码,因此字符类型也是占用 4 个字节: fn main() { let x = '中'; println!("字符'中'占用了{}字节的内存大小",std::mem::size_of_val(&x)); //4 let your_character:char='8'; // What's your favorite character? // Try a letter, try a number, try a special character, try a character // from a different language than your own, try an emoji! if your_character.is_alphabetic() { println!("Alphabetical!"); } else if your_character.is_numeric() { println!("Numerical!"); } else { println!("Neither alphabetic nor numeric!"); } }

Rust 中的布尔类型有两个可能的值:true 和 false,布尔值占用内存的大小为 1 个字节

单元类型就是 ()。main 函数就返回这个单元类型 (),不能说 main 函数无返回值,因为没有返回值的函数在 Rust 中是有单独的定义的:发散函数( diverge function ),顾名思义,无法收敛的函数。 例如常见的 println!() 的返回值也是单元类型 ()。再比如,可以用 () 作为 map 的值,表示我们不关注具体的值,只关注 key。 可以作为一个值用来占位,但是完全不占用任何内存。

1.3 语句和表达式

Rust 的函数体是由一系列语句组成,最后由一个表达式来返回值,例如:

fn add_with_extra(x: i32, y: i32) -> i32 { let x = x + 1; // 语句 let y = y + 5; // 语句 x + y // 表达式 }

语句会执行一些操作但是不会返回一个值,而表达式会在求值后返回一个值,因此在上述函数体的三行代码中,前两行是语句,最后一行是表达式。

对于 Rust 语言而言,这种基于语句(statement)和表达式(expression)的方式是非常重要的,你需要能明确的区分这两个概念。基于表达式是函数式语言的重要特征,表达式总要返回值。

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

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

上面使用一个语句块表达式将值赋给 y 变量。表达式不能加分号!

表达式如果不返回任何值,会隐式地返回一个 () 。

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

if 是最基本的控制流语句,使用方法:

pub fn foo_if_fizz(fizzish: &str) -> &str { if fizzish == "fizz" { "foo" } else if fizzish=="fuzz" { "bar" }else { "baz" } } 1.4 函数

函数要点:

函数名和变量名使用蛇形命名法(snake case),例如 fn add_two() -> {}函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可每个函数参数都需要标注类型

Rust 是强类型语言,因此需要你为每一个函数参数都标识出它的具体类型。

特殊返回类型 - 单元类型 (),是一个零长度的元组。它没啥作用,但是可以用来表达一个函数没有返回值:

函数没有返回值,那么返回一个()通过;结尾的表达式返回一个 () //显示返回() fn clear(text: &mut String) -> () { *text = String::from(""); }

当用!作函数返回类型的时候,表示该函数永不返回( diverge function ),特别的,这种语法往往用做会导致程序崩溃的函数。

函数示例:

fn main() { let original_price = 51; println!("Your sale price is {}", sale_price(original_price)); } fn sale_price(price: i32) ->i32 { if is_even(price) { price - 10 } else { price - 3 } } fn is_even(num: i32) -> bool { num % 2 == 0 } 2 所有权与借用

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

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

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

2.1 栈(Stack)与堆(Heap)

堆和栈是编程语言最核心的数据结构,核心目标就是为程序在运行时提供可供使用的内存空间。

栈按照顺序存储值并以相反顺序取出值,这也被称作后进先出。

与栈不同,对于大小未知或者可能变化的数据,我们需要将它存储在堆上。当向堆上放入数据时,需要请求一定大小的内存空间。操作系统在堆的某处找到一块足够大的空位,把它标记为已使用,并返回一个表示该位置地址的指针, 该过程被称为在堆上分配内存,有时简称为 “分配”(allocating)。接着,该指针会被推入栈中,因为指针的大小是已知且固定的,在后续使用过程中,你将通过栈中的指针,来获取数据在堆上的实际内存位置,进而访问该数据。

因此,入栈比在堆上分配内存要快,因为入栈时操作系统无需分配新的空间,只需要将新数据放入栈顶即可;栈数据往往可以直接存储在 CPU 高速缓存中,而堆数据只能存储在内存中。访问堆上的数据比访问栈上的数据慢,因为必须先访问栈再通过栈上的指针来访问内存。

2.2 所有权原则

Rust中关于所有权的规则:

Rust 中每一个值都被一个变量所拥有,该变量被称为值的所有者一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者当所有者(变量)离开作用域范围时,这个值将被丢弃(drop) 2.2.1 转移所有权

String 类型是一个复杂类型,由存储在栈中的堆指针、字符串长度、字符串容量共同组成,其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存,容量是堆内存分配空间的大小,长度是目前已经使用的大小。总之 String 类型指向了一个堆上的空间,这里存储着它的真实数据,例如:

let s1 = String::from("hello"); let s2 = s1;

注意,当变量离开作用域后,Rust 会自动调用 drop 函数并清理变量的堆内存。不过由于两个 String 变量指向了同一位置。这就有了一个问题:当 s1 和 s2 离开作用域,它们都会尝试释放相同的内存。这是一个叫做 二次释放(double free) 的错误,也是之前提到过的内存安全性 BUG 之一。两次释放(相同)内存会导致内存污染,它可能会导致潜在的安全漏洞。

Rust 这样解决该问题:当 s1 赋予 s2 后,Rust 认为 s1 不再有效,因此也无需在 s1 离开作用域后 drop 任何东西,这就是把所有权从 s1 转移给了 s2,s1 在被赋予 s2 后就马上失效了。拷贝指针、长度和容量而不拷贝数据听起来就像浅拷贝,但是又因为 Rust 同时使第一个变量 s1 无效了,因此这个操作被称为 移动(move),而不是浅拷贝。上面的例子可以解读为 s1 被移动到了 s2 中

Rust 永远也不会自动创建数据的 “深拷贝”。因此,任何自动的复制都不是深拷贝,可以被认为对运行时性能影响较小。

2.2.2 克隆(深拷贝)

如果我们确实需要深度复制 String 中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone 的方法。但是对于执行较为频繁的代码,使用 clone 会极大的降低程序性能。

fn main() { let s1 = String::from("hello"); let s2 = s1.clone(); println!("s1 = {}, s2 = {}", s1, s2); } 2.2.3 拷贝(浅拷贝)

Rust 有一个叫做 Copy 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy 特征,一个旧的变量在被赋值给其他变量后仍然可用。

那么什么类型是可 Copy 的呢?可以查看给定类型的文档来确认,不过作为一个通用的规则: 任何基本类型的组合可以 Copy ,不需要分配内存或某种形式资源的类型是可以 Copy 的。如下是一些 Copy 的类型:

所有整数类型,比如 u32。布尔类型,bool,它的值是 true 和 false。所有浮点数类型,比如 f64。字符类型,char。元组,当且仅当其包含的类型也都是 Copy 的时候。比如,(i32, i32) 是 Copy 的,但 (i32, String) 就不是。不可变引用 &T ,例如转移所有权中的最后一个例子,但是注意: 可变引用 &mut T 是不可以 Copy的 2.2.4 函数传值与返回

将值传递给函数,一样会发生 移动 或者 复制,就跟 let 语句一样,下面的代码展示了所有权、作用域的规则:

fn main() { let s1 = gives_ownership(); // gives_ownership 将返回值 // 移给 s1 let s2 = String::from("hello"); // s2 进入作用域 let s3 = takes_and_gives_back(s2); // s2 被移动到 // takes_and_gives_back 中, // 它也将返回值移给 s3 } // 这里, s3 移出作用域并被丢弃。s2 也移出作用域,但已被移走, // 所以什么也不会发生。s1 移出作用域并被丢弃 fn gives_ownership() -> String { // gives_ownership 将返回值移动给 // 调用它的函数 let some_string = String::from("hello"); // some_string 进入作用域. some_string // 返回 some_string 并移出给调用的函数 } // takes_and_gives_back 将传入字符串并返回该值 fn takes_and_gives_back(a_string: String) -> String { // a_string 进入作用域 a_string // 返回 a_string 并移出给调用的函数 } 2.3 引用与借用

Rust 通过获取变量的引用,称之为借用(borrowing)。正如现实生活中,如果一个人拥有某样东西,你可以从他那里借来,当使用完毕后,也必须要物归原主。

fn main() { let x = 5; let y = &x; assert_eq!(5, x);//错误,不同类型的比较 assert_eq!(5, *y);//使用 *y 来解出引用所指向的值(也就是解引用)。一旦解引用了 y,就可以访问 y 所指向的整型值并可以与 5 做比较。 } 可变引用

首先,声明 s 是可变类型,其次创建一个可变的引用 &mut s 和接受可变引用参数 some_string: &mut String 的函数。

fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }

同一作用域,特定数据只能有一个可变引用,这种限制的好处就是使 Rust 在编译期就避免数据竞争,数据竞争可由以下行为造成:

两个或更多的指针同时访问同一数据至少有一个指针被用来写入数据没有同步数据访问的机制

可以通过手动限制变量的作用域:

let mut s = String::from("hello"); { let r1 = &mut s; } // r1 在这里离开了作用域,所以我们完全可以创建一个新的引用 let r2 = &mut s;

可变引用与不可变引用不能同时存在,比如

let mut s = String::from("hello"); let r1 = &s; // 没问题 let r2 = &s; // 没问题 let r3 = &mut s; // 大问题 println!("{}, {}, and {}", r1, r2, r3);

其实这个也很好理解,正在借用不可变引用的用户,肯定不希望他借用的东西,被另外一个人莫名其妙改变了。多个不可变借用被允许是因为没有人会去试图修改数据,每个人都只读这一份数据而不做修改,因此不用担心数据被污染。

注意,引用的作用域 s 从创建开始,一直持续到它最后一次使用的地方,这个跟变量的作用域有所不同,变量的作用域从创建持续到某一个花括号 }

悬垂引用(Dangling References)

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

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 的 所有权被转移给外面的调用者。

3 复合类型 3.1 字符串与切片 3.1.1 切片(slice)

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

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

这就是创建切片的语法,使用方括号包括的一个序列:[开始索引…终止索引],其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,也就是这是一个 右半开区间。在切片数据结构内部会保存开始的位置和切片的长度,其中长度是通过 终止索引 - 开始索引 的方式计算得来的。

对于 let world = &s[6..11]; 来说,world 是一个切片,该切片的指针指向 s 的第 7 个字节(索引从 0 开始, 6 是第 7 个字节),且该切片的长度是 5 个字节。 截取完整的String切片:

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

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

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

因为我们只取 s 字符串的前两个字节,但是本例中每个汉字占用三个字节,因此没有落在边界处,也就是连 中 字都取不完整,此时程序会直接崩溃退出,如果改成 &s[0..3],则可以正常通过编译。

因为切片是对集合的部分引用,因此不仅仅字符串有切片,其它集合类型也有,例如数组:

let a = [1, 2, 3, 4, 5]; let slice = &a[1..3]; assert_eq!(slice, &[2, 3]);

该数组切片的类型是 &[i32],数组切片和字符串切片的工作方式是一样的,例如持有一个引用指向原始数组的某个元素和长度。

字符串是由字符组成的连续集合,Rust 中的字符是 Unicode 类型,因此每个字符占据 4 个字节内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4),这样有助于大幅降低字符串所占用的内存空间。Rust 在语言级别,只有一种字符串类型: str,它通常是以引用类型出现 &str,也就是上文提到的字符串切片。但在标准库中String 则是一个可增长、可改变且具有所有权的 UTF-8 编码字符串,当 Rust 用户提到字符串时,往往指的就是 String 类型和 &str 字符串切片类型,这两个类型都是 UTF-8 编码。

String与&str类型之间的转换:

fn main() { let s = String::from("hello,world!"); //等同于 let s="hello,world!".to_string() say_hello(&s); //String转&str say_hello(&s[..]); say_hello(s.as_str()); } fn say_hello(s: &str) { println!("{}",s); } 3.1.2 字符串操作 追加(push)

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

fn main() { let mut s = String::from("Hello "); s.push('r'); //Hello r s.push_str("ust!"); //Hello rust! } 插入(insert)

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

fn main() { let mut s = String::from("Hello rust!"); s.insert(5, ','); //Hello, rust! s.insert_str(6, " I like"); //Hello, I like rust! } 替换(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

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)

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 关键字修饰。

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 就会发生错误。

2、使用 format! 连接字符串

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

示例代码如下:

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

代码运行结果:

hello rust! 遍历字符串

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

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

输出如下

中 国 人

2、按字节遍历,返回字符串的底层字节数组表现形式:

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

输出如下:

228 184 173 229 155 189 228 186 186 3.2 元组

元组是由多种类型组合到一起形成的,因此它是复合类型,元组的长度是固定的,元组中元素的顺序也是固定的。

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

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

变量 tup 被绑定了一个元组值 (500, 6.4, 1),该元组的类型是 (i32, f64, u8)

用模式匹配解构元组

用同样的形式把一个复杂对象中的值匹配出来。

fn main() { let tup = (500, 6.4, 1); let (x, y, z) = tup; println!("The value of y is: {}", y); } 用 . 来访问元组

模式匹配可以让我们一次性把元组中的值全部或者部分获取出来,如果只想要访问某个特定元素,那模式匹配就略显繁琐,对此,Rust 提供了 . 的访问方式,元组的索引从 0 开始:

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

可以通过使用元组使函数返回多个值:

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) } 3.3 结构体 3.3.1 结构体语法 定义结构体

一个结构体由几部分组成:

通过关键字 struct 定义一个清晰明确的结构体 名称几个有名字的结构体 字段 struct User { active: bool, username: String, email: String, sign_in_count: u64, }

该结构体名称是 User,拥有 4 个字段,且每个字段都有对应的字段名及类型声明,例如 username 代表了用户名,是一个可变的 String 类型。

创建结构体实例

创建 User 结构体的实例:

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

有几点值得注意:

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

必须要将结构体实例声明为可变的,才能修改其中的字段,Rust 不支持将某个结构体某个字段标记为可变。

user1.email = String::from("anotheremail@example "); 简化结构体创建

当函数参数和结构体字段同名时,可以直接使用缩略的方式进行初始化

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

根据已有的结构体实例,创建新的结构体实例,例如根据已有的 user1 实例来构建 user2,Rust 为我们提供了 结构体更新语法:

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

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

.. 语法表明凡是我们没有显式声明的字段,全部从 user1 中自动获取。需要注意的是 ..user1 必须在结构体的尾部使用。

结构体更新语法跟赋值语句 = 非常相像,因此在上面代码中,user1 的部分字段所有权被转移到 user2 中:username 字段发生了所有权转移,作为结果,user1 无法再被使用。值得注意的是:username 所有权被转移给了 user2,导致了 user1 无法再被使用,但是并不代表 user1 内部的其它字段不能被继续使用:

let user1 = User { email: String::from("someone@example "), username: String::from("someusername123"), active: true, sign_in_count: 1, }; let user2 = User { active: user1.active, username: user1.username, email: String::from("another@example "), sign_in_count: user1.sign_in_count, }; println!("{}", user1.active); // 下面这行会报错 println!("{:?}", user1);

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

3.3.2 元组结构体(Tuple Struct)

结构体必须要有名称,但是结构体的字段可以没有名称,这种结构体长得很像元组,因此被称为元组结构体,例如:

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。

3.3.3 单元结构体(Unit-like Struct)

单元结构体没有任何字段和属性,但是好在,它还挺有用。如果你定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用 单元结构体:

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

如果想在结构体中使用一个引用,就必须加上生命周期。

3.3.4 使用 #[derive(Debug)] 来打印结构体的信息 #[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 }

当结构体较大时,我们可能希望能够有更好的输出表现,此时可以使用 {:#?} 来替代 {:?},输出如下:

rect1 is Rectangle { width: 30, height: 50, }

还有一个简单的输出 debug 信息的方法,那就是使用 dbg! 宏,它会拿走表达式的所有权,然后打印出相应的文件名、行号等 debug 信息,当然还有我们需要的表达式的求值结果。除此之外,它最终还会把表达式值的所有权返回!

dbg!` 输出到标准错误输出 `stderr`,而 `println!` 输出到标准输出 `stdout

下面的例子中清晰的展示了 dbg! 如何在打印出信息的同时,还把表达式的值赋给了 width:

#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let scale = 2; let rect1 = Rectangle { width: dbg!(30 * scale), height: 50, }; dbg!(&rect1); }

最终的 debug 输出如下:

$ cargo run [src/main.rs:10] 30 * scale = 60 [src/main.rs:14] &rect1 = Rectangle { width: 60, height: 50, } 3.4 枚举 3.4.1 枚举的语法

枚举(enum 或 enumeration)允许通过列举可能的成员来定义一个枚举类型,枚举类型是一个类型,它会包含所有可能的枚举成员, 而枚举值是该类型中的具体某个成员的实例。 如:

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

结构体实现带值的枚举成员:

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(char), Hearts(char), } fn main() { let c1 = PokerCard::Spades(5); let c2 = PokerCard::Diamonds('A'); }

任何类型的数据都可以放入枚举成员中: 例如字符串、数值、结构体甚至另一个枚举。以下代码:

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 3.4.2 Opention枚举处理空值

Option 枚举包含两个成员,一个成员表示含有值:Some(T), 另一个表示没有值:None,定义如下:

enum Option<T> { Some(T), None, }

其中 T 是泛型参数,Some(T)表示该枚举成员的数据类型是 T,换句话说,Some 可以包含任何类型的数据。Option<T> 和 T(这里 T 可以是任何类型)是不同的类型。

为了使用 Option<T> 值,需要编写处理每个成员的代码。match 表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。

fn plus_one(x: Option<i32>) -> Option<i32> { match x { None => None, Some(i) => Some(i + 1), } } let five = Some(5); let six = plus_one(five); let none = plus_one(None);

plus_one 通过 match 来处理不同 Option 的情况。

3.5 数组

在 Rust 中,最常用的数组有两种,第一种是速度很快但是长度固定的 array,第二种是可动态增长的但是有性能损耗的 Vector。这两个数组的关系跟 &str 与 String 的关系很像,前者是长度固定的字符串切片,后者是可动态增长的字符串。其实,在 Rust 中无论是 String 还是 Vector,它们都是 Rust 的高级类型:集合类型。数组的具体定义很简单:将多个类型相同的元素依次组合在一起,就是一个数组。数组声明格式为:[类型; 长度]。结合上面的内容,可以得出数组的三要素:

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

这里说的数组是 Rust 的基本类型,是固定长度的,这点与其他编程语言不同,其它编程语言的数组往往是可变长度的,与 Rust 中的动态数组 Vector 类似

3.5.1 创建数组 fn main() { //let a = [1, 2, 3, 4, 5]; 或者 let a: [i32; 5] = [1, 2, 3, 4, 5]; //包含5个i32类型的数 let b = [3; 5]; //数组b中包含5个3 } 3.5.2 数组越界访问

下面是一个接收用户的控制台输入,然后将其作为索引访问数组元素的例子:

use std::io; fn main() { let a = [1, 2, 3, 4, 5]; println!("Please enter an array index."); let mut index = String::new(); // 读取控制台的输出 io::stdin() .read_line(&mut index) .expect("Failed to read line"); let index: usize = index .trim() .parse() .expect("Index entered was not a number"); let element = a[index]; println!( "The value of the element at index {} is: {}", index, element ); }

使用 cargo run 来运行代码,因为数组只有 5 个元素,如果我们试图输入 5 去访问第 6 个元素,则会访问到不存在的数组元素,最终程序会崩溃退出:

Please enter an array index. 5 thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 5', src/main.rs:19:19 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

这就是数组访问越界,访问了数组中不存在的元素,导致 Rust 运行时错误。

当你尝试使用索引访问元素时,Rust 将检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度,Rust 会出现 *panic*。这种检查只能在运行时进行,比如在上面这种情况下,编译器无法在编译期知道用户运行代码时将输入什么值,这种就是 Rust 的安全特性之一。

数组元素为非基础类型时,应调用std::array::from_fn

let array: [String; 8] = core::array::from_fn(|i| String::from("rust is good!")); println!("{:#?}", array); 3.5.3 数组切片

切片允许你引用集合中的部分连续片段,而不是整个集合,对于数组也是,数组切片允许我们引用数组的一部分:

let a: [i32; 5] = [1, 2, 3, 4, 5]; let slice: &[i32] = &a[1..3]; assert_eq!(slice, &[2, 3]);

上面的数组切片 slice 的类型是&[i32],与之对比,数组的类型是[i32;5],简单总结下切片的特点:

切片的长度可以与数组不同,并不是固定的,而是取决于你使用时指定的起始和结束位置创建切片的代价非常小,因为切片只是针对底层数组的一个引用切片类型[T]拥有不固定的大小,而切片引用类型&[T]则具有固定的大小,因为 Rust 很多时候都需要固定大小数据类型,因此&[T]更有用,&str字符串切片也同理

数组综合示例:

fn main() { // 编译器自动推导出one的类型 let one = [1, 2, 3]; // 显式类型标注 let two: [u8; 3] = [1, 2, 3]; let blank1 = [0; 3]; let blank2: [u8; 3] = [0; 3]; // arrays是一个二维数组,其中每一个元素都是一个数组,元素类型是[u8; 3] let arrays: [[u8; 3]; 4] = [one, two, blank1, blank2]; // 借用arrays的元素用作循环中 for a in &arrays { print!("{:?}: ", a); // 将a变成一个迭代器,用于循环 // 你也可以直接用for n in a {}来进行循环 for n in a.iter() { print!("\t{} + 10 = {}", n, n+10); } let mut sum = 0; // 0..a.len,是一个 Rust 的语法糖,其实就等于一个数组,元素是从0,1,2一直增加到到a.len-1 for i in 0..a.len() { sum += a[i]; } println!("\t({:?} = {})", a, sum); } }

运行结果:

[1, 2, 3]: 1 + 10 = 11 2 + 10 = 12 3 + 10 = 13 ([1, 2, 3] = 6) [1, 2, 3]: 1 + 10 = 11 2 + 10 = 12 3 + 10 = 13 ([1, 2, 3] = 6) [0, 0, 0]: 0 + 10 = 10 0 + 10 = 10 0 + 10 = 10 ([0, 0, 0] = 0) [0, 0, 0]: 0 + 10 = 10 0 + 10 = 10 0 + 10 = 10 ([0, 0, 0] = 0)

数组虽然很简单,但是其实还是存在几个要注意的点:

数组类型容易跟数组切片混淆,[T;n]描述了一个数组的类型,而[T]描述了切片的类型, 因为切片是运行期的数据结构,它的长度无法在编译期得知,因此不能用[T;n]的形式去描述[u8; 3]和[u8; 4]是不同的类型,数组的长度也是类型的一部分在实际开发中,使用最多的是数组切片[T],我们往往通过引用的方式去使用&[T],因为后者有固定的类型大小
标签:

【Rust基础①】基本类型、所有权与借用、复合类型由讯客互联互联网栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“【Rust基础①】基本类型、所有权与借用、复合类型