Rust学习-变量和类型

变量和类型

1.1 变量声明

  Rust的变量必须先声明后使用。对于局部变量,最常见的声明语法为:

let variable : i32 = 100;

  与传统的C/C++语言相比,Rust的变量声明语法不同。这样的设计主要有以下几个方面的考虑。

  1. 语法分析更容易
    从语法分析的角度来说,Rust的变量声明语法比C/C++语言更简单,局部变量声明一定是以let开头,类型一定是跟在冒号:后面。语法歧义更少,语法分析器更容易编写。
  2. 方便引入类型推导功能
    Rust的变量声明的一个重要特点是:要声明的变量前置,对它的类型描述后置。这也是吸取了其他语言的教训后的结果。因为在变量声明语句中,最重要的是变量本身,而类型其实是个附属的额外描述,并非必不可少的部分。
  3. 模式解构
    let语句不光是局部变量声明语句,而且具有parttern destructure(模式结构)的功能。
    Rust中声明变量缺省是"只读"的,比如如下程序:
fn main() {
    let x = 5;
    println!("{}", x);
    x = 10;
    println!("{}", x);
}

会得到“cannot assign twice to immutable variable x”这样的编译错误。
如果我们需要让变量是可写的,那么需要使用mut关键字:

fn main() {
    let mut x = 5;
    println!("{}", x);
    x = 10;
    println!("{}", x);
}

此时,变量x才是可读写的。
  实际上,let语句在此处引入了一个模式解构,我们不能把let mut视为一个组合,而应该将mut x视为一个组合。
  let mut是一个“模式”,我们还可以用这种方式同时声明多个变量:

let (mut a, mut b) = (1, 2);
let Point {x: ref a, y: ref b} = p;

  Rust中,每个变量必须被合理初始化之后才能被使用。使用未初始化变量这样的错误,在Rust中是不可能出现的。也不可能编译通过:

fn main() {
    let x : i32;
    println!("{}", x)
}

错误信息为: used binding x isn’t initialized
编译器会帮我们做一个执行路径的静态分析,确保变量在使用前一定被初始化:

fn test(condition: bool) {
    let x : i32; // 声明变量x,不必使用mut修饰
    if condition {

        x = 1; // 初始化x,不需要x是mut的,因为这是初始化,不是修改

        println!("{}", x);
    }
    // 如果条件不满足,x没有被初始化
    // 不使用x就没关系
}

1.1.1 变量遮蔽

  Rust允许在同一个代码块中声明同样名字的变量。如果这样做,后面声明的变量会将前面声明的变量"遮蔽"起来

fn main() {
    let x = "hello";
    println!("x is {}", x);

    let x = 5;
    println!("x is {}", x);
}

  上面这个程序是可以编译通过的。注意第5行代码,它不是x=5;它前面有一个let关键字。如果没有这个let关键字,这条语句就是对x的重新赋值。而有了这个let关键字,就是又声明了一个新的变量,只是它的名字恰巧与前面一个变量相同而已。
  但是这两个x代表的内存空间完全不同,类型也完全不同,他们实际上是两个不同的变量。从第5行开始,一直到这个代码快结束,我们没有办法再去访问前一个x变量,因为它的名字已经被遮蔽了。
  变量遮蔽在某些情况下非常有用,比如,我们需要在同一个函数内部把一个变量转换为另一个类型的变量,但又不想给他们起不同的名字。再比如,在同一个函数内部,需要修改一个变量绑定的可变性。例如,我们对一个可变数组执行初始化,希望此时它是可读写的,但是初始化完成后,我们希望它是只读的。可以这样做:

fn main() {
    let mut v = Vec::new();
    v.push(1);
    v.push(2);
    v.push(3);

    let v = v;
    for i in &v {
        println!("{}", i);
    }
}

反过来,如果一个变量是不可变的,我们也可以通过变量遮蔽创建一个新的、可变的同名变量。

fn main() {
    let v = Vec::new();
    let mut v = v;
    v.push(1);
    println!("{:?}", v);
}

1.1.2 类型推导

  Rust的类型推导功能是比较强大的。它不仅可以从变量声明的当前语句中获取信息进行推导,而且还能通过上下文信息进行推导。

fn main() {
    // 没有明确标出变量的类型,但是通过字面量的后缀,编译器知道elem的类型为u8
    let elem = 5u8;

    // 创建一个动态数组,数组内包含的是什么元素类型可以不写
    let mut vec = Vec::new();
    vec.push(elem);
    // 到后面调用了push函数,通过elem变量的类型,编译器可以推导出vec的实际类型是Vec<u8>

    println!("{:?}", vec);
}

