Rust | 01. 安装、基础、所有权、结构体、枚举

官网直接下载安装即可,有如下命令行可以使用:

  • rustup updat更新版本

  • rustup self uninstall卸载

  • 使用 rustc –version查看rust版本

    /image/Rust-01-Version.png
  • 包含本地文档,使用 rustup doc查看本地文档

  • 使用vscode作为开发工具,下载Rust插件作为辅助工具
  • 代码文件名后缀为.rs,代码需要编译,使用rustc 文件名.rc进行编译,成功后出现exe文件和pdb文件(包含调试信息),执行exe即可运行程序
  • main是程序最先运行的函数,缩进为四个空格,println!是一个宏
  • rust程序是预编译程序,其他电脑运行程序时不需要下载rust
  • Cargo是rust的构建系统和包管理工具,安装rusrt时同时安装,使用cargo –version命令确认安装
  • cargo new 项目名称:创建项目,完成后创建文件夹,包含toml配置文件和src源码文件,同时初始化了一个git仓库(gitignore文件)
  • toml里包含项目的相关信息(package信息块下),和其他的crate的依赖
  • 自己手动写出来这几个文件也相当于手动创建了项目
  • cargo build用于构建项目(编译),会在target目录下创建可执行文件和cargo.lock文件负责追踪版本
  • cargo run用于直接构建和运行项目,如果编译过且未修改则直接运行
  • cargo check用于检查代码而不产生可执行文件,该命令速度比build快很多,可以连续使用来检查
  • cargo build –release用于发布文件,产生的可执行文件在release目录下
use std::io;

fn main(){
    println!("Guess a number");
    let mut guess = String::new(); 
    // let定义,默认不可变,mut表示可变
    // String是utf-8编码,可以根据需求扩展大小;::new是关联函数,针对类型本身(而不是实例),相当于类函数或静态方法
    
    io::stdin().read_line($mut guess).expect("Unreadable");
    // $是取值符,参数以引用传递,在代码的不同位置使用同一块地址,默认引用也不可变,使用mut使其可变
    // io::Result是一个枚举类型,有OK和Err两种类型,当返回OK时提取数据并赋值,返回Err时则中断程序并执行expect内容
    
    println("The number you guessed is {}", guess);
}
  • crates.io/crate是Rust的仓库,crate是由源代码文件组成,有两种,包括二进制的可以行crate和不可执行的library crate(库包)
  • 在cargo.toml中修改dependencies即可下载,比如rand=“0.3.14”,表示下载该版本;版本号前也可以加^表示公共兼容该版本的任意版本;下载后第一次build会编译你下载的依赖,并将其写在cargo.lock文件,并将其锁定,避免重复搜索、下载,同时保证编译的可重复性
  • ctrl+shift+P,搜索rust可以开启或关闭rust服务器,从而打开自动下载或关闭
  • cargo update命令运行时会忽略cargo.lock,重新从网络抓取可使用的、最新版本的包,并将其重新下载、编译和写入cargo.lock文件
  • 声明变量需要使用let关键字
  • 数据默认不可变,需要使用mut关键字指定可变类型
  • constant和不可变变量式不一样的,其永远不可变,定义时类型必须标注,可以在任意作用域内声明,只可以绑定到常量表达式而不可以是函数调用结果或运算时才计算的值,命名需要全部大写字母加下划线,常量在运行期间在作用域内一直有效
  • 常量数字的字面量中可以使用下划线增强可读性
  • 使用相同的变量名,隐藏之前的变量,此时可以更改变量的类型

    let x = 5;
    x = x + 1; // 不合法
    let x = x + 1; //合法,隐藏之前的变量,x之后代表全新的变量
    
  • 使用usize.len()返回字符串的长度

  • 数据类型包含标量和复合类型两种:

  • 标量是指一个单独的值,包含整数、浮点、布尔、字符

    • 整数:无符号u32 0到2^32-1 / 含符号i32是指-2^32+1到2^32-1/i64,除byte外都可以标注类型后缀,整数默认i32;可以十进制、八进制、十六进制;在编译模式下整数溢出会报错,发布模式下则会顺延,256u8是0、257u8是1;usize是无符号的根据系统位数决定的类型
    • 浮点:f32、f64,IEEE-754标准,默认f64
    • 数值操作:+、-、*、/、取余%
    • 布尔:占用一个字节,true或false
    • 字符(char):单个字符,单引号,4个字节,unicode标量值,可以是多种语言的字符或emoji ​
  • 复合类型是多种类型在一起,Rust提供两种符合类型:

    • 元组Tuple:可以多个类型的多个值、长度固定、不可修改;(元素1, 元素2, ……)来定义;使用模式匹配来解构destructure元组的元素值 let (x, y, z) = tulpe;使用点标记法获取元素值,tuple.0,tuple.1……
    • 数组:多个值在一个类型里,类型相同,长度不可变;使用中括号,逗号分开;有利于栈存放或固定数量元素;但是没有Vector类型灵活(vector由标准库提供,数组由prelude提供),Vector长度可变;let a[i32, 5] = [1,2,3,4,5]来声明,注意数组的格式书写方法,let b[3,5]相当于[5,5,5,5,5];使用中括号索引;数组访问超出索引范围,编译有时会通过,运行时报错(Rust不允许连续访问其他相应地址的内存)
  • rust是静态编译语言,即在编译时需要知道变量的全部的变量类型;编译器可以根据写的数字自动推断变量的类型;但对于多种可能时,需要指定,比如parse()方法返回的具体类型可以是多种

  • main函数是程序的入口;Rust使用snack case命名规范;函数声明可以在运行之后(不像C只能在运行之前) ​

  • 函数的参数:parameters是形参,arguements是实参;在函数定义的签名里,必须指定参数的类型 ​

  • Rust是一个基于表达式的语言,语句是执行一些动作的指令,表达式是可以产生值的运算,语句没有返回值,所以不可以用于变量赋值;而函数体由一系列语句组成,可选的由一个表达式结束

    let x = {
        let y = 1;
        y + 1 //是表达式,可以作为x的赋值
        y + 1//是语句,返回值为一个空元组
    }
    
  • 函数的返回值不可以命名,在箭头 -> 符号的后面指定类型,默认最后一个表达式为返回值,提前返回时需要使用return关键字

