rust学习 -- 第二章 数据类型

170 阅读14分钟

第二章 数据类型

介绍示例
数组, 固定大小的同构序列 [T; N][u32; 16]
布尔值true, false
utf-8'a' '%'
浮点数0f32 3.1415926
函数指针fn(&str) -> usize
分配在堆上的类型Tlet v: Box = Box::new(1)
T要么存在, 要么为NoneSome(42) None
要么成功Ok(T) 要么失败 Err(E)Ok(42) Err(ConnectionError::TooMany)
可变列表, 分配在堆上let mut arr = vec!
字符串let s = String::from("hello");
哈希表let map: HashMap<&str, &str> = HashMap::new();
集合let set: HashSet = HashSet::new();
为T提供内部可变性的智能指针let v = RefCell::new(42); let mut borrowed = v.borrow_mut();
为T提供引用计数的智能指针let v = Rc::new(42) let v1 = Arc::new(42);

标量类型+复合类型

通常编译器可以推断出类型.但是复杂情况需要自己指定新类.

let guess:u32 = "42".parse().expect("Not a number");

标量类型

  1. 整数类型
  2. 浮点类型
  3. 布尔类型
  4. 字符类型

布尔类型

布尔类型(bool)代表的是“是”和“否”的二值逻辑.它有两个值: truefalse.一般用在逻辑表达式中,可以执行“与”“或”“非”等运算.

fn main() { 
    let x = true;
    let y: bool = !x; // 取反运算 
​
    let z = x && y; // 逻辑与,带短路功能 
    println!("{}", z); 
​
    let z = x || y; // 逻辑或,带短路功能 
    println!("{}", z); 
​
    let z = x & y; // 按位与,不带短路功能 
    println!("{}", z); 
​
    let z = x | y; // 按位或,不带短路功能 
    println!("{}", z); 
​
    let z = x ^ y; // 按位异或,不带短路功能 
    println!("{}", z); 
}

char类型

它可以描述任何一个符合unicode标准的字符值.在代码中,单个的字符字面量用单引号包围.

   fn main(){
       let x = 'z';
       let y: char = '¥';
       let z = '😀';
       let love = '❤'; // 可以直接嵌入任何 unicode 字符
   ​
   }

因为char类型的设计目的是描述任意一个unicode字符,因此它占据的内存空间不是1个字节,而是4个字节.

对于ASCII字符其实只需占用一个字节的空间,因此Rust提供了单字节字符字面量来表示ASCII字符.我们可以使用一个字母b在字符或者 字符串前面,代表这个字面量存储在u8类型数组中,这样占用空间比char型数组要小一些.

    let x :u8 = 1;
    let y :u8 = b'A';
    let s :&[u8;5] = b"hello";
    let r :&[u8;14] = br#"hello \n world"#;

整数类型

各种整数类型之间的主要区分特征是: 有符号/无符号, 占据空间大小.

整数类型有符号无符号
8 bitsi8u8
16 bitsi16u16
32 bitsi32u32
64 bitsi64u64
128 bitsi128u128
Pointer sizeisizeusize

Rust原生支持了从8位到128位的整数. 需要特别关注的是isize和usize类型. 它们占据的空间是不定的, 与指针占据的空间一致, 与所在的平台相关. 如果是32位系统上, 则是32位大小; 如果是64位系统上, 则是64位大小.

    let var1 : i32 = 32; // 十进制表示 
    let var2 : i32 = 0xFF; // 以0x开头代表十六进制表示 
    let var3 : i32 = 0o55; // 以0o开头代表八进制表示 
    let var4 : i32 = 0b1001; // 以0b开头代表二进制表示

在Rust中,我们可以为任何一个类型添加方法,整型也不例外.我们甚至可以不使用变量,直接对整型字面量调用函数:

    fn main() {
        println!("9 power 3 = {}", 9_i32.pow(3));
    }

如果推断不出数据的类型,默认使用i32

整数溢出

默认情况下,在debug模式 下编译器会自动插入整数溢出检查,一旦发生溢出,则会引发panic;在 release模式下,不检查整数溢出,而是采用自动舍弃高位的方式.

如果编译一个优化后的版本,加上-O选项:

    rustc -O test.rs 

Rust编译器还提供了一个独立的编译开关供我们使用,通过这个开关,可以设置溢出时的处理策略

    rustc -C overflow-checks=no test.rs

如果用户确实需要更精细地自主控制整数溢出的行为,可以调用标准库中的checked_* saturating_*wrapping_*系列函数

    fn main() {
        let i = 100_i8;
        println!("checked {:?}", i.checked_add(i));
        println!("saturating {:?}", i.saturating_add(i));
        println!("wrapping {:?}", i.wrapping_add(i));
    }

