Code Monkey home page Code Monkey logo

rust_learn's People

Contributors

chenmoonmo avatar

Watchers

 avatar

rust_learn's Issues

2.5 流程控制

if else

fn main() {
	if condition == true {
    // A...
	} else {
    // B...
	}
}

注意点:

  • if 语句块是表达式,这里我们使用 if 表达式的返回值来给 number 进行赋值:number 的值是 5
  • 用 if 来赋值时,要保证每个分支返回的类型一样(事实上,这种说法不完全准确,见这里),此处返回的 5 和 6 就是同一个类型,如果返回类型不一致就会报错

循环控制

for 循环

fn main() {
    for i in 1..=5 {
        println!("{}", i);
    }
}

以上代码循环输出一个从 1 到 5 的序列,for 元素 in 集合。在使用 for 时通常使用集合的引用形式,如果不使用引用的话,所有权会被转移到 for 语句块中,后续无法再使用此集合。

对于实现了 copy 特征的数组(例如 [i32; 10] )而言, for item in arr 并不会把 arr 的所有权转移,而是直接对其进行了拷贝,因此循环之后仍然可以使用 arr 。

如果在循中想要修改元素,可以使用 mut 关键字:

fn main() {
	for item in &mut collection {
	  // ...
	}
}

for item in collection 等价于 for item in IntoIterator::into_iter(collection)
for item in &collection 等价于 for item in collection.iter()
for item in &mut collection 等价于 for item in collection.iter_mut()

如果想在循环中获取元素的索引:

fn main() {
	let a = [4,3,2,1];
	for (i,v) in a.iter().enumerate() {
		println!("第{}个元素是{}", i + 1, v);
	}
}

不声明变量的循环:

fn main() {
	for _ in 0..10 {
  // ...
	}
}

continue

使用 continue 可以跳过当前当次的循环,开始下次的循环:

break

使用 break 可以直接跳出当前整个循环

while 循环

当条件为 true 时,继续循环,条件为 false,跳出循环。

loop 循环

 loop 就是一个简单的无限循环,你可以在内部实现逻辑通过 break 关键字来控制循环何时结束。
 - break 可以单独使用,也可以带一个返回值,有些类似 return

  • loop 是一个表达式,因此可以返回一个值
fn main() {
	let counter = 0;
	let res = loop {
		counter += 1;
		if counter === 10 {
			break counter * 2;
		}
	}
}

2.7 方法

定义方法

在其他的语言中,方法往往存在于 Class 中,但是在 Rust 中,对象的定义和方法的定义是分离的。方法往往和结构体,枚举,特征一起使用,在Rust中 使用 impl 来定义方法:

struct Circle {
	x: f64,
	y: f64,
	radius: f64,
}

impl Circle {
	// new是Circle的关联函数,因为它的第一个参数不是self,且new并不是关键字 
	// 这种方法往往用于初始化当前结构体的实例
	fn new(x: f64, y: f64, radius: f64) -> Circle {
		Circle {
			x,
			y,
			radius
		}
	}
	// Circle的方法,&self表示借用当前的Circle结构体 
	fn area(&self) -> f64 { 
		std::f64::consts::PI * (self.radius * self.radius)
	 }
}

impl 和结构体会自动关联

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

impl Rectangle 表明为 Rectangle 实现方法,这样的写法表示 impl 语句块中的一切都是跟 Rectangle 相关联的。

self, &self 和 &mut self

impl 内部,我们使用 &self 来替代本来的参数名和类型, &self 实际是 self: &Self 的简写,在一个 impl 块内,Self 指代被实现方法的结构体类型,self 指代此类型的实例。
需要注意的是,self 依然有所有权的概念:

  • self 表示 Rectangle 的所有权转移到该方法中,这种形式用的较少
  • &self 表示该方法对 Rectangle 的不可变借用
  • &mut self 表示可变借用

方法名和结构体字段名相同

在 Rust 中,允许方法名跟结构体的字段名相同,我们使用 () 调用是,调用的是它的方法,使用 . 访问时,访问的是它的字段。
这样可以让我们实现一个 getter , 可以将字段设置为私有字段,而使用方法来访问,当用户尝试访问字段时,就会报错。

