Rust学习笔记-2-基础篇:变量、常量、基本数据类型

前言

本文主要对Rust基础概念中关于变量、常量、数据类型的知识点进行梳理。

变量

  • 变量默认不可变,不能对不可变变量进行二次赋值。要使变量可变,可以在变量名前mut;
  • 变量在使用前,必须初始化。编译器会做一个执行路径的静态分析,确保变量在使用前一定被初始化。
  • 变量可以在同一代码块被遮蔽(Shadowing,有些语言也允许遮蔽,但只能在嵌套的子块内)。
  • 变量的标识符必须由数 字、字母、下划线组成,且不能以数字开头。不能使用关键字作为标识符,如果一定要,可加上前缀:r#。单个下划线(_)作为标识符,表示忽略此绑定。
  • 变量申明时,可以省略类型,由编译器根据上下文进行类型推导;Rust允许对局部变量和全局变量进行推导,函数签名的类型不允许推导;类型推导的示例:字面量后带上类型后缀(例如:let elem = 5u8;)、Vec声明时不指定类型,当push item后编译器自动推导;
  • 可以用type关键字给某个类型取个别名;类型别名也可用在泛型场景。
  • 静态变量使用static关键字进行声明;其生命周期为'static,在整个程序运行期间都存在;static是Rust中声明全局变量的唯一方法;
  • 全局变量的规则:1,声明时必须马上初始化,且必须是编译期可确定的常量,2,带mut的全局变量,使用时必须使用unsafe关键字;3,禁止声明时调用普通函数(或者其他的非const代码);
  • 如果用户需要使用比较复杂的全局变量初始化,推荐使用lazy_static库。

常量

  • 常量使用const关键字进行声明;不允许用mut修饰;初始化表达式必须是编译期常量;
  • 和static声明的变量相比:编译器并不一定会给const常量分配内存空间,在编译过程中,它很可能会被内联优化。

注:

let语句不光是局部变量声明语句,而且具有pattern destructure(模式解构)的功能;与let语句一样,static语句同样也是一个模式匹配;但const声明一个常量,不具备类似let语句的模式匹配功能。

基本数据类型

分为标量类型和复合类型。标量类型有:整型、浮点型、布尔型、字符型。复合类型有:tuple(元组)、数组、struct、enum。