​使用//表示单行注释;另一种注释为文档注释

条件判断:

  • if 表达式,条件必须是bool(不会发生自动类型转化),对应不同的arm

    let number = 6;
    if number % 4 == 0 {
        println!()
    } else if number % 3 == 0{
        println!() // 顺序执行, 会执行这一个arm,而不是下面整除2的
    } else if number %2 == 0{
        peintln!()
    } else {
        println!()
    }
    
  • if是表达式,可以写在let的右边;但是要求if和else的类型必须相同,类型安全!

    let number = if condition { 5 } else { 6 }
    

Rust提供了3种循环:

  • loop循环,一直执行到喊停为止,可以使用break停止或ctrl+C手动停止;下面的案例注意break后面的表达式和分号

    let mut counter = 0;
    let res = {
        counter += 1;
        if counter == 10{
            break counter * 2;
        }
    };
    
  • while条件循环,每次执行前判断,

    let mut number = 3;
    while number != 0 {
        println!("{}!", number);
        number = number - 1;
    }
    println!("LIFTOFF!");
    
  • for循环遍历集合,iter返回迭代器,element相当于一个指针

    let a = [1,2,3,4,5]
    for element in a.iter(){
        println(element);
    }
    
  • range由标准库提供,可以生成他们之间的数字,左闭右开,rev可以反转range

    for number in (1..4).rev(){
        println(number);
    }
    
  • 所有权是Rust最独特的特性,他让rust无需GC(垃圾收集器)就可以保证内存安全;所有程序在运行时都需要管理他们的内存使用方式,有些语言使用垃圾收集,也有些语言需要程序员显式地声明;而Rust使用了所有权机制来进行管理

  • Stack 栈内存 vs Heap 堆内存:stack和heap都是可用的内存,但其存储方式不一样,stack式先进后出(Last in first out),存储过程叫做压栈,stack必须拥有已知的固定大小(编译时大小未知或运行时大小可能变化的数据只能在Heap);Heap的内存组织性查一点,使用时系统在内存中找到一块足够大的空间,标记为在用并返回指针,即分配内存;而指针大小是一样的,所以可以将指针放在stack;存储和访问Heap的速度都较stack慢,因为内存空间存在跳转,需要利用指针调用

  • 所有权解决的问题:

    1. 跟踪代码哪些部分在使用Heap的哪些数据
    2. 最小化Heap上的重复数据
    3. 清理Heap上未使用的数据以避免空间不足;在学会所有权后则不需要频繁思考stack和heap问题
  • 所有权的3条规则:

    1. 每个值都有一个变量,这个变量是该值的所有者
    2. 每个值同时只能有一个所有者
    3. 当所有者超出作用域scope时,该值将被删除
  • 作用域是程序中一个项目的有效范围

    fn main(){
        // s不可用
        let s = 1; // s可用,可以对s进行相关操作
    } // 离开作用域,s不再可用
    

