Rust变量与数据类型

238 阅读17分钟

变量与数据类型

常用的三大数据结构:

  • 动态数组
  • 映射
  • 字符串

Rust标准库std::collections提供了4种通用的容器类型,其中包含8种数据结构。

动态数组可细分为普通动态数组Vec和双端队列VecDeque

映射包括HashMap

字符串包括String等类型

变量和可变性

Rust的变量不同于其他编程语言的变量,其本质上是一种绑定语义,即将一个变量名与一个值绑定在一起。变量名和值建立关联关系。

变量默认是不可改变的

变量声明

使用let关键字声明变量,先声明后使用

let x :i8 =1
let x = 1 ; // 等价于 let x: i32 = 1;

变量声明以let关键字开头,x为变量名,变量名后紧跟冒号和数据类型

Rust编译器具有变量类型的自动推导功能

在可以根据赋值类型或上下文信息推导出变量类型的情况下,冒号和数据类型可以省略。

变量命名
  • 由字母、数字、下划线组成

  • 字母区分大小

  • 不能以数字开头,也不能只有下划线

    Rust中下划线是一种特殊的标识符,其含义是“忽略这个变量”

变量的可变性

let声明的变量默认是不可变的,在第一次赋值后不能通过再次赋值来改变它的值,即声明的变量是只读状态

在变量名的前面加上mut关键字就是告诉编译器这个变量是可以重新赋值的

let mut x = 3;
x = 5;
println!("x: {}",x);

Rust编译器保证了如果一个变量声明为不可变变量,那它就真的不会变

变量遮蔽

Rust允许在同一个代码块中声明一个与之前已声明变量同名的新变量,新变量会遮蔽之前的变量,即无法再去访问前一个同名的变量,这样就实现了变量遮蔽

fn main(){
    let x = 3;
    let x = x+2;
    let x = x*2;
    let x = "Hello, World";
}

变量遮蔽的实质是通过let关键字声明了一个新的变量,只是名称恰巧与前一个变量名相同而已,但它们是两个完全不同的变量,处于不同的内存空间,值可以不同,值的类型也可以不同

常量

常量是指绑定到一个标识符且不允许改变的值,一旦定义后就没有任何方法能改变其值了

const MAX_NUM : u32 = 1024;

使用const关键字来声明常量

常量名通常是大写字母,且必须指定常量的数据类型

常量与不可变变量的区别主要在于:

  • 常量声明使用const关键字,且必须注明值的类型

  • 通过变量遮蔽的方式可以让不可变变量的值改变(本质上是新的变量,只是同名而已)。但是,常量不能遮蔽,不能重复定义

    不存在内层或后面作用域定义的常量去遮蔽外层或前面定义的同名常量的情况。常量一旦定义后就永远不可变更和重新赋值。

  • 常量可以在任何作用域中声明,包括全局作用域。在声明它的作用域中,常量在整个程序生命周期内都有效

  • 常量只能被赋值为常量表达式或数学表达式,不能是函数返回值,或是其他在运行时才能确定的值。

    在编译阶段就要确定其值

基本数据类型

强类型的静态编译语言

Rust的基本数据类型有整数类型、浮点数类型、布尔类型、字符类型、范围类型等。

整数类型

整数可以分为有符号整型和无符号整型

按照存储大小,整数类型可以进一步分为1字节、2字节、4字节、8字节、16字节

Rust默认的整数类型是i32

isize和usize主要作为数组或集合的索引类型使用,其长度依赖于运行程序的计算机系统。在64位计算机系统上,其长度是64位;在32位计算机系统上,其长度是32位。

长度有符号无符号
8 biti8u8
16 biti16u16
32 biti32u32
64 biti64u64
128 biti128u128
archisizeusize
  • 数字字面量后使用类型后缀
  • 前缀0b、0o和0x表示二进制、八进制和十六进制的数字
let integer1 : u32 = 17 ; // 类型声明
let integer2 = 17u32;     // 类型后缀声明
let integer3 = 17 ;       // 默认i32
let integer4 : u32 = 0b1001; // 二进制
let integer5 : u32 = 0o21;   // 八进制
let integer6 : u32 = 0x11 ;  // 十六进制
let integer7 = 50_000;   // 数据可读性分隔符_

