三、基础语法及所有权、引用

96 阅读10分钟

前言

耗时5分钟

目录

三、基础语法及所有权、引用.png

一、变量

定义变量

如何声明变量?

let a = 123; 

Rust 它是强类型语言,具备自动判断变量类型能力。上面的 a 自动会推导成 i32类型(数据类型章节会介绍)。
等价于

let a:i32 = 123;

再来看下下面语句是否正确?

let a = 123;
a = 456;

上述代码在IDE中就会提示cannot assign twice to immutable variable 'a' , 所以let 只是定义不可变变量字段关键字
怎么定义可变变量?
引入 mut 关键字

let mut b = 123;
b = 456;

不可变变量和常量有什么区别?
看下面例子就知道了

fn main() {
    let a = 23;
    let a = 45;
    println!("a = {}", a);
}
输出结果是 a = 45;

但是按照下面这样写就不行了

fn main() {
    const a:i32 = 23;
    let a = 45; // 这行报错,introduce a variable instead: `a_var`
    println!("a = {}", a);
}

所以变量的值可以重新绑定,一个变量可以多次使用,允许同名变量存在,后面使用的将是最后一个同名变量,这种机制在 rust 中被称为 Shadowing 机制,隐藏机制,会将之前的的同名变量隐藏。
常量有命名规范,一般使用全大写字母加下划线。例如 const MAX_LENGTH:u32 = 10_000;(简写,就是=10000)

二、数据类型

你是不是对之前提到的 i32 很疑惑?怎么没见过,不应该是intbooleanbool 这种吗? 这是Rust的基本类型,Rust有整数型、浮点型、布尔型、字符型、复合型五种基本类型。

整数型

分为无符号和有符号两种

位长度有符号无符号
8biti8u8
16biti16u16
32biti32u32
64biti64u64
128biti128u128
archisizeusize

有符号的就是 i开头,无符号就是u开头
isizeusize :如果计算机是64位的,那就是64位,在32位机器上就是32位的。通常用于表示容器的大小或者数据的大小,可用于对某种集合进行索引操作。

一些表示方法示例

  • 十进制 98_222
  • 十六进制 0xff
  • 八进制 O0 77
  • 二级制 0b1111_0000
  • Byte(u8 only) b'A'

浮点型

32位64位
f32f64
let x = 2.0; //默认等价于 let x:f64 = 2.0;

布尔型

let x = true;//等价于 let x:bool = true;

字符型

字符型,4个字节,单引号,使用char关键字

let a = '❤'; //等价于 let a:char = '❤';

它代表 Unicode标量值,这意味着它可以支持中文,日文和韩文字符等非英文字符甚至表情符号和零宽度空格,所以上面这样写每问题。

复合型

    let (x,y,z) = (1,2,3);
    let tup:(i32, f32, f64) = (10, 10.1, 10.2);
    let (i,j,k) = tup;

相当于模式匹配,这种利用元组的写法来达到赋值,内部对应的数据结构可以不一样。而数组就不同了。
数组就必须要一样的类型

let a = [1,2,3,4,5]; //等价于 let a:[i32;5] = [1,2,3,4];
let b = [3;5] ; //等价于 let b = [3, 3, 3, 3, 3, 3 ];

同样数组也符合可变和不可变准则,想要可以修改数组的值,需要加上mut关键字,例如 let mut a = [1, 2,3];

三、基本操作

条件语句

fn main() {  
    let a = 1;
    let b ;
    if (a == 1) {
        b = 1;
    }else if (a == 2){
        b = 2;
    }else{
        b = 3;
    }
    println!("b == {}", b);
}

循环

fn main() {  
    let mut number = 1;  
    while number != 4 {  
        println!("{}", number);  
        number += 1;  
    }  
    println!("EXIT");  
    
    let a = [1020304050];  
    for i in a.iter() {  
        println!("值为 : {}", i);  
    }
    for i in 0..5 {  
        println!("a[{}] = {}", i, a[i]);  
    }
}

除了上面三种循环外,还有loop循环(无限循环),不推荐使用

fn main() {  
    let s = ['R''U''N''O''O''B'];  
    let mut i = 0;  
    let location = loop {  
        let ch = s[i];  
        if ch == 'O' {  
            break i;  
        }  
        i += 1;  
    };  
    println!(" \'O\' 的索引为 {}", location);  
}