关联函数

impl 中且没有 self 的函数被称之为关联函数,关联函数没有 self 不能使用 . 调用,只能使用 :: 来调用,例如经常使用的 String::from(),这些方法一般用来做构造器。

关联函数类似于静态方法,方法类似于成员方法。

多个 impl 定义

Rust 中可以为一个结构体定义多个 impl 块,可以提供更多的灵活性和代码组织性。

为枚举实现方法

枚举也可以像结构体一样实现方法

enum Message {
	Quit,
	Move {x: i32, y: i32},
	Write(String),
	ChangeColor(i32, i32, i32)
}

impl Message {
	fn call(&self) {
		
	}
}

fn main() {
	let m = Message::Wirte(String::from("hello"));
	m.call();
}

2.1 变量绑定和解构

变量绑定

使用 let a = "hello world" 来绑定变量,将内存对象的所有权交给一个变量。

所有权 :所有的内存对象都是有主人的,就像现实世界的物品那样。

变量可变性

变量的值默认不能改变。但是可以通过 mut 关键字让变量变为可变变量。

fn main {
	let mut x = 5;
	x = 6;
}

忽略未使用的变量

未使用的变量需要通过带有 _ 前缀的变量名来创建。

fn main {
	let _y = 6;
	let x = 5; // 会报错
}

变量解构

fn main() {
	let (a,mut b):(boolean,boolean) = (true,false);
	// a = true; b = false
	println!("a = {:?}, b = {:?}", a, b);
}

解构赋值

struct Struct { e: i32 }

fn main() {
	let (a,b,c,d,e);
	(a,b) = (1,2);
	[c,...,d,_] = [1,2,3,4,5];
	Struct { e, .. } = Struct { e: 5 };
}

常量

常量使用 const 关键字来定义,不能使用 mut ,从始至终都不可改变,而且常量的值必须标注类型。

fn main() {
	const MAX_POINTS: u32 = 100_000;
}

变量遮蔽

Rust 中可以声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的。

fn main() {
	let x = 5;
	let x = x + 1;
	{
		let x = x * 2; // 12
		println!({},x);
	}
	println!({},x); // 6
}

2.8 泛型和特征

泛型

泛型的出现是为了支持函数的多态,比如对于数的加法来讲,在不支持泛型的语言中,需要为每个数字类型单独编写函数。但是支持泛型后,就可以简化代码,用一个函数支持不同的类型

fn add<T> (a:T,b:T) -> T {
	a + b
}
fn main() {
 add(20,20);
 add(1.2,1.2);
}

泛型详解

T 是泛型参数,他的名称不受限制,使用泛型参数前,必须先对其进行声明。例如我们有一个方法,是在列表中找出最大的值,这个函数接受一个 元素为T 的数组切片,它的返回值也是 T

fn largest<T>(list: &[T]) -> T {
	let mut largest = list[0];

	for &item in list {
		if item > largest {
			largest = item
		}
	}
	
	largest
}

但是并不是所有的类型都是可以使用 > 来进行比较的,所以我们需要使用 特征 来为泛型加上类型限制。

在结构体内使用泛型

大体和 TS 类似

struct Point<T,U> {
	x: T,
	y: U,
}

在枚举中使用泛型

fn main() {
	enum Option<T> {
	    Some(T),
	    None,
	}
	enum Result<T, E> {
	    Ok(T),
	    Err(E),
	}
}

在方法中使用泛系

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

在方法中使用泛型,需要提前在 impl<T> 声明,后面的 Point<T> 不再是泛型声明,而是一个完整的结构体类型。
除了结构体泛型外,我们还能为结构体的方法定义额外的泛型。

为具体的泛型实现方法

对于 Point<T> 泛型,我们可以为具体的类型定义方法,例如下面的方法,当泛型为 i32 类型时会有一个名为 distance_from_origin 的方法,而其他类型的 Point 则不具备这个方法。

