Unique's Blog

Rust基础(一)

2022-05-28 · 3614字 · 16 min read
🏷️  Rust

hello rust.

[TOC]

Rust 快速入门,参考 《Rust程序设计语言》简要归纳 Rust 的基础,尽量简约而不失重点,方便查阅。

变量与类型

变量

基础类型

标量类型

复合类型

字符串和切片

经常使用的字符串类型有:&str,String,&String

str系统类型,具有动态大小,所以不能直接用来声明变量。可以看作是一个字节数组区域,并且符合utf-8字符边界。

String动态大小字符串,可以用来声明(栈)变量指向堆上分配的数据(使用起始地值+长度+容量实现)。&String是其引用类型,借用而不获取所有权。

&str字符串切片,可以用来声明变量,也是一种引用类型,指向一块字符串字节数组。(起始地值+长度实现,没有所有权)。字符串字面量创建的变量默认声明为&str,即引用分配在静态存储区的常量字符串。必须符合utf-8字符的边界。

示例:

fn main() {
	let s = "Hello, world!";
	let ss = String::from("Hello, world!");
	println!("{}", first_word(s));
	println!("{}", first_word(&ss)); // &String
}

fn first_word(s: &str) -> &str {
	let bytes = s.as_bytes();
	
	for (i, &item) in bytes.iter().enumerate() {
		if item == b' ' {
			return &s[0..i];
		}
	}
	&s[..]
}
  • 函数参数如果是字符串类型,尽量使用&str,更加通用(String类型容易转换为切片,&String 可通过解引用强制转换为 &str,或者使用切片)

字符串切片使用:

let s = "hello world";
let s1 = &s[0..5]; // &s[..5]
let s2 = &s[6..11];// &s[6..]
let s3 = &s[0..11];// &s[..]

其他类型的切片原理类似,例如 整形切片&[i32]

注:整形切片可以由数组,Vec来构造。

struct 与 enum

自定义类型,封装多个字段。

  • 如果定义 struct 是可变的,其所有的字段都是可变的;

  • 函数中字段初始化简写,如果函数参数与字段名同名,可忽略赋值操作只写字段名;

  • 基于原 struct 的更新语法:

struct User{
name: String,
active: bool,
}

let user2 = User{
active = false, // 不同的字段
..user1 // 其他字段与user1相同
};

tuple struct

类似与元组并且可以结构,其元素没有名称,但是整个类型是具名类型。

struct Point(i32, i32, i32);
let origin = Point(0, 0, 0);
// unit-like struct没有字段的结构体
struct AlwaysEqual;

定义方法和关联函数

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

impl Rectangle {
//构造函数
pub fn new(width: i32, height: i32) -> Self {
	Rectangle {
		//同名可以省略
		width,
		height,
	}
}
//方法,&self参数
fn erea(&self) -> i32 {
	self.width * self.height
}
//关联函数
fn erea2(width: i32, height: i32) -> i32 {
	width * height
}

fn drop(mut self) {
	println!("drop myself");
	}
}
  • 使用imple块来定义方法和关联函数,可以有多个块;

  • 如果其中函数第一个参数为&self | &mut self为该类型的方法,self 即为该类型的实例本身;其他方法就是关联方法,使用 structName::funcName() 调用;

枚举类型,含有类型的多个变体,rust 枚举变体可以关联一组不同类型的数据,关联的数据可以通过下面中的match模式匹配来获取:

enum IpAddrKind {
	V4(u32,u32,u32,u32),
	V6(String),
}

标准库枚举Option<T>用来解决空值问题,其2个变体Some<T>, None可以直接使用。

let some_num = Some(5);
let absent_num: Option<i32> = None;

函数与控制流

函数使用 fn 声明,使用尾置形式返回类型:

fn add(a: i32, b: i32) -> i32{
	a+b
}

函数只要在模块作用域范围可见就可以使用,不需要先声明后使用。

if/else

条件表达式必须是 bool 类型,不使用小括号。

for/while/loop

for i in 0..10{
	println!("{}", i);
}

while number > 0 {
	println!("{}", number);
	number -= 1;
}

loop {
	println!("{}", number);
	number -= 1;
	if number == 0 {
		break;
	}
}

match模式匹配

match类似传统的switch语法,当是支持更多特性。匹配的模式支持字面量、变量或者通配符;支持绑定值的模式,用来提取变量中的值:

let some_num = Some(5);
let some_num_value = match some_num {
Some(i) => i,
	None => 0,
};
println!("some_num_value is {}", some_num_value);
  • match 匹配必须穷举所有可能模式,可以使用_通配符表示剩余其他值。

  • if let可以只匹配一种模式:

// if let pattern = var
if let Some(5)=some_num{
	println!("some_num is 5");
}else{
	println!("other");
}

所有权机制

引用类型,例如 &String 是不拥有所有权的类型/借用类型。

引用类型,可直接调用该类型的方法(语法糖),类型方法中的第一个参数就是该类型的引用/不可变类型。