Rust允许使用下划线“_”作为虚拟分隔符来对数字进行可读性分隔

Rust在编译时会自动移除数字可读性分隔符“_”。

如果某个变量的值超出了给定的数值范围,将会发生整型溢出。编译器将其视为一种错误。

浮点数类型

浮点数分为f32和f64两类。Rust默认的浮点数类型是f64

  • f32: 单精度, 小数点后至少6位有效数字
  • f64 : 双精度, 小数点后至少15位有效数字

浮点数支持使用数字可读性分隔符“_”

let float1 : f32 = 1.1;  // 类型声明
let float2  = 2.2f32;  // 类型后缀声明
let float3 = 3.3  ;  // 默认f64类型
let float4 = 11_00.555_01;  // 数字可读性分隔符
布尔类型

使用bool来声明布尔类型的变量

let t : bool = true;  //显式类型声明
let f = false ;       // 隐式类型声明
字符类型

Rust使用UTF-8作为底层的编码。

字符类型代表的是一个Unicode标量值(Unicode Scalar Value),包括数字、字母、Unicode和其他特殊字符。

每个字符占4个字节。

字符类型char由单引号来定义

let z = 'z' ;  // 使用单引号
let hz = '中';
范围类型

范围类型常用来生成从一个整数开始到另一个整数结束的整数序列,有左闭右开全闭两种形式

  • (1..5)是左闭右开区间,表示生成1、2、3、4这4个数字
  • (1..=5)是全闭区间,表示生成1、2、3、4、5这5个数字

范围类型自带一些方法

  • rev方法可以将范围内的数字顺序反转
  • sum方法可以对范围内的数字进行求和
fn main(){
    print!("(1..5):")
    for i in 1..5 {
        print!("{} ",i);
    }
    println!();
    
    print!("(1..=5).rev:");
       for i in (1..=5).rev() {
        print!("{} ",i);
    }
    println!();
    
    let sum :i32 = (1..=5).sum();
    println!("sum(1..=5)={}",sum);
}

//(1..5):1 2 3 4 
//(1..=5).rev:5 4 3 2 1 
//sum(1..=5)=15
复合数据类型

复合数据类型是由其他类型组合而成的类型

Rust的复合数据类型有元组、数组、结构体、枚举等

元组类型

元组类型是由一个或多个类型的元素组合成的复合类型,使用小括号“()”把所有元素放在一起。元素之间使用逗号“,”分隔

元组中的每个元素都有各自的类型,且这些元素的类型可以不同。

元组的长度固定,一旦定义就不能再增长或缩短。

如果显式指定了元组的数据类型,那么元素的个数必须和数据类型的个数相同。

可以使用元组名.索引的方式来访问元组中相应索引位置的元素。

当元组中只包含一个元素时,应该在元素后面添加逗号来区分是元素,而不是括号表达式

let tup1 :(i8,f32,bool) = (-10,7.7,false);
let tup2 = (7.7,(false,10));
let tup3 = (100,);
println!("{},{}",tup1.0,tup2.1)
// 解构赋值
let (x,y,z) = tup1;
数组类型

由相同类型的元素组合成的复合类型

使用[T;n] 表示,T代表元素类型,n代表长度即元素个数

// 指定数组类型,为每一个元素赋值
let arr:[i32;5] = [1,2,3,4,5];
// 省略数组类型 --> 编译器可以从初始值推断出数组类型
let arr = [1,2,3,4];
// 省略数组类型,为所有元素使用默认值初始化
let arr = [1;5]; // -> 等价let arr = [1,1,1,1,1]

可以使用"数组名[索引]"来访问数组中相应索引位置的元素,元素的索引从0开始计数。

动态数组Vec, Vec是允许增长和缩短长度的容器类型,其提供的get方法在访问元素时可以有效避免索引越界

结构体类型