可以看到:checked_*系列函数返回的类型是Option<_>,当出现溢出的时候,返回值是None;saturating_*系列函数返回类型是整数,如果溢出,则给出该类型可表示范围的“最大/最小”值;wrapping_*系列函数 则是直接抛弃已经溢出的最高位,将剩下的部分返回.

为了 方便用户,标准库还提供了一个叫作std::num::Wrapping<T>的类型.它重载了基本的运算符,可以被当成普通整数使用.凡是被它包裹起来的整数,任何时候出现溢出都是截断行为.

    use std::num::Wrapping;
    ​
    fn main() {
        let big = Wrapping(std::u32::MAX);
        let sum = big + Wrapping(2_u32);
        println!("{}", sum.0);
    }

浮点类型

按占据空间大小区分,分别为f32和f64. 浮点类型的默认类型是 f64

表示形式有如下几种:

    let f1 = 123.0f64;    // f64
    let f2 = 0.1f64;      // f64
    let f3 = 0.1f32;      // f32
    let f4 = 12E+99_f64;  // f64 科学计数法
    let f5 : f64 = 2;     // f64
浮点数的不正常状态

浮点数的麻烦之处在于:它不仅可以表达正常的数值,还可以表达不正常的数值.

在标准库中,有一个std::num::FpCategory枚举,表示了浮点数可能的状态:

    enum FpCategory {
        Nan,
        Infinite,
        Zero,
        Subnormal,
        Normal,
    }

其中Zero表示0值、Normal表示正常状态的浮点数.

Subnormal状态是一种非常小的处在收敛的状态, 他是不准确的

Infinite和Nan是带来更多麻烦的特殊状态.Infinite代表的是“无穷大”,Nan代表的是“不是数字”(not a number).

    fn main() {
        let x = 1.0f32 / 0.0;
        let y = 0.0f32 / 0.0;
        println!("{} {}", x, y);
    }

编译执行,打印出来的结果分别为inf NaN.0数除以0值,得到的是inf,0除以0得到的是NaN.

NaN这个特殊值有个特殊的麻烦,主要问题还在于它不具备“全序”的特点.

    fn main() {
        let nan = std::f32::NAN;
        println!("{} {} {}", nan < nan, nan > nan, nan == nan);
    }
    // 结果是  false false false

也就是他的数值是不确定的,在任何时候都不确定.

指针类型

rust对数据的组织操作有多种维度:

  • 同一个类型, 某些时候可以指定它在栈上, 某些时候可以指定它在堆上. 内存分配方式可以取决于使用方式, 与类型本身无关.
  • 既可以直接访问数据, 也可以通过指针间接访问数据. 可以针对任何一个对象取得指向它的指针.
  • 既可以在复合数据类型中直接嵌入别的类型的实体, 也可以使用指针, 间接指向别的类型.
  • 甚至可能在复合数据类型末尾嵌入不定长数据构造出不定长的复合数据类型.

rust中有多种指针类型 image-20211224032415504.png 除此之外, 在标准库中还有一种封装起来的可以当作指针使用的类型, 叫“智能指针”(smart pointer)

image-20211224032443385.png

后面再具体介绍

类型转换

Rust对不同类型之间的转换控制得非常严格.必须显式的进行类型转换 以防止出现bug

    fn main() {
        let var1: i8 = 41;
        let var2: i16 = var1 as i16;
        // 如果你写成let var2: i16 = var1; 那么就会报错
    }

使用as进行强制的类型转换.并且只允许编译器认为合理的类型转换, 任意类型的转换也是不允许的,比如 将一个非数字字符转换为数字.有些时候, 甚至需要连续写多个as才能转成功, 比如&i32类型就不能直接转换为*mut i32类型, 必须像下面这样写才可以:

    fn main() {
        let i = 42;
        let p = &i as *const i32 as *mut i32;
        println!("{:p}", p);
    }
    fn main() {
        let i = 42;
        println!("{:p}", &i);
        let p = &i as *const i32 as *mut i32;
        unsafe {
            *p = 2;  // 裸指针需要放到 unsafe中
        }
        println!("{:?} {:p}", unsafe{*p}, p);  // 取值的时候也要
    }

as表达式允许的类型转换如表所示. 对于表达式e as U, e是表达式, U是要转换的目标类型, 表中所示的类型转换是允许的.

image-20211224034105163.png