常用集合

Vec

大小可变的动态数组,基本用法:

let mut v = vec![1, 2, 3, 4, 5];
// let mut mv: Vec<i32> = Vec::new();
let first = &v[0]; // ref
println!("{}", first);
match v.get(2) {
	Some(x) => println!("{}", x),
	None => println!("None"),
}
// 遍历
for i in &mut v {
	*i += 10;
}
for i in v {
	println!("{}", i);
}
  1. 由于枚举类型的灵活性-可以附加数据,可以使用 Vec+enum 组合,从而可以存储枚举的不同变体以及不同类型的关联数据;

String

字符串String类型底层就是Vec<u8>字节数组,字符串使用 UTF-8 编码,并提供字节-文本解析方法。String 类型来自标准库而非核心语言,可增长,可修改和获得所有权的类型。

💡

核心语言层面,Rust只有一个字符串类型 str (大小可变的字符串),通常使用 &str 来表示字符串切片。字符串切片:引用类型,对存储在其它地方的utf-8编码字符串的引用。

常用方法:

// 创建String
let s = String::from("hello");
let s = String::new();
let s = "hello".to_string();
// 添加字符/字符串
s.push_str(", world!");
s.push('!');
// 拼接字符串
let s3 = s1 + &s2; // fn add(self, s: &str) -> String
let s3 = format!("{}-{}", "hello", s); // format!
  • 拼接字符串的+操作,注意获取了s1的所有权/后续失效,参数是&str;解引用强制转换(deref coercion)可以将 &String 转换为 &str 类型。

  • 使用format!宏拼接字符串,不会获取所有权,方便于多个字符串的拼接。

内部表示:

String 底部是u8字节数组,使用 UTF-8 编码存储 Unicode 标量值,主要特性:

  1. 不支持整形下标索引(保证字符索引安全);

  2. 使用 [..] 创建字符串切片时,也必须符合 utf-8 字符边界,否则出现运行时错误;

HashMap

不在 prelude 中,需要显示导入use std::collections::HashMap;创建和使用:

// 创建HashMap
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
println!("{:#?}", scores);

// collect
let teams = vec![String::from("Blue"), String::from("Yellow")];
let initial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.iter().zip(initial_scores.iter()).collect();
println!("{:#?}", scores);

let vec = vec![(String::from("Blue"), 10), (String::from("Yellow"), 50)];
let scores: HashMap<_, _> = vec.into_iter().collect();
println!("{:#?}", scores);

所有权问题:

  • 对于实现了 Copy trait类型例如i32,会复制到 HashMap中;对于拥有所有权的类型会被移动(上述into_iter()方法会移动原来的vec中元素)

  • 如果将引用插入到HashMap中,值不会移动,但是要保证被引用值的有效性;

使用:

常用的操作有:插入键值对、获取key对于的值、遍历以及修改。

// get 获取值
let score = scores.get("Blue");
match score {
	Some(score) => println!("{}", score),
	None => println!("No score"),
}
// 不检查
let score = scores["Blue"]; //  借用使用 &scores["Blue"]

// 遍历
for (k,v) in &scores{
	println!("{}:{}", k, v);
}

更新操作:

  • 使用insert()如果存在旧值会替换

  • 使用 entry()返回 Option 类型,使用变体的 or_insert() 方法在不存在时插入;如果存在会返回该值的可变引用,通过解引用来更新。

// 添加值,insert方法替换旧值
let words = "hello world wonderful world";
let mut map = HashMap::new();
for word in words.split_whitespace() {
	let count = map.entry(word).or_insert(0);
	*count += 1;
}

泛型

对于 struct,enum,方法/普通函数都可以定义为泛型:

struct Point<T> {
	x: T,
	y: T,
}
impl<T> Point<T> {
	fn x(&self) -> &T {
		&self.x
	}
}

impl Point<i32> {
	fn origin() -> Point<i32> {
		Point { x: 0, y: 0 }
	}
}
  1. Rust泛型实现与 C++类型,编译时都会进行具体类型替换-单态化;

  2. struct 方法可以定义其他的类型参数,与struct是否为泛型无关;

  3. 对泛型struct的某个具体类型,例如Point<i32>定义方法是对该类型添加的,其他类型参数的Point不具有(与C++偏特化/特化不同)

泛型可以扩展代码的通用性,常见类型都定义为泛型,Option<T>, Result<T, E>

Trait

类似与接口概念,告诉编译器某种类型具有哪些特定行为/功能,用来抽象地定义共享/公共行为

一个主要作用就是为泛型类型参数进行约束Trait bounds,指定为实现特定行为的类型。

定义与为类型实现Trait:

pub trait Summary {
	fn summarize(&self) -> String;
}
pub struct Tweet {
	pub username: String,
	pub content: String,
}
// impl
impl Summary for Tweet {
	fn summarize(&self) -> String {
		format!("{}: {}", self.username, self.content)
	}
}

Trait可以定义默认实现。