impl Ponit<i32> {
		fn distance_from_origin(&self) -> f32 { 
			(self.x.powi(2) + self.y.powi(2)).sqrt() 
		}
}

const 泛型

对于数组来说,[i32, 2][i32, 3] 是完全不同的数组类型。如果我们编写一个针对数组的函数,可以通过数组的不可变切片引用来实现。但是有一种针对值的 const 泛型,可以处理数组的长度问题。

fn get_last<T: Copy, const N: usize>(arr: &[T; N]) -> T {
	let len = arr.len();
	arr[len-1]
}

fn main() {
	let arr: [i32; 2] = [3; 2];
	get_last(&arr);
}

const 泛型表达式

fn something<T>(val: T) where Assert<{ core::mem::size_of::<T>() < 768 }>: IsTrue, 
{ 
	//
}

2.4 复合类型

复合类型

复合类型是由其他类型组合而成的,例如结构体 struct 和枚举 enum

字符串

fn main() {
  let my_name = "Pascal";
  greet(my_name);
}

fn greet(name: String) {
  println!("Hello, {}!", name);
}

上面的这段代码,在编译时会报错,greet 函数需要一个 String 类型的参数,却传递了一个 &str
类型。

error[E0308]: mismatched types
 --> src/main.rs:3:11
  |
3 |     greet(my_name);
  |           ^^^^^^^
  |           |
  |           expected struct `std::string::String`, found `&str`
  |           help: try using a conversion method: `my_name.to_string()`

error: aborting due to previous error

切片(slice)

切片允许我们使用集合中连续的元素序列,而不是引用整个集合。而字符串就是切片对 String 类型集合中的某一部分的引用。类似于:

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

可以通过 [开始索引..终止索引] 的方式创建()一个切片,其中开始索引是切片中第一个元素的索引位置,而终止索引是最后一个元素后面的索引位置,也就是这是一个 右半开区间
一些特殊的截取规则:

fn main() {
	let s = String::from("hello");
	// 省略索引 0
	let s1 = &s[..3]; // 'hel'
	
	// 截取到最后一个字节
	let s2 = &s[3..]; // 'lo'
	// 或
	let len = s.len();
	let s3 = &s[3..len];

	// 截取完整字节
	let s4 = &s[0..len];
	// 或
	let s5 = &s[..];
}

但切片在处理 utf-8 字符串时,切片的索引必须落在字符之间的边界位置,也就是 UTF-8 字符的边界,否则代码会崩溃。
字符串切片的类型标识符是 &strlet a = [1,3,4,5] 这样的数组的切片类型是 &[i32]

字符串字面量切片

字符串字面量的类型是字符串切片 &str&str 是一个不可变引用。

Rust 中的字符串

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