标量类型

  • 整型:i8/i16/i32/i64/i128/isize(有符号)和 u8/u16/u32/u64/u128/usize(有符号)
  • isize 和 usize 类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。isize 或 usize 主要作为某些集合的索引。
  • 16进制:前缀0x;8进制:前缀0o;二进制:0b;单字节(仅限u8):前缀b。除 byte 以外的所有数字字面值允许使用类型后缀,例如 57u8,同时也允许使用 _ 做为分隔符以方便读数,例如1_000。一些示例如下:
    let var1 : i32 = 32;          // 十进制表示 
    let var2 : i32 = 0xFF;        // 以0x开头代表十六进制表示 
    let var3 : i32 = 0o55;        // 以0o开头代表八进制表示 
    let var4 : i32 = 0b1001;      // 以0b开头代表二进制表示
    let var5 = 0x_1234_ABCD;        //使用下划线分割数字,不影响语义,但是极大地提升了阅读体验。
    let var6 = 123usize;        // var6变量是usize类型 
    let var7 = 0x_ff_u8;        // var7变量是u8类型 
    let var8 = 32;              // 不写类型,默认为 i32 类型
    fn main() {    println!("9 power 3 = {}", 9_i32.pow(3)); } //可以不使用变量,直接对整型字面量调用函数。
  • 整数溢出:Rust在这个问题上选择的处理方式为:默认情况下,在debug模式 下编译器会自动插入整数溢出检查,一旦发生溢出,则会引发panic;在 release模式下,不检查整数溢出,而是采用自动舍弃高位的方式。Rust编译器还提供了一个独立的编译开关供我们使用,通过这个开关(overflow-checks为yes或者no),可以设置溢出时的处理策略。
    $ rustc -C overflow-checks=no test.rs
  • 如果需要更精细地自主控制整数溢出的行 为,可以调用标准库中的checked_*、saturating_*和wrapping_*系列函 数。
    fn main() {    
        let i = 100_i8;    
        println!("checked {:?}", i.checked_add(i)); //输出:checked None,checked_*系列函数返回的类型是Option<_>,当出现溢 出的时候,返回值是None;
        println!("saturating {:?}", i.saturating_add(i)); //输出:saturating 127,saturating_*系列函数返回类型是整数,如果 溢出,则给出该类型可表示范围的“最大/最小”值;
        println!("wrapping {:?}", i.wrapping_add(i)); //输出:wrapping -56,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); 
    }
    
  • Rust 的浮点数类型是 f32 和 f64,分别占 32 位和 64 位。默认类型是 f64。一些示例如下:
    let f1 = 123.0f64;        //type f64 
    let f2 = 0.1f64;          //type f64 
    let f3 = 0.1f32;          //type f32 
    let f4 = 12E+99_f64;      //type f64 科学计数法 
    let f5 : f64 = 2.;        //type f64
    
  • 在标准库中,有一个std::num::FpCategory枚举,表示了浮点数可能的状态:
    enum FpCategory {    
        Nan,    //Nan代表的是“不是数字”(not a number)。 
        Infinite,  //Infinite代表的是“无穷大”. 
        Zero,    //Zero表示0值
        Subnormal, 
        Normal, //Normal表示正常状态的浮点数
    }

    注:subnormal 请参考非规格化浮点数

  • NaN这个特殊值有个特殊的麻烦,主要问题还在于它不具备“全序”的特点(一个数字可以不等于自己)。因为NaN的存在,浮点 数是不具备“全序关系”(total order)。
    fn main() {    
        let nan = std::f32::NAN;    
        println!("{} {} {}", nan < nan, nan > nan, nan == nan); 
    }
    //输出结果:false false false
  • Rust 中的布尔类型有两个可能的值:true 和 false。Rust 中的布尔类型使用 bool 表示。
  • Rust 的 char 类型是语言中最原生的字母类型。Rust 的 char 类型的大小为四个字节(four bytes),并代表了一个 Unicode 标量值。以下为一些示例:
    let c1 = '\n';  //换行符
    let c2 = '\x7f';  //8 bit 字符变量
    let c3 = '\u{7FFF}';  //unicode字符
    let x :u8 = 1;
    let y :u8 = b'A';
    let s :&[u8;5] = b"hello";
    let r :&[u8;14] = br#"hello \n world"#;  //第2个#不能删除。否则编译器会认为后面的字符全部都是。
    

tuple

  • 元组(tuple)通过圆括号包含一组表达式来表示;元组长度固定:一旦声明,其长度不会增大或缩小;为了从元组中获取单个值,可以使用模式匹配(pattern matching)来解构(destructure)元组值;除了使用模式匹配解构外,也可以使用点号(.)后跟值的索引来直接访问它们。
    let a = (1i32, false);              // 元组中包含两个元素,第一个是i32类型,第二个是bool类型 
    let b = ("a", (1i32, 2i32));        // 元组中包含两个元素,第二个元素本身也是元组,它又包含了一个元组
    let a = (0,);       // a是一个元组,它有一个元素
    let b = (0);        // b是一个括号表达式,它是i32类型。注意它没有逗号
    
    let p = (1i32, 2i32); 
    let (a, b) = p; //模式解构方式访问
    let x = p.0; let y = p.1; println!("{} {} {} {}", a, b, x, y);//索引方式访问
    
  • 元组内部也可以一个元素都没有,称之为:unit。unit类型是Rust中最简单的类型之一(它占用0内存空间),也是占用空间最小的类型之一(空struct也是如此)。示例如下:
    let empty : () = ();