类型实现Trait必须实现 trait 定义的所有没有默认实现的方法,对于有默认实现的,可以选择重写该方法;trait 定义中,(默认)方法可以调用没有默认实现的方法,类型会保证实现这些方法

类型实现Trait的条件:

  1. 类型或者该 Trait 是在本地 crate定义的;

  2. 即无法为外部类型实现外部的 trait(孤儿规则)

Trait作为参数

使用impl Trait或者Trait bound语法:

// impl Trait 修饰参数
pub fn notify(item: impl Summary+Display) {
	println!("Breaking news! {}", item.summarize());
}
// 使用泛型 + trait约束
pub fn notify<T: Summary+Display>(item: T) {
	println!("Breaking news! {}", item.summarize());
}
// where
pub fn notify2<T, U>(a: T, b: U)
where
	T: Summary + Display,
	U: Clone + Debug,
{
	println!("Breaking news! {}", a.summarize());
}

Trait作为返回类型

使用impl trait语法:

pub fn news() -> impl Summary {
	Tweet {
		username: String::from("ebooks"),
		content: String::from("people"),
	}
}

限制:

  • 需要保证函数返回的类型需要一致,返回确定的同一种类型;
💡

Trait 主要用来表示类型约束,其他用法: 在泛型类型的 impl 块上使用 Trait bound,可以为类型参数实现了特定 Trait 的泛型类型有条件地实现某些方法; 为实现特定 Trait 的任意类型有条件地实现另一个 Trait (覆盖实现)

示例代码
// 1. 有条件地实现某些方法
impl<T: Display+PartialOrd> Pair<T>{
	fn cmp_display(&self){
		...
	}
}
// 2. 有条件实现另一个 Trait
impl<T: fmt::Display+?Sized> ToString for T{
	...
}

生命周期

生命周期目的:避免悬垂引用 dangling reference.

Rust中每个引用都有自己的生命周期(保持有效的作用域),当生命周期以不同的方式互相关联,需要手动标注生命周期,使用泛型声明来规范生命周期的名称。

例如函数签名中使用泛型生命周期参数:

可以理解为返回引用的生命周期至少是x,y中较短的生命周期(交集)

fn long_str<'a>(x: &'a str, y: &'a str) -> &'a str {
	if x.len() > y.len() {
		x
	} else {
		y
	}
}
  • 生命周期标注不会实际改变引用的生命周期长度

  • 只是描述了多个引用的生命周期的关系,不影响其生命周期;因此单个生命周期的标注没有意义;

  • 'static特殊生命周期标识,整个程序的运行时间(例如字符串字面量)

💡

注意:函数返回引用类型,返回类型的生命周期参数需要与一个参数的生命周期匹配(即跟输入参数相关,否则就是悬垂引用)

省略规则

Rust引用分析中考虑了一些特定模式/生命周期省略规则,符合该模式的代码无需显示标注生命周期。(输入生命周期: 参数为引用的生命周期;输出生命周期:返回值是引用的生命周期)

规则如下:

  1. 每个输入**参数(引用类型)**如果省略生命周期,则具有不同的生命周期参数(例如'a,'b,'c);

  2. 如果只有一个输入生命周期参数,该生命周期被赋给所有的输出生命周期参数;

  3. 如果有多个输入生命周期参数,但是其中有一个是&self,&mut self(适用于方法中),那么 self 的生命周期被赋给所有的输出生命周期参数。

应用上述规则后如果不能确定签名中所有引用的生命周期编译器会报错。或者出现不匹配,例如返回值的生命周期与返回类型生命周期不同(示例见impl块和方法)。

struct

struct 定义中字段除了基本类型和自拥有类型,其引用类型需要使用生命周期标注:

struct Stu<'a> {
	name: &'a str, // 至少比Stu实例生命周期长
	age: u8,
}
// main
fn main(){
	let name = String::from("xiaoming");
	let stu = Stu {
		name: name.as_str(),
		age: 18,
	};
	println!("{:#?}", stu);
}

impl块和方法

对于字段的生命周期需要在 imp 块中显示标注:

struct Stu<'a> {
	name: &'a str,
	age: u8,
}
// 对于struct 字段的生命周期标注显示指明/语法类似泛型
// 方法中的生命周期参数可以使用字段声明的,也可以自定义
// &self 方法可以有默认规则
impl<'a> Stu<'a> {
	fn get_age(&self) -> u8 {
		self.age
	}

	fn get_name(&self) -> &str {
		self.name
	}

//fn get_name2(&self, other: &str) -> &str{
//	other  // error 引用规则3返回类型的声明周期与 self相同
//}
}

阅读

rust_cheat_sheet.pdf

本文链接: Rust基础(一)

版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

发布日期: 2022-05-28

最新构建: 2024-12-26

本文已被阅读 0 次,该数据仅供参考

欢迎任何与文章内容相关并保持尊重的评论😊 !

共 43 篇文章 | Powered by Gridea | RSS
©2020-2024 Nuo. All rights reserved.