举例

  • 字符串字面值是程序中写好的那些字符串值,是不可变的,是硬编码到最终的可执行文件中的,其快速、高效

  • 而Rust提供另一种字符串,叫做String,是在运行期间可变的,存储在heap上;变量离开作用域时,会自动调用drop() ,从而将内存空间清空返还给操作系统

    let mut s = String::from("hello"); //使用from在函数运行时请求内存
    s.push_str(", world");
    println("{}", s);
    
  • 移动(Move):多个变量可以与同一个数据使用一种独特的方式来交互,例如

    let x = 5; // 变量5绑定到变量x上
    let y = x; // 创建了一个x的副本,绑定到变量y上
    // 此时两个整数(固定大小)都被压到stack中
    
  • 一个String由三部分组成:指向存放数据内容的指针、长度(len,字符串内容所需要的字节数)、容量(capacity是指String从操作系统总共获得内存的总字节数),这三个信息是固定大小,存放在stack上,而字符串的内容则在heap上

    let s1 = String::from("hello");
    let s2 = s1;
    
  • 当把s1赋值给s2时,String在stack上的信息复制了一份,但heap上的信息则没有被复制;当变量离开作用域时,Rust上自动调用drop()函数,将heap上的内存释放;当s1和s2离开时,如果没有处理机制就会产生二次释放Double Free Bug(第二次释放的空间可能已经是别的信息了)

  • 而在Rust中,s2产生时不会复制被分配的内存,而是让s1失效,这样s1离开作用域后,就不会释放任何东西;如果在失效后再次使用,就会报如下的编译错误:

    /image/Rust-01-Ownership-BorrowAfterMove.png

  • 浅拷贝:像是String只复制stack上指针等信息的操作;深拷贝则是指同时复制指针和数据;rust在浅拷贝的基础上还是得原变量失效,这两个操作合起来叫做移动(move)

  • Rust的设计原则之一:不会自动创建数据的深拷贝(就运行时性能而言,任何自动赋值的操作都是廉价的)

  • 克隆(clone):Rust上所谓的深拷贝,既复制heap又赋值stack;这种方法相对而言更消耗资源;let s2 = s1.clone()

  • 复制(copy):对于整数这种固定大小的类型,其数据本身保存在stack上,在二次赋值之后,就会发生一个直接拷贝(因为不在heap上,所以无关乎深浅拷贝的问题,其都会发生一次stack上的复制);如果一个类型实现了copy这个trait(trait可以暂时理解为接口),那么旧的变量在赋值后仍旧可用;如果一个类型或该类型的一部分实现了drop trait,rust就不允许它再去实现copy trait;任何简单标量的组和类型都是可以copy的,如u32、bool、char、f64、字段都可以copy的Tuple;任何需要分配内存或某种资源的都不是可copy的

    let x = 5;
    let y = x;
    println!("{}, {}", x, y) // x和y都是有效的
    
  • 在语义上,将值传递给函数和把值赋给变量是类似的;将值传递给函数会发生移动或复制

    fn main(){
        let s = String::from("hello");
        take_ownership(s);
        println!("s: {}", s); //发生移动后s失效,会报错
    
        let x = 5;
        make_copy(x); 
        println!("x: {}", x); //发生复制,some_number被drop不影响x
    }
    
    fn take_ownership(some_string: String){
        println!("{}", some_string);}
    
    fn make_copy(some_number: i32){
        println!("{}", some_number);
    }
    
  • 函数在返回值的过程中同样会发生所有权的转移;

    fn main(){
        let s = give_ownership(); // 把函数的返回值移动给s
    }
    
    fn give_ownership() -> String{
        let some_string = String::from("hello");
        some_string
    }
    
  • 一个变量的所有权总会遵循同样的模式:把一个值赋给其他变量时会发生移动;当一个包含heap数据的变量离开作用域时,它的值会被drop函数清理,除非数据的所有权移动到了另一个变量上

  • NEXT QUESTION:如何让函数使用某个值,但不获得其所有权?

    fn main(){
        let s1 = String::from("hello");
        let (s2, len) = calculate_length(s1);
        // 为了能够留住s1值的所有权,需要函数在使用后将其所有权再次返回
        println!("The length of {} is {}", s2, len);
    }
    
    fn calculate_length(s: String) -> (String, usize) {
        let length = s.len();
        (s, length)
    }
    
  • 另一种解决办法叫做引用(Reference)

  • 在变量名前加上&符号,表示引用,即允许你引用某些值而不获得其所有权

    fn main(){
        let s1 = String::from("hello");
        let len = calculate_length(&s1); //&s1指向了s1的值,但其不拥有s1,引用被清理,其指向数据不会被清除
        println!("The length of {} is {}", s1, len);
    }
    
    fn calculate_length(s: &String) -> usize {
        s.len()
    }
    
  • 以引用方式获得函数参数的行为叫做借用(borrow),借用的东西是不可修改的;引用默认也是不可修改的,使用mut关键字改为可变引用;

    fn main(){
        let mut s1 = String::from("hello");
        let len = calculate_length(&mut s1); //这里是可变引用
        println!("The length of {} is {}", s1, len);
    }
    
    fn calculate_length(s: &mut String) -> usize {
        s.push_str(", world");
        s.len()
    }
    
  • 可变引用的限制:在特定作用域内,对某一块数据,只能有一个可变的引用,防止数据竞争!可以通过创建新的作用域,来允许非同时的创建多个可变引用;此外,不可以同时拥有一个可变引用和一个不可变引用;但是可以同时存在多个不可变引用

    fn main(){
        let mut s = String::from("hello");
        {
            let s1 = &mut s;
        }
        let s2 = &mut s;
    }
    
  • 悬空指针是指一个引用了内存中某个地址,但这块内存可能已经释放并分配给其他人使用了,此时读取的数据是不正确的;而在Rust中,编译器可以保证引用永远不是悬空引用,即如果引用了某些数据,编译器将保证在引用离开作用域前数据不会离开作用域

    fn main(){
        let r = dangle(); // 报错缺少生命周期的说明符
    }
    
    fn dangle() -> &String{
        let s = String::from("hello");
        &s
    }
    
  • 引用的规则:

    1. 在任何给定的时刻,只能满足下列条件之一:
      1. 一个可变的引用
      2. 任意数量不可变的引用
    2. 引用必须一致有效
  • 切片(slice)是另一种不持有所有权的数据类型,再本质上是一种不可变引用(字符串字面值本质上就是字符串切片)

    let s = String::from("hello world");
    let world = &s[6..11]; //左闭右开,字符串切片,指向字符串的一部分
    let hello = &s[..5];
    let world = &s[6..];
    let whole = &s[..]; // 语法糖
    
  • Question:编写一个函数,接受字符串作为参数,返回其中的第一个单词,如果没有任何空格,则返回整个字符串

    fn main(){
        let mut s = String::from("Hello world");
        let word_index = first_word(&s);
        s.clear();
        println!("{}", word_index); 
        // word_index和s之间没有硬绑定关系,在s发生改变后,word_index已经失去了原有的意义,但在当前的程序中并不会体现
    }
    
    fn first_word(s:&Sting) -> usize {
        let bytes = s.as_bytes(); //将String转变为一个数组
        for (i, &item) in bytes.iter().enumerate(){
            if item == b' '{
                retrun i
            }
        }
        s.len()
    }
    
    fn main(){
        let mut s = String::from("Hello world");
        let word_index = first_word(&s);
        s.clear(); // 因为word_index绑定了s的不可变引用,s不可以再修改了
        println!("{}", word_index);
    }
    
    fn first_word(s:&Sting) -> &str {
        let bytes = s.as_bytes(); //将String转变为一个数组
        for (i, &item) in bytes.iter().enumerate(){
            if item == b' '{
                retrun &s[..i]
            }
        }
        &s[..] // 返回的切片是s的不可变引用
    }
    
  • 将字符串切片作为参数传递相比于字符串引用更好,因为&str可以同时接受String类型(制作一个完整切片传入即可 string[..])或&str类型数据,可以使API更加通用且不损失任何功能

  • 除字符串外,还存在一些其他类型的切片,如

    let a = [1,2,3,4,5];
    let slice = &a[1..3]; //slice是&[i32]类型
    
  • 结构体是自定义的数据类型,为相关联的值命名打包,成为有意义的组合;使用struct关键字命名,在花括号内列出所有字段,定义名称和类型;使用时需要实例化,指定具体的值(无需按照声明的顺序指定);使用点标记法访问字段;结构体内参数如果需要修改,需要指定整个结构体为mut类型,不允许仅部分字段可变;结构体可以作为函数的返回值;当字段名和变量名一致时可以简写为一个;如果想要基于某个struct实例创建一个新的实例时,可以使用struct的更新语法

    struct mut User{
        username: String,
        email: String,
        sign_in_count: u64,
        active: bool, //注意这里也有逗号
    }
    
    fn main(){
        let user1 = User{
            email: String::from("abc@126.com"),
            username: String::from("Nikky"),
            active: true,
            sign_in_count: 556, //数据不能少
        }; //记得这里的分号
    
        user1.email = String::from("123");
    
        //更新语法
        let user = User{
            email: String::from("another@example.com"),
            username: String::from("567"),
            ..user1
        }
    }
    
  • Tuple Struct:类似tuple的struct,整体有名,但里面的字段没有名

    struct Color(i32, i32, i32);
    struct Point(i32, i32, i32);
    let black = Color(0,0,0);
    let orgin = Point(0,0,0);
    // black和orgin是不同的类型
    
  • Unit-Like Struct(没有任何字段的结构体):与空元组()类似,适用于在某个类型上实现某个trait,但在里面有没有想要存储的数据

  • 关于结构体的所有权:在上面User的案例中,其使用了String类型,而不是&str,因而该结构体拥有其所有数据的所有权,只要strcut实例是有效的,里面的字段也就是有效的;而当struct里存放引用,需要考虑生命周期问题

  • 如果需要打印struct,需要实现display()这个trait,可以使用{:?}/{:#?}#[derive(Debug)]进行扩展显示

    #[derive(Debug)] //注解,一种格式化的方式
    struct Rectangle {
        width: u32,
        length: u32,
    }
    
    fn main() {
        let rect = Rectangle {
            width: 30,
            length: 50,
        };
        println!("{}", area(&rect));
    
        println!("{:?}", rect);
    }
    
    fn area(rect: &Rectangle) -> u32 {
        rect.width * rect.length
    }
    
  • 方法和函数类似,具有fn关键字、名称、参数、返回值;不同之处:方法是在struct的上下文中定义(impl块中),第一个参数可以是&self,表示方法被调用的struct实例(和其他参数一样,也可以获得其所有权或可变借用);每个Struct可以拥有多个impl块

    impl Rectangle {
        fn area(&self) -> u32 {
            self.width * self.length
        }
    }
    
    fn main(){
        println!("{}", rect.area())
    }
    
  • 在C/C++中,可以是对象.方法,如果是一个指针,可以是obj -> something() ,其含义等同于 (obj).something;而Rust中->表示返回值,不是指针对象上的方法,在方法调用时,其提供自动引用或解引用,即是说,在调用方法时,Rust会自动根据情况添加&、&mut或,以便object可以匹配方法的签名
  • 在impl块里还可以定义第一个参数不是self的函数,注意其不是方法,不依赖于实例调用,例如String::from(),通常用于构造器
  • 枚举允许我们列举所有可能的值来定义一个类型,使用enum关键字定义,列举的可能性被称为枚举变体

  • 枚举变体位于标识符的命名空间下,使用两个冒号::分割;枚举类型是一种自定义的数据类型,可以作为结构体内的字段

  • 此外,Rust允许将数据直接附加到枚举的变体中,此时就不需要额外再使用struct,每个变体可以拥有不同的类型及其关联的数据量

    enum IpAddrKind{
        V4,
        V6,
    }
    struct IpAddr{
        kind: IpAddrKind,
        address: String,
    }
    
    fn main(){
        let four = IpAddrKind::V4; //创建枚举值
        let six = IpAddrKind::V6;
        route(four);
        route(IpAddrKind::V4)
    }
    fn route(ip_kind:IpAddrKind){ }
    
    enum IpAddrKind{
        V4(u8,u8,u8,u8),
        V6(String),
    }
    
    fn main(){
        let home = IpAddrKind::V4(127,0,0,1); //创建枚举值
        let six = IpAddrKind::V6(String::from("::1"));
    }
    fn route(ip_kind:IpAddrKind){ }
    
  • 定义域标准库中,在prelude中,描述了某个值可能存在(某种类型)或不存在的情况;

  • 在其他语言中,Null是一个值,表示“没有值”,即是说一个变量可以处于两种状态,空或非空;而在Rust中,没有Null的概念(Null引用是一个Billion Dallar Mistake),其问题在于,当你像使用非空值那样来使用null值时,就会引起错误;但其概念仍旧是有意义的,其表示因某种原因而变为无效或缺失的值;

  • 在Rust中,为了解决Null问题,提供了一个类似Null概念的枚举,叫做Option,其在标准库中的定义如下,其中表示泛型参数;这种设计的好处在于Option和T不是同一类型,在使用时,必须进行转换,这就强制要求程序员解决值不存在的情况

    enum Option<T>{
        Some(T),
        None,
    }
    
  • 允许一个值与一系列模式进行匹配,并执行匹配到模式的对应代码;这些模式可以是字面值、变量名、通配符;需要注意的是,Match表达式需要穷举所有的格式匹配,如果不想细致书写,可以使用通配符_表示余下的可能

    #[derive(Debug)]
    enum UsState{ Alabama, Alaska }
    enum Coin{ Penny, Nickel, Dime, Quarter(UsState), }
    fn value_in_cents(coin:Coin) -> u8{
        match coin{
            Coin::Penny => {
                println!("1");
                1
            },
            Coin::Nickel => 5,
            Coin::Dime => 10,
            Coin::Quarter(state) => {
                println("{:?}", state);
                25
            },
        }
    }
    
fn pulse_one(x:Option<i32>) -> Option<i32>{
    match x{
        None => None,
        Some(i) => Some(i+1),
    }
}

-​ if let是一种简单的控制流,其处理只关心一种匹配而忽略其他匹配的情况(放弃了穷举的可能,可以看作是match的语法糖)

match v{
    Some(3) => println!("three"),
    _ => (),
}
if let Some(3) = v {
    println!("three")
} else {
    println!("others!")
}