1.1.3 类型别名

  我们可以用type关键字给同一个类型起个别名(type alias)。示例如下:

type Age = u32;

fn grow(age: Age, year: u32) -> Age {
    age + year
}

fn main() {
    let x : Age = 20;
    println!("20 years later: {}", grow(x, 20)) 
}

类型别名还可以用在泛型场景,比如:
type Double<T> = (T, Vec<T>)
那么以后使用Double的时候,就等同于(i32, Vec),可以简化代码

1.1.4 静态变量

Rust中可以用static关键字声明静态静态变量。如下所示:
static GLOBAL: i32 = 0;
  与let语句一样,static语句同样也是一个模式匹配。与let语句不同的是,用static声明的变量的生命周期是整个程序,从启动到退出。static变量的声明周期永远是'static,他占用的内存空间也不会再执行过程中回收。这也是Rust中唯一的声明全局变量的方法。
  由于Rust非常注重内存安全,因此全局变量的使用有许多限制。这些限制都是为了防止程序员写出不安全的代码:

  • 全部变量必须在声明的时候马上初始化;
  • 全局变量的初始化必须是编译期可确定的常量,不能包括执行期才能确定的表达式、语句和函数调用;
  • 带有mut修饰的全局变量,在使用的时候必须使用unsafe关键字
    示例如下:
fn main() {
    // 局部变量声明,可以留待后面初始化,只要保证使用前已经初始化即可
    let x;
    let y = 1_i32;
    x = 2_i32;
    println!("{} {}", x, y);

    // 全局变量必须声明的时候初始化,因为全局变量可以写到函数外面,被任意一个函数使用
    static G1 : i32 = 3;
    println!("{}", G1);

    // 可变全局变量无论读写都必须用unsafe修饰
    static mut G2 : i32 = 4;
    unsafe {
        G2 = 5;
        println!("{}", G2)
    }
// 全局变量的内存不是分配到当前函数栈上,函数推出的时候,并不会销毁全局变量占用的内存空间,程序退出才会回收
}

Rust禁止在声明static变量的时候调用普通函数,或者利用语句块调用其他非const代码:

fn main() {
    // 这样是允许的
    static array : [i32; 3] = [1, 2, 3];
    // 这样是不允许的
    static vec : Vec<i32> = { let mut v = Vec::new(); v.push(1); v};
}

1.1.5 常量

在Rust中还可以用const关键字做声明。如下所示:
const GLOBAL: i32 = 0;
  使用const声明的是常量,而不是变量。因此一定不允许使用mut关键字修饰这个变量绑定,这是语法错误。常量的初始化表达式也一定要是一个编译期常量,不能是运行期的值。它与static变量的最大区别在于:编译器并不一定会给const常量分配内存空间,在编译过程中,它很可能会被内联优化。因此,千万不要用hack的方式,通过unsafe代码去修改常量的值,这么做是没有意义的。以const声明一个常量,也不具备类似let语句的模式匹配功能。

1.2 基本数据类型

1.2.1 bool

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

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);
}

一些比较运算表达式的类型就是bool类型:

fn logical_op(x: i32, y: i32) {
    let z: bool = x < y;
    println!("{}", z);
}

bool类型表达式可以用在if/while等表达式中,作为条件表达式 。比如:

if a >= b{
    ...
} else {
    ...
}

1.2.2 char

  字符类型由char表示。它可以描述任何一个符合unicode标准的字符值。在代码中,单个的字符字面量用单引号包围。
let love = 'a'; // 可以嵌入任何unicode字符
字符类型字面量也可以使用转义符:

let c1 = '\n';      // 换行符
let c2 = '\x7f';    // 8 bit 字符变量
let c3 = '\u{7FFF}'; // unicode字符

1.2.3 整数类型

  Rust有很多的数字类型,主要分为整数类型和浮点数类型。各种整数类型之间的主要区分特征是:有符号/无符号,占据空间大小。

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

  所谓有符号/无符号,指的是如何理解内存空间中bit表达的含义。如果一个变量是有符号类型,那么它的最高为的哪一个bit就是"符号位",表示该数为正值还是负值。如果一个变量是无符号类型,那么它的最高为和其他位一样,表示该数的大小。比如对于一个byte(8 bits)的数据来说,如果存的是无符号数,那么它的表达范围是0~255,如果存的是有符号数,那么它的表达范围是-128~127.
  关于各个整数类型所占据的空间大小,在名字中就已经表现的很明确了。Rust原生支持了从8位到128位的整数。需要特别关注的是isize和usize类型。它们占据的空间是不定的,与指针占据的空间一致,与所在平台有关。
  数字类型的字面量表示可以有许多方式:

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;    // i6变量是usize类型
let var7 = 0x_ff_u8;    // i7变量是u8类型
let var8 = 32;          // 不写类型,默认为i32类型

  在Rust中,我们可以为任何一个类型添加方法,整形也不例外。比如在标准库中,整数类型有一个方法是pow(),它可以计算n次幂,于是我们可以这么用:

let x: i32 = 9;
println!("9 power 3 = {}", x.pow(3));

同理,我们甚至可以不使用变量,直接对整形字面量调用函数:
println!("9 power 3 = {}", 9_i32.pow(3));
我们可以看到这是非常方便的设计。
  对于整数类型,如果Rust编译器通过上下文无法分析出该变量的具体类型,则自动默认为i32类型。比如:

fn main() {
    let y = 10;
    let z = y * y;
    println!("{}", z);
}

  在此例中,编译器只知道x是一个整数,但是具体是i8 i16 i32或者u8 u16 u32等,并没有足够的信息判断,这些都是有可能的。在这种情况下,编译器就默认把x当作i32类型处理。这么做的好处是,很多时候,我们不想在每个地方都明确地指定数字类型,这么做很麻烦。

1.2.4 整数溢出

  在整数的算数运算中,有一个比较头疼的事情是"溢出"。在c语言中,对于无符号类型,算数运算永远不会overflow,如果超过表示范围,则自动舍弃高位数据,对于有符号类型,如果发生了overflow,标准规定这是undefined behavior,也就是说随便怎么处理都可以。

  Rust的设计思路更倾向于预防bug,而不是无条件的压榨效率,Rust设计者希望能尽量减少"未定义行为"。比如彻底杜绝"Segment Fault"这种内存错误是Rust的一个重要设计目标。当然还有许多其他种类的bug,即便是无法完全解决,我们也希望能尽量避免。整数溢出就是这样的一种bug.
  Rust在这个问题上的处理方式为:默认情况下,在debug模式下编译器会自动插入整数溢出检查,一旦发生溢出,则会引发pacin;在release模式下,不检查整数溢出,而采用自动舍弃高位的方式。实力如下:

fn arithmetic(m: i8, n: i8) {
    // 加法运算,有溢出风险
    println!("{}", m + n)
}

fn main() {
    let m:i8 = 120;
    let n:i8 = 120;
    arithmetic(m, n)
}

执行这个程序,结果为:
thread 'main' pacicked at 'attempt to add with overflow', test.rs:3:20

1.2.5 浮点类型

  Rust提供了基于IEEE 754-2008标准的浮点类型。按占据空间大小区分,分别为f32和f63,其使用方法与整形区别不大。浮点数字面量表示方式有如下几种。

fn main() {
    let f1 = 123.0f64;   // type f64
    let f2 = 0.1f64;     // type f64
    let f3 = 0.1f32;     // type f64
    let f4: f64 = 12E+99_f64; // type f64 科学计数法
    let f5: f64 = 2.;         // type f64
}

  与整数类型相比,Rust的浮点数类型相对复杂的多。浮点数的麻烦之处在于:它不仅可以表达正常的数值,还可以表达不正常的数值。

1.2.6 指针类型

无GC的变成语言,如C、C++以及Rust,对数据的组织操作有更多的自由度,具体表现为:

  • 同一类型,某些时候可以指定它在栈上,某些时候可以指定它在堆上。内存的分配方式可以取决于使用方式,与类型本身无关。
  • 既可以直接访问数据,也可以通过指针间接当问数据。可以针对任何一个对象取得指向它的指针。
  • 既可以在符合数据类型中直接嵌入别的类型的实体,也可以使用指针,简介指向别的类型。
  • 甚至可能在符合数据类型末尾嵌入不定长数据构造出不定长的复合数据类型。
    Rust里面也有指针类型,而且不止一种指针类型。常见的集中指针类型如下:
类型名简介
Box指向类型T的、具有所有权的指针,有权释放内存
&T指向类型T的借用指针,也称为引用,无权释放内存,无权写数据
&mut T指向类型T的mut型指针,无权释放内存,有权写数据
*const T指向类型T的只读裸指针,没有声明周期信息,无权写数据
*mut T指向类型T的可读写裸指针,没有生命周期信息,有权写数据

  除此之外,在标准库中还有一种封装起来的可以当作指针使用的类型,叫"智能指针"。常见的智能指针如下:

类型名简介
Rc指向类型T的引用计数指针,共享所有权,线程不安全
Arc指向类型T的原子性引用计数指针,共享所有权,线程安全
Cow<'a, T>Clone-on-write,写时复制指针。可能是借用指针,也可能是具有所有权的指针