struct

  • struct 定义、初始化方式、简化的初始化方式,如下示例。相比元组,它的成员有名字。
    //定义示例
    struct Point { x: i32, y: i32, }
    
    //初始化示例
    fn main() {    
        let p = Point { x: 0, y: 0};    // key: value 键-值对的形式,提供各个成员的值
        println!("Point is at {} {}", p.x, p.y); 
    }
    
    //如果有局部变量名字和成员变量名字相同时,那么可以简化如下:
    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); 
    }
    
  • struct有两种访问方式:“点”号加变量名;模式解构。如下示例。
    //两种访问方式:“点”号加变量名;模式解构。
    fn main() {    
        let p = Point { x: 0, y: 0};    
        // 声明了px 和 py,分别绑定到成员 x 和成员 y    
        let Point { x : px, y : py } = p;    
        println!("Point is at {} {}", px, py);    
        // 同理,在模式匹配的时候,如果新的变量名刚好和成员名字相同,可以使用简写方式    
        let Point { x, y } = p;    println!("Point is at {} {}", x, y); 
    }
    
  • 简化struct赋值的语法糖:default函数和..expr语法,也叫结构体更新语法(struct update syntax
    struct Point3d { x: i32, y: i32, z: i32, }
    
    //default()函数示例
    fn default() -> Point3d {    
        Point3d { x: 0, y: 0, z: 0 } 
    }
        
    // 可以使用default()函数初始化其他的元素 
    let origin = Point3d { x: 5, ..default()}; 
    
    // ..expr 这样的语法,只能放在初始化表达式中所有成员的最后,最多只能有一个 
    let point = Point3d { z: 1, x: 2, ..origin };
    
  • 定义空struct的方式。没有成员的struct也叫类单元结构体unit-like structs)。其常见的应用场景:当你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。
    //以下三种都可以,内部可以没有成员 
    struct Foo1; 
    struct Foo2(); 
    struct Foo3{}
    
  • 定义tuple struct的方式如下。tuple struct有名字,而它们的成员没有名字。就像是tuple和struct的混合体。tuple struct有一个特别有用的场景:那就是当它只包含一个元素的 时候,就是所谓的newtype idiom。
    struct Color(i32, i32, i32); 
    struct Point(i32, i32, i32);
  • 定义struct时,要考虑成员的所有权。可以使struct存储被其他对象拥有的数据的引用,但此时需要用上 生命周期lifetimes)注解。如下例,username 不是String而是&str,表示该成员的所有权不归User。此时编译器会报错。解决办法:要么将&str改成String;要么加上生命周期注解。所有权和生命周期后续会详细解释。
    struct User {
        username: &str, 
        email: &str,
        sign_in_count: u64,
        active: bool,
    }
    
    fn main() {
        let user1 = User {
            email: "someone@example.com",
            username: "someusername123",
            active: true,
            sign_in_count: 1,
        };
    }
    
    //加上生命周期注解:
    struct User<'a> {
        username: &'a str, 
        email: &'a str,
        ...//此处省略
    }

enum 

  • enum中的每个元素的定义语法与struct的定义语法类似。可以像空结构体一样,不指定它的类型;也可以像tuple struct一样,用圆括号加无名成员;还可以像正常结构体一样,用大括号加带名字的成员。以下是示例:
    //定义方式一:
    enum IpAddrKind { V4, V6,}
    
    struct IpAddr {
        kind: IpAddrKind,
        address: String,
    }
    
    let home = IpAddr {
        kind: IpAddrKind::V4,
        address: String::from("127.0.0.1"),
    };
    
    
    //定义方式二:更简洁。使用枚举并将数据直接放进每一个枚举成员,而不是将枚举作为结构体的一部分。
    //无需一个额外的struct
    enum IpAddr {
        V4(String),
        V6(String),
    }
    
    let home = IpAddr::V4(String::from("127.0.0.1"));
  • 定义enum时可以为每个成员指定附属的类型信息。用enum替代struct还有另一个优势:每个成员可以处理不同类型和数量的数据。如下示例。

    enum IpAddr {
        V4(u8, u8, u8, u8),
        V6(String),
    }
    
    let home = IpAddr::V4(127, 0, 0, 1);
    let loopback = IpAddr::V6(String::from("::1"));
  •  enum和struct还有另一个相似点:就像可以使用 impl 来为struct定义方法那样,也可以在enum上定义方法。如下示例:

    impl Message {
        fn call(&self) {
            // 在这里定义方法体
        }
    }
    
    let m = Message::Write(String::from("hello"));
    m.call();
  • Rust中常见的一个enum:Option<T>。定义如下。因为太常用,所以被包含进prelude,使用时你无需显式地引入作用域;其设计目的是为了限制空值的泛滥以增加 Rust 代码的安全性;将T从Some(T)取值出来,可以参考它的文档(看了下,是unwrap*系列方法)。

    enum Option<T> {
        Some(T),
        None,
    }
  • Rust的enum类型的变量需要区分它里面的数据究竟是哪种变体,所 以它包含了一个内部的“tag标记”来描述当前变量属于哪种类型。这个标记对用户是不可见的(但会占用存储空间)。如果是在FFI场景下,要保证Rust里面的enum的内存布局和C语言兼 容的话,可以给这个enum添加一个#[repr(C,Int)]属性标签。