如果需要更复杂的类型转换, 一般是使用标准库的From,Into等trait.

    &str    -> String--| String::from(s) or s.to_string() or s.to_owned()
    &str    -> &[u8]---| s.as_bytes()
    &str    -> Vec<u8>-| s.as_bytes().to_vec() or s.as_bytes().to_owned()
    String  -> &str----| &s if possible* else s.as_str()
    String  -> &[u8]---| s.as_bytes()
    String  -> Vec<u8>-| s.into_bytes()
    &[u8]   -> &str----| s.to_vec() or s.to_owned()
    &[u8]   -> String--| std::str::from_utf8(s).unwrap(), but don't**
    &[u8]   -> Vec<u8>-| String::from_utf8(s).unwrap(), but don't**
    Vec<u8> -> &str----| &s if possible* else s.as_slice()
    Vec<u8> -> String--| std::str::from_utf8(&s).unwrap(), but don't**
    Vec<u8> -> &[u8]---| String::from_utf8(s).unwrap(), but don't**

复合类型

元组 tuple

创建tuple:

在小括号里,将值用逗号分开.tuple中的每一个位置都对应一个类型,tuple中各个元素类型可以不同.tuple是把几个类型组合到一起的最简单的方式.

    let a: (i32, bool) = (1i32, false);
    let b = ("a", (1i32, 2i32));

如果元组中只包含一个元素, 应该在后面添加一个逗号, 以区分括号表达式和元组:

    let a = (0,); // a是一个元组,它有一个元素
    let b = (0); // b是一个括号表达式,它是i32类型

获取tuple中的值:

  1. 类似于python中的序列解包(模式匹配 pattern destructuring)
  2. 直接获得指定位置的值,点表记法,后接元素的索引号.
    fn main(){
        let some:(i32, f64, u8) = (50, 1.4, 3);
    ​
        let (x, y, z) = some;
    ​
        println!("{},{},{}", some.0, some.1, some.2);
    }

元组内部也可以一个元素都没有. 这个类型单独有一个名字, 叫unit (单元类型):

let empty: () = ();

它占用0内存空间,这和空结构体一样. 使用std::mem::size_of::<i8>()可以计算空间大小

fn main() {
    println!("size of '()' {}", std::mem::size_of::<()>());  // 大小为0
}

数组 array

数组中的每个元素的类型必须是相同的. 数组的长度也是固定的,声明之后不能改变.数组是stack上分配的单个块的内存.

    fn main(){
        // 声明数组
        let a[i32; 5] = [1,2,3,4,5];  // a[i32; 5] [类型; 长度]
    ​
        let a = [3; 5]; // a == [3,3,3,3,3] 初始化的方法
    ​
        let first = a[0]; // 访问数组元素
    }

如果访问的索引超出了数组的范围,那么 编译会通过并且不会报错, 运行时会报错!!!!!

结构体 struct

和元组类似,但是struct中的每个元素都可以有自己的名字

    struct Point {
        x: i32,
        y: i32,
    }

每个元素之间采用逗号分开, 最后一个逗号可以省略不写. 类型依旧跟在冒号后面, 但是不能使用自动类型推导功能, 必须显式指定.

struct类型的初始化:

语法类似于json的语法, 使用“成员–冒号–值”的格式.

    fn main() {
        let p = Point {x: 0, y: 0};
        println!("Point is at {} {}", p.x, p.y);
    }

struct有一种简化的写法,如果有局部变量的名字和成员变量的名字恰好一致, 那么可以省略重复的冒号初始化. 另一种简化方式是, 使用另一个struct的部分成员.

    struct Point{
        x: i32,
        y: i32
    }
    ​
    fn main() {
        // 刚好局部变量名字和结构体成员名字一致
        let x = 10;
        let y = 20;
        // 下面是简略写法, 等同于 由冒号的写法 Point {x:x, y:y}
        let p = Point {x, y};
    ​
        println!("Point is at {} {}", p.x, p.y);
    }

rust 允许使用一种简化的语法赋值,使用另一个struct的部分成员,初始化struct.

    struct Point3d {
        x: i32,
        y: i32,
        z: i32,
    }
    ​
    fn default() -> Point3d{
        Point3d {x: 0, y: 0, z: 0}
    }
    ​
    fn main() {
        // 使用default()函数初始化其他的元素 
        // ..expr 这样的语法, 只能放在初始化表达式中,所有成员的最后最多只能有一个
        let origin = Point3d {x: 5, ..default()};  // 赋值 给y,z
        let point = Point3d {z: 1, x: 2, ..origin};   // 赋值给z
    }

获取结构体内部元素的方法, 1 加点的方式, 2 模式匹配

    fn main() {
        let x = 10;
        let y = 20;
        let p = Point {x, y};
    ​
        let Point {x: px, y: py} = p;
        println!("Point is at {} {}",px, py);
        let Point {y, x} = p;  // 在模式匹配的时候 也可以使用简化写法, 会按照名字匹配
        println!("Point is at {} {}",x, y);
    }

tuple struct

rust 有一种数据类型叫 tuple struct, 他是tuple和struct的混合. 区别在于 tuple struct 有名字, 而他的成员没有名字.

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
​
// they are defined as follows
struct Color{
    0: i32,
    1: i32,
    2: i32,
}