1.2.7 类型转换

  Rust对不同类型之间的转换控制的非常严格。即便是下面这样的程序,也会出现编译错误:

fn main() {
    let var1: i8 = 41;
    let var2: i16 = var1;
}

  编译结果为mismatched types!i8类型的变量无法向i16类型的变量赋值!这可能对于很多用户来会所都是一个意外。
  Rust提供了一个关键字as,专门用于这样的类型转换:

fn main() {
    let var1: i8 = 41;
    let var2: i16 = var1 as i16;
    println!("{}", var2)
}

  也就是说,Rust设计者希望在发生类型转换的时候不是偷偷摸摸进行的,而是显式的标记出来,防止隐藏bug.虽然在很多时候会让代码显得不那么精简,但这也算是一种合理的折中。
as关键字也不是随便可以用的,它只允许编译器认为合理的类型转换。任意类型转换是不允许的。

fn main() {
    let a = "some string";
    let b = a as u32; // 编译错误
}

  有些时候,甚至需要连续写多个as才能转换成功,比如&i32类型就不能直接转换为*mut i类型,必须像下面这样写才可以:

fn main() {
    let a = "some string";
    let p = &i as *const i32 as *mut i32;
    println!("{:p}", p);
}

  as表达式允许的转换类型如表所示。对于表达式e as u,e是表达式,u是要转换的目标类型,表中所示的类型转换是允许的。如果需要更复杂的类型转换,一般是使用标准库的From Into等trait.

Type of eU
Integer or Float typeInteger or Float type
C-like enumInteger type
bool or charInterger type
i8char
*T*V whereV: Sized *
*T whereT: SizedNumeric type
Integer type*V whereV: Sized
&[T; n]*const T
Function pointer&V whereV: Sized
Function pointerInteger

1.3 复合数据类型

  复合数据类型可以在其他类型的基础上组成更复杂的组合关系。本章介绍tuple、struct等集中复合数据类型。

1.3.1 tuple

  tuple指的是元组类型,它通过元括号包含一组表达式构成。tuple内的元素没有名字。tuple是把集合类型组合到一起的最简单的方式。比如:

初始化

fn main() {
    let a = (li32, false); // 元组中包含两个元素,第一个是i32类型,第二个是bool类型
    let b = ("a", (li32, 2i32)); // 元组中包含两个元素,第二个元素本身也是元组,它又包含两个元素
}

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

let a = (0, )
let b = (0)

访问元组元素

访问元组内部元素有两种方法,一种是模式匹配,另一种是数字索引

fn main() {
    let p = (1i32, 2i32);
    // 索引访问
    let x = p.0;
    let y = p.1;
    // 解构元组
    let (a, b) = p;
    println!("{} {} {} {}", a, b, x, y);
    // 模式匹配
    match p {
        (age, hei) => {
            println!("age: {} hei: {}", age, hei);
        }
    }
    // 元组输出
    println!("{:?}", p)
}

1.3.2 struct

结构体与元组类似,也可以把多个类型组合到一起,作为新的类型。区别在于,它的每个元素都有自己的名字。举个例子:

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

结构体实例化

每个元素之间采用逗号分开,最后一个逗号可以忽略不写。类型依旧跟在冒号后面,但是不能使用自动类型推导功能,必须显式指定。struct类型的初始化语法类似于json的语法,是哟将那个"成员-冒号-值"的格式。

fn main() {
    // 声明式实例化
    let p = Point{x: 0, y: 1};
    println!("Point is at {} {}", p.x, p.y);

    // 可变式实例化
    let mut p = Point{x:0, y: 1};
    p.x = 10;
    p.y = 11;
    println!("Point is at {} {}", p.x, p.y);
}

有些时候,Rust允许struct类型的初始化使用一种简化写法。如果有局部变量名字和成员变量名字恰好一致,那么可以忽略掉重复的冒号初始化:

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);
}

访问结构体元素

访问结构体内部的元素,也是使用"点"加变量名的方式。当然,我们也可以使用模式匹配功能。

fn main() {
    let p = Point{x: 0, y: 1};
    // 声明了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);
}

结构体的方法

在Rust中,结构体可以拥有自己的方法。方法是与结构体关联的函数,可以通过结构体实例调用。

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

impl Point {
    fn hello(&self) -> &str{
        return "hello";
    } 
}

let p = Point{x: 0, y: 1};
println!("{}", p.hello());

在上述示例中,定义了一个名为Point的结构体,并为其实现了一个hello方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值