注 :

1,《Rust深入浅出》中提到这个属性标签#[repr(C,Int)]当时有通过设计还未实现。动手试了下,发现编译器会报错。使用#[repr(C)] 虽然不报错,但不确定是否同样的意义。列成一个TODO项吧。

2,《Rust深入浅出》中举了一个例子(如下代码)。文中有句话:“它总共占用的内存是8 byte,多出来的4 byte就是用于保存类型标记的”。那么对于成员为不同类型的enum,其总共占用的空间该如何计算呢?是各个成员所占用空间的最大值,再加上一个类型标记(4字节)吗?动手试了一下。如下的Message占32byte,成员中的类型String占24byte(成员中它最多),多出来的8个byte怎么来的?还不太理解。列成一个TODO项吧。

//此例来自《深入浅出》2.3.4
fn main() {    
// 使用了泛型函数的调用语法,请参考第21章泛型    
    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>()); 
}
//输出是:8,4,4

//自动动手试的代码
#![allow(unused)]
fn main() {
    enum Message {
        Quit,
        Move { x: i32, y: i32 },
        Write(String),
        ChangeColor(i32, i32, i32),
    }
    
    println!("Size of Message: {}", std::mem::size_of::<Message>()); //为啥是32?目前还未理解。
    println!("Size of String: {}", std::mem::size_of::<String>()); 
    println!("Size of String: {}", std::mem::size_of_val(&Message::Move{x:10,y:11}));//看任意一个成员占多少。答案是和Message一样多。这个倒还好理解。
}
//输出是32,24,32
  • Rust里面也支持union类型,这个类型与C语言中的union完全一致。 但在Rust里面,读取它内部的值被认为是unsafe行为,一般情况下我们 不使用这种类型。它存在的主要目的是为了方便与C语言进行交互。 

  • 在Rust中,enum和struct为内部成员创建了新的名字空间。如果要 访问内部成员,可以使用::符号。因此,不同的enum中重名的元素 也不会互相冲突。

  • 也可以手动指定每个变体自己的标记值。如下示例:

    fn main() {    
        enum Animal {        
            dog = 1,        
            cat = 200,        
            tiger,    
        }    
        let x = Animal::tiger as isize;    
        println!("{}", x); 
    }
    
  • Rust的enum实际上是一种代数类型系统(Algebraic Data Type, ADT)。enum内部的variant只是一个名字而已,恰好我们还可以将这个名字作为类型构造器使用。意思是说,我们可以把enum内部的variant当成一个函数使用。如下例子:
    //这是rust标准库中定义的:enum Option<T> { None, Some(T), } 
    
    fn main() {    
        let arr = [1,2,3,4,5];    
        // 请注意这里的map函数    
        let v: Vec<Option<&i32>> = arr.iter().map(Some).collect();    
        println!("{:?}", v); 
    }
    
  •  enum和struct还有另一个相似点:就像可以使用 impl 来为struct定义方法那样,也可以在enum上定义方法。如下示例:

数组

  • 数组中的值位于中括号内的逗号分隔的列表中;Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,它们的长度不能增长或缩小。
  • 声明数组:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量。例如:
    let a: [i32; 5] = [1, 2, 3, 4, 5];
  • 如果希望创建一个每个元素都相同的数组,可以在中括号内指定其初始值,后跟分号,再后跟数组的长度。例如:

    let a = [3; 5];
  • 数组并不如 vector 类型灵活。当不确定是应该使用数组还是 vector 的时候,你可能应该使用 vector。

  • 数组是一整块分配在栈上的内存。可以使用索引来访问数组的元素。数组访问越界时,编译虽然通过,但程序运行时会panic。

  • 类型转换可以使用as关键字;类型转换须是显式,编译器不会做自动地隐式转换;as关键字也不是随便可以用的,它只允许编译器认为合理的类型转换;有些时候,甚至需要连续写多个as才能转成功;如果需要更复杂的类型转换,一般是使用标准库的From Into等 trait,

注:

1,这些知识点主要参考了官方的《Rust程序设计语言》和《Rust深入浅出》。《Rust编程之道》还没看。后续若看了再更新此文。

2,针对复合数据类型,例如struct、enum、数组等,本章主要说明了其概念和定义方式等。还有些其他知识点(使用方式、高级主题相关)等,不适合放在本篇。后续笔记要再补上这一块。列成TODO项吧。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值