四、函数

函数定义

函数示例如下

fn main() {
    print_function();
    print_function2(3);
}
fn print_function(){
    let x = 3;
    println!("x = {}", x);
}

fn print_function2(x: i32){
     println!("x = {}", x);
}

print_function()函数的定义无所谓,不像C++,必须要先声明

函数体的语句和表达式

fn main() {
    let a = 6;
    let b = test();
    let c = test;
    print!("c() == {}" , c());
}
输出:
c() == 3

还有比较花的写法

fn main() {
    let x = 5;
    let y = {
        let x = 3;//它的生命周期只在{}中
        println!("x == {}" , x);
        x + 1
    };
    println!("x == {}",x);
    println!("y == {}" , y);
}
输出
x == 3
x == 5
y == 4

五、所有权

所有权是Rust最独特的特性,它让Rust无需GC就可以保证内存安全。它这个规则,在编译器编译的时候就会检查。
Rust它自己内存申请和释放,也是基于堆和栈,对于一些定义固定大小,例如定义的一个变量,这些处于内存的栈中。而对于无法在编译阶段令程序分配固定长度的内存空间,都会在堆中去分配。基本上所有说的内存资源都是堆空间。在内存释放的时候,Rust它自己会调用drop函数。
即拥有的某个变量走出作用域时,内存就会立即自动free掉,交还给系统

那么Rust是怎么申请内存的?

  • 操作系统会在heap中找到一块足够大的空间,把它标记为在用,并返回一个指针(这个空间的地址)。
  • 依旧是通过指针访问heap中的数据

所有权解决的问题

  • 跟踪代码哪些部分正在使用heap的哪些数据
  • 最小化heap上的重复数据
  • 清理heap上未使用的数据以避免空间不足

所有权的三条规则

  • 每个值都有一个变量,这个变量是该值的所有者
  • 每个值同时只能有一个所有者
  • 当所有者超出作用域时,该值将被删除

简而言之,就是作用域内,一个值只能有一个所有者

数据的移动

1.栈
基本类型都是放在栈中,这种属于直接复制数据给另一个变量

let x = 4;
let y = x;

能够在栈上存放的数据,都是实现了Copy trait的,什么是Copy? 理解为栈上复制的接口,有这个就可以复制栈上的数据,所以遇到不支持栈上复制的数据,就可以实现Copy trait。基本标量类型都是支持的。 例如下y 就是复制了 x的栈上数据,复制后 , xy都能用
2.堆

    let s1 = String::from("hello");
    let s2 = s1;
    println!("s1 = {}", s1);//报错

这个和Java类似,s1 存在栈中,指向堆内存。s2 也是存在于栈中,指向同一块堆内存,s1s2 不相同
但是你知道 第二句 let s2 = s1 发生了什么吗?

  • 将栈中数据赋值给 s2,此时s1 已经无效了,不能再用了,等出了作用域就都被清理了。这可以叫做所有权移交
  • 这完美解决了 C++ 中,一个对象内存被释放两次的问题

既然是堆,那肯定要释放资源,它是自动调用drop函数释放,实现了Drop trait的才会在堆上申请内存,内存释放。 它和Copy trait 一个类型中只能存在一个。

数据的拷贝

我们应该都知道深拷贝和浅拷贝,理念都是一样的。
刚才介绍的堆的例子,其实就是浅拷贝,知识拷贝了栈上数据而已,指向的堆内存还是一样的。我们称为这个叫Move,而深拷贝称为Clone
Rust有一个设计原则,不会自动创建数据的深拷贝,所以运行时性能很好。想要深拷贝,就需要调用clone方法。

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();
    println!("s1 = {}", s1);
}

六、所有权在函数上的应用

实现了Copy Trait类型的所有权示例

基本类型都是实现了Copy Trait,就是之前说的栈上复制内存

fn main() {
    let x = 8;
    make_copy(x); // 传入的是 x的副本
    println!("x == {}", x); //这块依旧可以操作x
}
fn make_copy(number: i32){
    println!("number = {} ",  number);
}
输出
number = 8 
x == 8