结构体类型是一个自定义数据类型,通过struct关键字加自定义命名,可以把多个类型组合在一起成为新的类型。

结构体中以"name: type"格式定义字段,name是字段名称,type是字段类型。

字段默认不可变,并要求明确指定数据类型,不能使用自动类型推导功能。

struct ListNode {
    var : i32,
    next : Option<Box<ListNode>>,  // next类型时指向ListNode的智能指针
}

使用"实例名.字段名"形式更改和访问结构体实例某个字段的值。

结构体实例默认是不可变的,且不允许只将某个字段标记为可变,如果要修改结构体实例必须在实例创建时就声明其为可变的。

struct Student {
    name : &'static str,
    score: i32,
}

fn main(){
    let score = 59;
    let username = "wkk";
    
    let mut student = Student{
        score, // 变量和字段同名,可以简写
        name : username,
    };
    
    student.score = 60;
    //结构体更新语法,对除字段name外未显式设置值的字段以student实例对应字段的值来赋值。
    let student2 = Student {
        name : "yyr",
        ..student
    };
}

特殊结构体:

  • 元组结构体

    字段只有类型,没有名称

    struct Color(i32,i32,i32);
    
  • 单元结构体

    没有任何字段的结构体

    struct Solution;
    
枚举类型

使用enum关键字加自定义命名来定义

含若干枚举值,可以使用“枚举名::枚举值”访问枚举值。

变量的值限于枚举值范围内

根据枚举值是否带有类型参数,枚举类型还可以分成无参数枚举类型和带参数枚举类型。

无参枚举类型

// #[derive(Debug)] 让ColorNoParam自动实现Debug trait
// 只有实现了Debug trait的类型才拥有使用{:?}格式化打印的行为
#[derive(Debug)]
enum ColorNoParam {
	Red,
    Yellow,
    Blue,
}

fn main(){
    let color_no_param = COlorNoParam::Red;
    match color_no_param{
		ColorNoParam :: Red => println!("{:?}",ColorNoParam::Red),
        ColorNoParam :: Yellow => println!("{:?}",ColorNoParam::Yellow),
        ColorNoParam :: Blue => println!("{:?}",ColorNoParam::Blue),
    }
}

带参枚举类型

#[derive(Debug)]
enum ColorParam{
    Red(String), //带有String类型参数
    Yellow(String),
    Blue(String),
}

fn main(){
    //使用这种枚举值需要传入实参
    println!("{:?}",ColorParam::Blue(String::from("blue")));
}
容器类型

Rust标准库std::collections提供了4种通用的容器类型,包含8种数据结构

image-20221101142431588

Vec

动态可变长数组, 在运行时可以增长或者缩短数组的长度

动态数组在内存中开辟了一段连续内存块用于存储元素,且只能存储相同类型的元素。

新加入的元素每次都会被添加到动态数组的尾部

  • 创建

    // 创建空的动态数组
    let mut v: Vec<i32> = Vec::new();
    // 创建指定容量的动态数组
    let mut v: Vec<i32> = Vec::with_capacity(10);
    // 使用vec!宏在创建动态数组的同时进行初始化,并且根据初始值自动推断动态数组的元素类型
    let mut v: Vec<i32> = vec![]; // 没有初始值,需要声明元素类型
    let mut v = vec![1,2,3]; // 自动推断元素个数
    let mut v = vec![0;10]; // 10个元素,元素的初始值都是0
    

    应该尽可能根据初始元素个数以及增长情况来指定合理的容量。

  • 修改

    // 使用push方法在动态数组尾部添加新元素
    v.push(1);
    // 使用数组名[索引] 获取元素
    v[1] = 5;
    // 使用pop方法删除并返回动态数组的最后一个元素,如果数组为空返回None
    v.pop();
    // remove方法删除并返回动态数组指定索引的元素,同时后面的所有元素向前移动一位
    // 索引越界将导致程序错误
    v.remove(1);
    
  • 访问

    //使用索引访问
    v[2];
    // 使用get方法以索引作为参数访问
    v.get(1);
    // 遍历
    for i in v {
        print!("{}",i);
    }
    // 可变引用
    for i in &mut v {
        *i += 50;
        print!("{}",i);
    }
    