String 和 &str 的转换

  • String&str :
    • `String::form("hello,world")
    • "hello,world".to_string()
  • &strString : 使用引用
     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);
     }

字符串索引访问

Rust 中无法通过索引访问字符串。

字符串切片

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

字符串操作

插入

  • insert() :接受两个参数,插入索引的位置,插入的字符。
  • insert_str() :接受两个参数,插入索引的位置,插入的字符串

这两个方法都会修改原字符串,因此需要可变变量才能调用。

替换
  • replace(): 接受两个参数*,第一个是要被替换的字符串,第二个是新的字符串
  • replacen():接收三个参数,前两个参数与 replace() 方法一样,第三个参数则表示替换的个数。

这两个方法适用于 String&str 类型,返回一个新的字符串而不是操作原来的字符串。

  • replace_range():接收两个参数,第一个参数是要替换字符串的范围(一个range,.. 表示),第二个参数是新的字符串。

这个方法直接操作原来的字符串,不会返回新的字符串。该方法需要使用 mut 关键字修饰。

删除

  • pop(): 删除并返回字符串的最后一个字符,直接修改原来的字符串。
  • remove():删除并返回指定位置的字符,直接修改原来的字符串。
  • truncate():删除字符串中从指定位置开始到结尾的全部字符,直接修改原来的字符串。
  • clear():清空字符串,直接修改原来的字符串。

连接

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

字符串转义

通过\ 转义字符串,输出 ASCII 和 Unicode 字符。

操作 UTF-8 字符串

字符

以 Unicode 字符的方式遍历字符串,最好的办法是使用 chars 方法

fn main() {
	for c in "**人".chars() {
	    println!("{}", c);
	}
}
字节

使用 bytes 方法可以返回字符串的底层字节数组表现形式

fn main() {
	for b in "**人".bytes() {
	    println!("{}", b);
	}
}
获取子串

标准库无法完成这个操作,crates.io 寻找第三方库支持。

2.2 基本类型

基本类型

Rust 中的类型分为基本类型和复合类型,基本类型往往是一个最小化原子类型,无法结构为其他类型。

  • 数值类型
    • 有符号整数: i8i16i32i64isize
    • 无符号整数: u8u16u32u64usize
    • 浮点数:f32f64
    • 有理数
    • 复数
  • 字符串:字符串字面量和字符串切片 &str
  • 布尔类型:truefalse
  • 字符类型:表示单个 Unicode 字符,存储为 4 个字节
  • 单元类型:即 () ,其唯一的值也是 ()

类型推导与标注

Rust 是一门静态类型语言,编译器在编译期必须知道所有变量的类型,但 Rust 编译器可以根据变量的值和上下文中的使用方法来推导出变量的类型,但是在某些情况下无法推导出变量类型,需要我们手动标注类型。

数值类型

整数类型

整数类型的定义形式为: 有无符号 + 类型大小(位数)。有符号数字为 i (integer),无符号数字为 (unsigned)。
有符号数字的数字范围为 -(2^(n - 1)) ~ 2^(n - 1) - 1,无符号数字的范围为 0 ~ 2^(n - 1)。
i8 类型的范围为 -2^7 ~ 2^7 - 1 ,u8 类型的范围为 0 ~ 2^7 - 1 。
isizeusize 则视 CPU 架构而定,分为 32 位或 64 位。

Rust 的整数默认使用 i32isizeusize 主要的应用场景是用作集合的索引。

整型溢出

假设有一个 u8 ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生整型溢出。关于这一行为 Rust 有一些有趣的规则:当在 debug 模式编译时,Rust 会检查整型溢出,若存在这些问题,则使程序在编译时 panic(崩溃,Rust 使用这个术语来表明程序因错误而退出)。

在当使用 --release 参数进行 release 模式构建时,Rust 检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出(two’s complement wrapping)的规则处理。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 u8 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖这种默认行为的代码都应该被认为是错误的代码。

要显式处理可能的溢出,可以使用标准库针对原始数字类型提供的这些方法:

  • 使用 wrapping_* 方法在所有模式下都按照补码循环溢出规则处理,例如 wrapping_add
  • 如果使用 checked_* 方法时发生溢出,则返回 None 值
  • 使用 overflowing_* 方法返回该值和一个指示是否存在溢出的布尔值
  • 使用 saturating_* 方法使值达到最小值或最大值
fn main() {
    let a : u8 = 255;
    let b = a.wrapping_add(20);
    println!("{}", b);  // 19
}

浮点类型

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

fn main() {
    let x = 2.0; // f64
    let y: f32 = 3.0; // f32
}

浮点数根据 IEEE-754 标准实现。f32 类型是单精度浮点型,f64 为双精度。

序列

序列通过一个简洁的方式用来生成连续数值,例如 1..5 会生成 1 到 4 不包含 5 的连续数字,1..=5 生成从 1 到 5 的连续数字。
序列还可以用于字符类型,用来生成连续的字符。

fn main() {
	for i in 1..=5 {
	    println!("{}",i);
	}
	for i in 'a'..='c' {
	    println!("{}",i);
	}
}

// 1
// 2
// 3
// 4
// 5

// 'a'
// 'b'
// 'c'

字符

所有的 Unicode 值都可以作为 Rust 的字符,由于 Unicode 都是 4 个字节编码,因此字符类型也是占用 4 个字节。
Rust 的字符只能用 '' 来表示。

布尔

Rust 中的布尔类型包含 truefalse ,占用内存大小为 1 字节。
使用布尔类型的场景主要在于流程控制。

单元类型

单元类型就是 ()main 函数的返回值就是单元类型,可以作为一个值用来占位,但是完全不占用任何内存。

语句和表达式

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

fn main() {
	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 a = 8;
	let b: Vec<f64> = Vec::new();
	let (a, c) = ("hi", false);
}

以上都是语句,它们完成了一个具体的操作,但是并没有返回值,因此是语句。因为语句没有返回值,所以不会给其他变量赋值。

表达式

表达式会进行求值,然后返回一个值。例如 5 + 6,在求值后,返回值 11,因此它就是一条表达式。调用一个函数是表达式,因为会返回一个值,调用宏也是表达式,用花括号包裹最终返回一个值的语句块也是表达式,总之,能返回值,它就是表达式:

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

    println!("The value of y is: {}", 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" };
}

函数

Rust 的函数使用以下格式表示:

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

函数要点

  • 函数名和变量名使用 蛇形命名(小写 + 下划线分割)
  • 函数的位置不重要
  • 每个函数的参数都需要标注类型

返回值

函数的返回值就是最后一行表达式(不带 ;)的结果,函数的返回值也可以用于赋值。

  • 返回 () 单元类型
    • 用来表示函数没有返回值
    • 通过 ; 结尾返回一个 ()
  • 永不返回的发散函数,这种语法往往用作会导致程序崩溃的函数
fn dead_end() -> ! {
	
}

2.9 动态数组

动态数组

动态数组使用 Vec<T> 表示,它允许储存多个相同类型的值,如果需要储存不同类型的值,需要使用枚举类型或特征对象。

创建动态数组

在 Rust 中,有多种方式可以创建动态数组。

Vec::new

使用 Vec 类型中的关联函数可以声明一个动态数组。

let v: Vec<i32> = Vec::new();

上面代码中我们显示声明了 v 的类型,因为Rust 编译器无法从 Vec::new() 中得到任何关于类型的暗示信息,因此也无法推导出 v 的具体类型。

let mut v = Vec::new();
v.push(1);

当我们向 v 中增加一个元素时,编译器可以推测出 v 中的元素类型是 i32,因此推导出的 v 的类型是 Vec<i32>

Vec![]

使用 Vec![]宏创建数组,可以在创建的同时给予初始化值:

let v = vec![1,2,3];

更新 Vector

向数组尾部添加元素可以使用 push 方法,而且必须将实例声明为 mut 后才能进行修改。

Vector 作用域

Vector 元素在超出作用域范围后,会被自动删除。它内部的元素也会被自动删除,但是当 Vector 被引用后情况会有所不同。

Vector 中读取元素

Vector 中读取元素可以使用 下标索引get 方法两种方法。

let v = vec![1,2,3,4];

let third = v[2];

let third = match v.get(2) {
	Some(third) => third,
	None => None
}

下标索引和 get 方法的区别

  • 下标索引直接返回值,而 get 方法返回一个 Option<&T> 需要通过模式匹配得到值。
  • get 方法比较安全,当发生数组越界的情况时,我们拿到的值是一个 None
  • 下标索引比较简洁,发生数组越界的情况时,程序会发生崩溃。
    当我们能确保我们的访问不会发生越界时,就使用 下标索引,否则就使用 get 方法。

同时借用多个数组元素

fn main() {
	let mut v = vec![1,2,3,4];
	let first = &v[0];
	v.push(5);
	println!("first element is {}",{})
}

在第 3 行代码时,我们创建了对 v 的一个不可变引用,第 4 行我们使用 v.push 可变引用。根据借用规则,不可变引用在可变引用之后被使用了,同一个作用域不能同时存在对一个变量的可变引用和不可变引用,这里会出现一个编译器错误。
原因在于:数组的大小是可变的,当旧数组的大小不够用时,Rust 会重新分配一块更大的内存空间,然后把旧数组拷贝过来。这种情况下,之前的引用显然会指向一块无效的内存,这非常 rusty —— 对用户进行严格的教育。

遍历 Vector 中的元素

使用迭代去访问数组元素比直接使用下标更加安全和高效(每次访问都会检查数组边界)。

let v = vec![1,2,3];
for i in v {
	println("{}",i)
}

在迭代过程过修改 动态数组

let mut v = [1,2,3];
for i in &mut v {
	*i += 10;
}

储存不同类型的元素

我们知道动态数组中只能储存相同类型的元素,但是通过枚举或者特征类型就能实现在动态数组中储存不同类型的元素。

使用枚举

struct Ipv4Address(String)
struct Ipv6Address(String)

enum NetworkAddress {
	Ipv4Address,
	Ipv6Address
}

let v= vec![
	NetworkAddress::Ipv4Address(String::from("...")),
	NetworkAddress::Ipv6Address(String::from("...")),
]

使用特征对象

trait IpAddr {
	fn display(&self);
}

struct Ipv4Address(String);
struct Ipv6Address(String);

impl IpAddr for Ipv4Address {
	fn display(&self) {
		println("{}",self.0);
	}
}

impl IpAddr for Ipv6Address {
	fn display(&self) {
		println("{}",self.0);
	}
}

fn main() {
	let v:Vec[Box<dyn IpAddr>] = vec![
		Box::new(Ipv4Address(String::from("...")),
		Box::new(Ipv6Address(String::from("...")),
	];
}

特征对象数组在实际场景中使用比枚举数组更加频繁,主要是特征对象比较灵活,而编译器对枚举类型限制比较多。

2.3 所有权和借用

所有权

所有权是编译器在编译时根据一系列规则来进行检查的一种内存管理方式。这种检查只发生在编译期,对于程序运行期不会有性能上的损失。

堆和栈

栈按照顺序存储值并以相反顺序取出值,这也被称作后进先出。增加数据叫做进栈,移出数据则叫做出栈
因为上述的实现方式,栈中的所有数据都必须占用已知且固定大小的内存空间,假设数据大小是未知的,那么在取出数据时,你将无法取到你想要的数据。

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

性能区别

写入: 栈的空间已知,入栈时无需分配新的空间,只需要将数据放入栈顶。所以栈的写入速度更快。
读取: 栈一般位于CPU 高速缓存中,而堆数据只能存储在内存中,访问堆上的数据比访问栈上的数据慢,因为必须先访问栈再通过栈上的指针来访问内存。
处理器处理和分配在栈上数据会比在堆上的数据更加高效。

所有权原则

  • Rust 中的每一个值都被一个变量所拥有,这个变量称为值的所有者。
  • 一个值同时只能被一个变量所拥有,即一个值只能拥有一个所有者。
  • 当所有者离开作用域范围时,这个值将被丢弃。

变量作用域

变量从声明的点开始直到声明的作用域结束都是有效的。

fn main {
	{ // s 尚未被声明,无效
		let s = "hello"; // s 有效
		
		println!({},s); 
	} // 作用域结束,无效
}

数据的拷贝

栈数据

对于储存在栈上的数据,直接使用拷贝的方式来赋值。

堆数据

对于堆数据,例如 String 类型,由存储在栈中的堆指针字符串长度字符串容量共同组成。其中堆指针是最重要的,它指向了真实存储字符串内容的堆内存。容量是堆内存分配空间的大小,长度是目前已经使用的大小。

fn main() {
	let s1 = String::from("hello");
	let s2 = s1;
}

我们已经知道当 Rust 中的变量在作用域结束时会被释放,那么当离开作用域时 s1 和 s2 都会被释放,如果同一个值拥有两个所有者,它就会被2次释放,会导致内存污染,造成潜在的安全漏洞。

在 Rust 中:当 s1 赋予 s2 后,Rust 认为 s1 不再有效,因此也无需在 s1 离开作用域后 drop 任何东西,这就是把所有权从 s1 转移给了 s2s1 在被赋予 s2 后就马上失效了。即当所有权转移后,对 s1 的使用也将无效。

![[Pasted image 20230112174414.png]]

对于 s1s2 来说,赋值的过程 s2 拷贝了 s1 的长度和容量,移动了 s1 的数据指针到 s2 ,这并不是深拷贝或浅拷贝。

深拷贝

Rust 永远也不会自动创建数据的“深拷贝”,因此任何自动的复制都是移动,对运行时的性能影响较少,当我们确实需要深拷贝时,使用 clone 方法。

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

浅拷贝

浅拷贝只发生在栈上,也就是说所有的除深拷贝之外的拷贝都是浅拷贝,即使是基本类型。
但是因为像整形这样的基本类型在编译时已知大小,所以拷贝是很快速的。
Rust 有一个叫做 Copy 的特征,可以用在类似整型这样在栈中存储的类型。如果一个类型拥有 Copy 特征,一个旧的变量在被赋值给其他变量后仍然可用。

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

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

函数和所有权

向函数传递参数会移交所有权,函数的返回值也有其所有权。

2.6 模式匹配

match 和 if let

enum Direction {
    East,
    West,
    North,
    South,
}

fn main() {
    let dire = Direction::South;
    match dire {
        Direction::East => println!("East"),
        Direction::North | Direction::South => {
            println!("South or North");
        },
        _ => println!("West"),
    };
}

match 跟其他语言中的 switch 非常像,_ 类似于 switch 中的 default

  • match 的匹配必须要穷举出所有可能,因此这里用 _ 来代表未列出的所有可能性
  • match 的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同
  • X | Y,类似逻辑运算符 ,代表该分支可以匹配 X 也可以匹配 Y,只要满足一个即可

使用 match

使用 match 表达式赋值

match 本身也是一个表达式,所以可以通过 match 完成赋值操作。

enum IpAddr {
   Ipv4,
   Ipv6
}

fn main() {
    let ip1 = IpAddr::Ipv6;
    let ip_str = match ip1 {
        IpAddr::Ipv4 => "127.0.0.1",
        _ => "::1",
    };

    println!("{}", ip_str);
}

模式匹配

模式匹配的另外一个重要功能是从模式中取出绑定的值,比如扑克牌的例子:

enum PokerCard {
	Clubs(u8),
	Spades(u8),
	Diamonds(u8),
	Hearts(u8),
}

fn get_poker_num(c: PokerCard) -> u8 {
	match c {
		PokerCard::Clubs(num) =>num,
		PokerCard::Spades(num) =>num,
		PokerCard::Diamonds(num) =>num,
		PokerCard::Hearts(num) =>num,
	}
}

穷尽匹配

在使用 match 时匹配必须穷尽所有的情况。

_通配符

当我们不想在匹配时列出所有值的时候,可以使用 Rust 提供的一个特殊模式,使用特殊的模式 _ 替代。

if let 匹配

只有一个模式的值需要被处理,其他的值都直接忽略的情况下,match 写起来就很多了。这时候我们用 if let 的方式就可以实现:

let v = Some(38);
// 使用 match
match c {
	Some(3) => println!("three"),
	_ => (),
}

// 使用 if let
if let Some(3) = v {
	println!("three")
}

matches! 宏

Rust 标准库中提供了一个非常实用的宏:matches!,它可以将一个表达式跟模式进行匹配,然后返回匹配的结果 true or false

enum MyEnum {
    Foo,
    Bar
}

fn main() {
    let v = vec![MyEnum::Foo,MyEnum::Bar,MyEnum::Foo];
}

// v.iter().filter(|x| x == MyEnum::Foo); 报错,无法将 x 直接于枚举成员比较
v.iter().filter(|x| matches!(x, MyEnum::Foo)); // 使用宏

变量覆盖

无论是 match 还是 if let,他们都可以在模式匹配时覆盖掉老的值,绑定新的值:

fn main() {
   let age = Some(30);
   println!("在匹配前,age是{:?}",age);
   if let Some(age) = age {
       println!("匹配出来的age是{}",age);
   }

   println!("在匹配后,age是{:?}",age);
}

解构 Option

对于使用 Option  枚举来表示变量中是否有值的情况,我们需要从一个 Option 类型中取出 T 值,但是会存在没有值的情况。

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

fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
	None => None, 
	Some(i) => Some(i + 1),
	}
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.