实现了Drop Trait类型的所有权示例

fn main() {
    let s = String::from("Hello World");
    make_drop_test(s);
    println!("s == {}", s); //这行直接编译器前就语法报错
}
fn make_drop_test(str: String){
     println!("str == {}", str);
}//str被释放

强制运行结果是

10 |     let s = String::from("Hello World");
   |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
11 |     make_drop_test(s);
   |                    - value moved here
12 |     println!("s == {}", s);
   |                         ^ value borrowed here after move

说没有实现Copy Trait, 也就是说make_drop_test(s) 传递的不是副本,在调用 make_drop_test()后,str也就出了作用域,立马就被释放了。

返回值的所有权移动

一个变量的所有权移动符合以下规则

  • 把一个值赋给其它变量就会发生移动
  • 当一个包含heap数据的变量离开了作用域,它的值就会被drop函数清理。除非数据所有权移动到了另一个变量

例如

fn main() {
    let s = String::from("Hello World");
    let s2 = make_drop_test(s); //所有权给了s2
    println!("{}", s);//报错 s的所有权移交给了s2 ,s不能再用了
    println!("{}", s2);
}
fn make_drop_test(str: String) -> String{
     str
}

但是我们其实想调用make_drop_test()只是使用s的值,我不想让它的所有权移交出去,怎么办呢?

这就需要用到引用了

七、引用与借用

引用与借用

同样是之前的所有权移交例子,修改一下

fn main() {
    let s = String::from("Hello World");
    let s2 = calc_length(&s); //传递引用
     println!("{} len is {}",s, s2);//我们还能继续用s
}
fn calc_length(str: &String) -> usize{
     str.len()
}
输出
Hello World len is 11

使用引用,就能实现我们引用某些值而不取得它的所有权。
我们这种把引用作为函数参数的行为称为借用,我把引用借给你用,所有权还归我,但是你只能读,不能写
例如

fn main() {
    let s = String::from("Hello World");
    let s2 = calc_length(&s); //传递引用
     println!("{} len is {}",s, s2);
}
fn calc_length(str: &String) -> usize{
     str.push_str("Hello World"); //这行会报错
     str.len()
}

会报错

cannot borrow `*str` as mutable, as it is behind a `&` reference  
`str` is a `&` reference, so the data it refers to cannot be borrowed as mutablerustc[E0596](https://doc.rust-lang.org/error-index.html#E0596)

就是说我给你了,但是我没有给你写的权限,除非加一个mut关键字,成为可变引用。

fn main() {
    let mut s = String::from("Hello World");
    let s2 = calc_length(&mut s); //传递引用
     println!("{} len is {}",s, s2);
}
fn calc_length(str: &mut String) -> usize{
     str.push_str("Hello World");
     str.len()
}

引用注意事项

在一个作用域内只能有一个可变引用。

  • 这样就避免了多个指针访问,同时对同一个数据进行写入

1.不在同一个作用域内的可变引用

fn main() {
    let mut s = String::from("Hello Rust!");
    {
        let s1 = &mut s;
    } //可变引用 s1出了作用域
    let s2 = &mut s;
}

2.同一个作用域内的可变引用

fn main() {
    let mut s = String::from("Hello Rust!");
    {
        let s1 = &mut s;
        let s2 = &mut s; //这里会报错,只要你用了就会报错
        println!("s1 = {} , s2 = {}", s1,s2);
    }  
   
}

3.同一时刻不能有一个可变引用和一个不可变引用
可变引用可以改变值,不可变引用

fn main() {
    let mut s = String::from("Hello Rust!");
    let m = &s;
    let s1 = &mut s;//报错
    println!("{} {}", m, s1);
}
`mut` can be used in several situations. The first is mutable variables, which can be used anywhere you can bind a value to a variable name

悬空引用

什么叫悬空引用?

  • 一个指针引用了内存中的某个地址,而这块内存可能已经释放并分配给其它人用了。
  • Rust里,编译器可以保证引用永远都保证不是悬空引用 例如
fn test() -> &String{ //报错
    let s = String::from("Hello");
    &s
}//s出了作用域 但是s竟然作为返回值,对s的引用返回了。s又被释放了,所以返回的引用指针会指向一个已经被释放的地址