虽然它们的内部结构是一样的,但是它们是完全不同的两个类型.

tuple、 struct、 struct tuple 的区别

image-20211225164531631.png 除了这些之外,他们没有区别了,整体对比

    struct T1 {
        v: i32  // define struct
    }
    ​
    struct T2(i32);  // define tuple structfn main(){
        let v1 = T1 { v: 1 };
        let v2 = T2(1);          // init tuple struct
        let v3 = T2 { 0: 1 };    // init tuple struct
    ​
        let i1 = v1.v;
        let i2 = v2.0;
        let i3 = v3.0;
    }

tuple struct有一个特别有用的场景, 那就是当它只包含一个元素的时候, 就是所谓的newtype idiom. 让我们非常方便地在一个类型的基础上创建了一个新的类型.

    fn main() {
        struct Inches(i32);
    ​
        fn f1(value: Inches) {}
        fn f2(value: i32) {}
    ​
        let v : i32 = 0;
        f1(v);  // 编译不通过,'mismatched types'
        f2(v);
    }

编译不通过是因为Inchesi32是不同的类型,函数调用参数不匹配.

    fn type_alias() {
        type I = i32;  // 当换成别名的时候就通过了
    ​
        fn f1(v : I) {}
        fn f2(v : i32) {}
    ​
        let v : i32 = 0;
        f1(v);
        f2(v);
    }

这是因为通过type创建的只是一个新的类型名称,但这个类型不是全新的类型,而只是一个具体类型的别名.在编译器看来, 这个别名与原先的具体类型是一模一样的. 而使用tuple struct做包装, 则是创造了一个全新的类型, 它跟被包装的类型不能发生隐式类型转换, 可以具有不同的方法, 满足不同的trait, 完全按需而定.

enum

枚举在rust中代表了的类型关系.

rust中的enum要强大很多, 它可以为每个成员指定附属的类型信息.

Rust的enum中的每个元素的定义语法与struct的定义语法类似.

  1. 可以像空结构体一样, 不指定它的类型;
  2. 也可以像tuple struct一样, 用圆括号加无名成员;
  3. 还可以像正常结构体一样, 用大括号加带名字的成员.
    enum Number {
        Int(i32),
        Float(f32),
    }

使用match语句读取enum内部数据的示例

    enum Number {
        Int(i32),
        Float(i32),
    }
    ​
    fn read_num(num: &Number) {
        match num {
            // 如果匹配到了 Number::Int, value就是i32
            &Number::Int(value) => println!("Integer {}", value),
            // 如果匹配到了 Number::Float, value就是f32
            &Number::Float(value) => println!("Float {}", value),
        }
    }
    ​
    fn main() {
        let n: Number = Number::Int(10);
        read_num(&n);
    }

neum需要记住当前存贮的值的类型,在C语言中需要程序员自己记录,在rust 中则是编译器帮助你处理.所以我们不需要自己处理值的类型.

我们查看类型的大小,可以看到 Number的类型是i32和f32中size最大的.max(sizeof(i32), sizeof(f32)) = max(4 byte, 4 byte) = 4 byte

而它总共占用的内存是8 byte, 多出来的4 byte就是用于保存类型标记的. 之所以用4 byte, 是为了内存对齐.

    fn main() {
        println!("Size of Number: {}", std::mem::size_of::<Number>());
        println!("Size of i32:    {}", std::mem::size_of::<i32>());
        println!("Size of f32:    {}", std::mem::size_of::<f32>());
    }

Rust标准库中有一个极其常用的enum类型Option<T>, 它的定义如下 :

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

由于它实在是太常用, 标准库将Option以及它的成员Some、 None都加入到了Prelude中, 用户甚至不需要use语句声明就可以直接使用.它表示的含义是“要么存在、 要么不存在”. 比如 Option<i32>表达的意思就是“可以是一个i32类型的值, 或者没有任何值”. 还有一个是 Result<T, E>

    enum Result<T, E> {
       Ok(T),
       Err(E),
    }

Some(T) 不能直接当T来使用, Rust就是这么设计, 需要你单独处理Some(T), 当处理完成获得T后, T一定是有值的, 不会再有空值的情况, 可以放心的处理.

    let x: i8 = 5;
    let y: Option<i8> = Some(5);
    let sum = x + y;  // 报错!

如何获取其中的值?

    fn main() {
        let x: i8 = 5;
        let y: Option<i8> = Some(5);
    ​
        let _y = if let Some(_y) = y {  // 方案一
            _y
        } else {
            0  // 给了个默认值
        }
        let _y = y.expect("获取到不正确的值!");  // 方案二
        let _y = y.unwrap();  // 方案三
    }