VecDeque

双端队列是一种同时具有栈(先进后出)和队列(先进先出)特征的数据结构,适用于只能在队列两端进行添加或删除元素操作的应用场景

定义在标准库的std::colllections::VecDeque中,使用前需要显式导入std::collections::VecDeque;

  • 创建

    // 创建空的VecDeque
    let mut v : VecDeque<u32> = VecDeque::new();
    // 创建指定容量的VecDeque
    let mut v : VecDeque<u32> = VecDeque :: with_capacity(10);
    
  • 修改

    // push_front 在队列头部添加新的元素
    v.push_front(1);
    // push_back 在尾部添加新元素
    v.push_back(2);
    
    // 使用索引修改元素
    v[1] = 5;
    
  • 删除

    // pop_front 删除并返回队列的头部元素
    v.pop_back();
    // pop_back 删除并返回尾部元素
    v.pop_front();
    
    // remove 删除并返回队列指定索引的元素,同时后面的所有元素向左移动一位
    // 索引越界返回None
    v.remove(1);
    
  • 访问

    // 使用索引访问
    v[0];
    // 使用get方法以索引作为参数访问元素
    v.get(0);
    
HashMap

哈希表(HashMap)是基于哈希算法来存储键-值对的集合,其中所有的键必须是同一类型,所有的值也必须是同一类型,不允许有重复的键

定义在标准库std::collections 模块中,使用前要显式导入std::collection::HashMap

  • 创建

    // 创建空的HashMap
    let mut map: HashMap<&str,i32> = HashMap::new();
    // 创建指定容量
    let mut map: HashMap<&str,i32> = HashMap::with_capacity(10);
    
  • 修改

    // insert 执行插入或者更新
    // 键不存在,执行插入并返回None
    // 键存在,执行更新,并返回旧值
    let zhangsan = map.insert("zhangsan",16);
    
    // 使用entry 和 or_insert 方法检查是否有对应值,没有对应值就插入,有对应值不执行操作
    // entry方法以键为参数,返回值是一个枚举类型Entry
    // Entry类型的or_insert 方法以值为参数,在键有对应值时不执行任何操作,没有对应值时,将键与值组成键值对插入
    map.entry("zhangsan").or_insert(23);
    
    // iter_mut 方法会返回由所有键值对的可变引用组成的迭代器
    for( _, val ) in map.iter_mut() {
        *var += 2; // 所有的值都加2
    }
    
    // remove 删除并返回指定键值对的值,不存在返回None
    let result = map.remove("wkk");
    
  • 访问

    // 使用实例名[键] 访问指定键值对, 键不存在会导致程序错误
    map["wkk"]
    
    // 使用get 方法,以键作为参数访问指定的键值对,存在返回值,不存在返回None
    map.get("wkk");
    
字符串

字符串的本质是一种特殊的容器类型,是由零个或多个字符组成的有限序列。

字符串常被作为一个整体来关注和使用

常用的字符串有两种:

  • 固定长度的字符串字面量
  • 可变长度的字符串对象String
创建
  1. &str的创建

    内置的字符串类型是str, 通常以引用的形式&str出现。

    字符串字面量&str是字符的集合,代表的是不可变的UTF-8 编码的字符串的引用,创建后无法再追加内容或者更改内容

    // 使用双引号创建字符串字面量
    let s1 = "hello,wkk";
    
    // 使用as_str方法将字符串对象转换为字符串字面量
    let str = String::from("hello,wkk");
    let s2 = str.as_str();
    
  2. String 的创建

    字符串对象String 由Rust 标准库提供。

    创建后可以为其追加内容或者更改内容。

    本质是一个字段为Vec<u8> 类型的结构体,把字符内容放在堆上,由指向堆上字节序列的指针(as_ptr方法)、记录堆上字节序列的长度(len方法)和堆分配容量(capacity) 3部分组成。

    // 创建空的字符串对象
    let mut s = String::new();
    
    // 根据指定的字符串字面量创建字符串对象
    let s = String::from("wkk");
    
    // 使用to_string 方法将字符串字面值转换为字符串对象
    let str = "wkk";
    let s = str.to_string();
    
修改
  1. 使用push方法在字符串后面追加字符,使用push_str方法在字符串后追加字符串字面量

    都是在原字符串上追加,不会返回新的字符串

    let mut s = String::from("wkk");
    s.push('R');
    s.push_str("111");
    

    要追加,字符串必须是可变的,使用mut关键字

  2. 使用insert方法在字符串中插入字符,使用insert_str方法在字符串中插入字符串字面量

    第1个参数是插入位置的索引,第2个参数是插入字符或者字符串字面量

    都是在原字符串上插入,并不是返回新的字符串

    索引非法会导致程序错误

    s.insert(5,',');
    s.insert_str(7,"Rust ");
    
  3. 使用 "+" 或者"+="运算符将两个字符串连接成一个新的字符串,要求运算符的右边必须是字符串字面量

    不能对两个String 类型的字符串使用

    连接与追加的区别在于,连接会返回新的字符串,而不是在原字符串上的追加

    let s = "hello " + "wkk";
    
  4. 对于较为复杂的带有格式的字符串连接,可以使用格式化宏format!

    对于String 类型 和 &str类型的字符串都适用

    let s = format!("{}-{}-{}",s1,s2,s3);
    
  5. replace 和 replacen() 方法将字符串中指定的子串替换为另一个字符串。

    replace 接收两个参数,第1个参数为要被替换的子串,第2个参数为新的字符串,会替换所有匹配的子串。

    replacen 方法除了上述两个参数外,还接受第3个参数来指定替换的个数

    let s1 = s.replace("aa","77");
    let s2 = s.replace("aa","77",1);
    
  6. 适用pop , remove , truncate 和 clear 方法删除字符串中的字符

    • pop

      删除并返回字符串的最后一个字符,返回类型为Option<char>, 如果字符串为空,返回None

    • remove

    删除并返回字符串中指定位置的字符,参数是该字符的起始索引位置。

    remove方法是按字节处理字符串的,如果给定的索引位置不是合法的字符边界,将会导致程序错误。

    • truncate

      删除字符串中从指定位置开始到结尾的全部字符,参数是起始索引位置。

      truncate 也是按照字节处理字符串,如果给定的索引位置不是合法的字符边界,会导致程序错误。

    • clear

      删除字符串中所有字符

    s.pop();
    s.remove(9);
    s.truncate(9);
    s.clear();
    
字符串的访问
  • 字符串是UTF-8 编码的字节序列,不能直接使用索引来访问字符

  • 字符串操作可以分为按字节处理和按字符处理两种方式,按字节处理使用bytes方法返回字节迭代的迭代器,按字符处理使用chars方法返回按字符迭代的迭代器。

    len 方法获取以字节为单位的字符串长度

  • UTF-8 中字母1字节,特殊字符2字节,汉字3字节,不同字符的长度是不一样的。

s.len();
// 按字节遍历
let bytes = s.bytes();
for b in bytes {
    print!("{} |",b);
}
// 按字符遍历
let chars = s.chars();
for c in chars{
    print!("{} |",c);
}
字面量和运算符
字面量

由文字,数字或者符号构成的值

可以在字面量后面追加类型后缀进行类型说明:

  • 1u8 : 8位无符号整数
  • 1.2f32 32位浮点数

单元类型,单元类型的值叫做单元值,以()表示,一个函数无返回值,实际上是以单元值作为函数的返回值了。

运算符

支持算术运算符、关系运算符、逻辑运算符、位运算符

算术运算符

+ - * / % 

不支持 ++ 和 --

关系运算符

比较两个值之间的关系,并返回一个布尔类型的值

> < >= <= == != 

逻辑运算符

组合两个或者多个条件表达式,返回一个布尔类型的逻辑运算结果

&& || !

位运算符

对二进制格式的数据进行操作

& | ^ ! << >>

<< 和 >> 空白位都是补0