RUST 入坑笔记

本文最后由 森林生灵 于 2022/02/13 12:58:32 编辑

文章目录 (?) [+]

    RUST

    Date: 2021/03/22

    Date: 2022/02/12


    1. 所有权机制

    2. 借用和生命周期

    3. 类型系统和 trait

    4. 突破抽象范式

    5. Unsafe Rust

    Hello World

     fn main() {
         println!("Hello World");
     }

    开发环境

     # 安装
     curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
     # 升级
     rustup update
     # 卸载
     rustup self uninstall
     
     # 编译
     rustc main.rs
     
     # 创建项目
     cargo new hello_world
     # 构建项目
     # --release 优化编译
     cargo build
     # 编译并运行
     cargo run
     # 代码检查
     cargo check


    数据类型

    数据存储在 Stack 上。

    标量类型(Scalar Type)

    标量类型代表一个单独的值。

    整型

    std::u128:MAX 定义最大整型为 340282366920938463463374607431768211455

    std::i128:MIN 定义最小整型为 -170141183460469231731687303715884105728

    sizesignedunsigned
    8 biti8u8
    16 biti16u16
    32 biti32u32
    64 biti64u64
    128 biti128u128
    archisizeusize
    举例字面值
    10_24十进制(Decimal)
    0xff十六进制(Hex)
    0o66八进制(Octal)
    0b1100_1101二进制(Binary)
    b'A'字节(Byte)仅限 u8 类型

     fn main() {
         // 默认标量类型为 i32
         let i = 01_86; // 186
         let num: i8 = 0x7f;
         // debug 模式溢出会抛出 panic,release 模式下会执行“环绕”操作,即从最小值开始循环
         let overflow: i8 = num+1;
         // 除 byte 外类型所有数值字面值可以使用类型后缀
         let cnt = 64u8;
     
         // println! 是 rust macro(宏)
         println!("i = {}, num = {}, cnt = {}, overflow = {}", i, num, cnt, overflow);
     }
     
     // --release
     // i = 186, num = 255, cnt = 64, overflow = -128

    浮点型

    数字类型不能自动类型转换。

     fn main() {
         // 默认类型 f64
         let f = 64.00;
         println!("f is {:?}", f);
     
         let f: i64 = f as i64;
         println!("f is {:?}", f)
     }
     
     /*
     f is 64.0
     f is 64
     */

    布尔型

    只有 truefalse 两种值,占用 1 个字节大小。

    字符型

    存放单个字符,表示一个 Unicode 标量值,字面值使用单引号,占用 4 个字节大小。

     fn main() {
         // 默认类型为 char
         let alphabet = 'A';
         let chinese = '丁';
         let emoji = '🦀';
         println!("{}, {}, {}", alphabet, chinese, emoji);
     }
     
     // A, 丁, 🦀

    复合类型(Compound Type)

    复合类型可以将多个值组合成一个类型。

    元组(tuple)

    长度固定不变;存放多种类型元素。

    元组类似 C 语言匿名结构体。

     fn main() {
         let tup: (i32, f32, char) = (500, 6.4, '🦀');
         println!("{}, {}, {}", tup.0, tup.1, tup.2);
     
         // 解构
         let (x, y, _) = tup;
         println!("{}, {}", x, y);
     }
     
     /*
     500, 6.4, 🦀
     500, 6.4
     */

    数组(array)

    长度固定不变;元素类型必须相同。

    数据存放在栈(stack)上。

     fn main() {
         // 声明长度为 3 的 u32 数组
         let arr1: [u32; 3] = [11, 33, 55];
         // 指定长度为 3 初始值为 666 的数组
         // 等效于 let arr2 = [666, 666, 666];
         let arr2 = [666; 3];
     
         // 访问数组越界时编译会通过但运行时会 panic
         let mut index = String::new();
         std::io::stdin().read_line(&mut index).expect("Failed to read line");
         let index = index.trim().parse::<usize>().expect("Not a number!");
     
         println!("{}, {}", arr1[0], arr1[index]);
         println!("{:?}", arr2);
     }
     
     // input 1
     /*
     11, 33
     [666, 666, 666]
     */



    变量常量

    常量与不可变变量的区别:

    • 常量名的命名规则可变量的命名规则一样,但常量名一般都是大写字母

    • 定义常量时必须指定数据类型,而定义变量时数据类型可以省略。

    • 常量可以在任何作用域内声明,包括全局作用域。

    • 常量一旦定义就永远不可变更,不能重新赋值和重新定义

    • 常量只能被赋值为常量表达式/数学表达式,不能是函数返回值或者其它只有在运行时才能确定的值。

    • 常量只是一个符号,会在编译时替换为具体的值,类似于 C 语言中的 #define 定义的符号。

     fn main() {
         // 通过 let 声明变量默认是不可变的(immutable)
         // 类似于 Java 中 使用 final 关键字修饰变量
         let msg = "hello";
         // 再次赋值将出现 cannot assign twice to immutable variable 错误
         // msg = "world";
     
         // 通过 mut 关键字声明的变量可以被修改
         let mut num = 16;
         num *= 2;
     
         // shadowing 使用相同的名字声明新的变量
         // let 重新声明同名变量会砍掉之前的变量
         // 如果需要之前变量值则会先取到值再重新分配地址
         // 可以通过查看内存地址查看变化
         let count = 32;
         println!("old pointer is {:p}", &count);
         let count = 32 + count;
         println!("new pointer is {:p}", &count);
     
         // 常量定义时必须指定数据类型
         const NUMBER: u64 = 99;
         // 常量再次赋值或定义将会抛出错误
         // const NUMBER:usize = msg.len();
     
         println!("msg: {}, num: {}, count: {}, NUMBER: {}", msg, num, count, NUMBER);
     }
     
     /*
     old pointer is 0x7fff6a496274
     new pointer is 0x7fff6a4962cc
     msg: hello, num: 32, count: 64, Number: 99
     */


    函数

    函数名使用 snake case 命名规范;

    参数列表需要声明参数类型;

    函数体由一系列语句组成,可选的由一个表达式结束;

    Rust 一个基于表达式的语言,表达式会计算产生一个值;

    语句是执行一些动作的指令,没有返回值;

    在 -> 后声明返回值类型,但不可为返回值命名;

    函数默认使用最后一个表达式作为返回值;

     fn main() {
         let _x = {
             let y = 1;
             let _ = y + 1; // 有 ; 为语句,没有返回值
                            // 返回值为空元组 ()
         };
     
         let x = {
             let y = 1;
             y + 1 // 没有 ; 为表达式
                   // 返回值为 2
         };
     
         // if 为表达式
         let mut y = if x > 1 { 2 } else { 1 };
     
         println!("{}, {}", x, y);
     
         // loop 为表达式
         let z = loop {
             y += 1;
             if y > 10 {
                 break y * 2;
             }
         };
     
         println!("{}, {}", y, z);
     
         // while 不是表达式
         while y < 20 {
             y += 1;
             if y == 15 {
                 continue;
             } else if y == 18 {
                 break;
             }
         }
     
         println!("{}", y);
     
         let a = [4, 5, 6];
         for v in a.iter() {
             // v 是一个引用,此处类型为 &i32
             println!("{}", v);
         }
         // enumerate() 记录下标和值
         for (i, v) in a.iter().enumerate() {
             println!("a[{}]: {}", i, v);
         }
         // 表示 [-3, -1) 间的 2 个元素,如果 -1..-3 则为 0 个元素
         // rev() 为反转
         for v in (-3..-1).rev() {
             println!("{}", v);
         }
     
         println!("{:?}", handler(1, 2));
     }
     
     fn handler(x: i32, y: i32) -> (bool, i32) {
         (x == y, x + y)
     }
     
     /*
     2, 2
     11, 22
     18
     4
     5
     6
     a[0]: 4
     a[1]: 5
     a[2]: 6
     -2
     -3
     (false, 3)
     */


    所有权(ownership)

    Stack 与 Heap 比较:

    • 存储在 Stack 上的数据必须拥有已知的固定大小,编译时大小未知或运行时大小可能发生改变的数据存储在 Heap 上;

    • Stack 按先进后出(LIFO)顺序读写数据,读写速度快,Heap 写数据时需要先申请一定空间(内存分配),读写速度慢。


    所有权解决的问题(管理 Heap 数据):

    • 跟踪代码哪些部分正在使用 Heap 的哪些数据;

    • 最小化 Heap 上重复数据量;

    • 清理 Heap 上未使用的数据以避免空间不足。


    所有权规则:

    • 每个值都有一个变量,这个变量是该值的所有者;

    • 每个值同时只能有一个所有者;

    • 当所有者超出作用域(scope)时,该值将被删除。


    内存和分配:对于某个值,当其离开作用范围时,会自动调用 drop 函数将内存返还给系统。


    变量和数据交互的方式:

    移动(Move)

    • 对于存储在 Stack 上的数据

       {
           let x = 1;
           let y = x; // 复制一份 x 字面值副本存储在栈上
           // x, y 在此都可以使用
       }
       // x, y 离开作用域在此都被释放
    • 对于存储在 Heap 上的数据

       {
           let s1 = String::from("hello"); // 在堆上分配一块内存存储数据
           let s2 = s1;
           // 在此 s1 被释放,s2 仍可以使用
       }
       // s2 离开作用域在此被释放

      将值 hello 绑定给 s1 的 String 在内存中的表现形式:

      将值 hello 绑定给 s1 的 String 在内存中的表现形式

      当我们将 s1 赋值给 s2,String 的数据被复制这意味着:

      为此 Rust 将 s1 赋值给 s2 同时使 s1 不再有效,这个操作被称为 移动move),因此不需要在 s1 离开作用域后清理任何东西。s1 无效之后的内存表现:

      • 浅拷贝shallow copy):从栈上拷贝了它的指针、长度和容量,而并没有复制指针指向的堆上数据。当 s2 和 s1 离开作用域时都会尝试释放相同的内存,会二次释放double free)相同内存导致内存污染,它可能会导致潜在的安全漏洞。

      • 深拷贝deep copy):从堆上复制一份数据副本,并在栈上存储其指针、长度和容量。如果堆上数据比较大的时候会对运行时性能造成非常大的影响。

    s1 无效之后的内存表现

    克隆(Clone)

    • 对于存储在 Stack 上的数据

      在编译时已知大小的类型被整个存储在栈上,所以拷贝其实际的值是快速的。

      如果一个数据存储在栈上的类型拥有 Copy trait 的特殊注解,可以实现一个旧的变量在将其赋值给其他变量后仍然可用。

      对于自身或其任何部分实现了 Drop trait 的类型不能使用 Copy trait,否则将会出现一个编译时错误。

      拥有 Copy trait 的类型:简单标量及其全由简单标量的组合类型如 (i32, i32)


    • 对于存储在 Heap 上的数据

     {
         let s1 = String::from("hello"); // 在堆上分配一块内存存储数据
         let s2 = s1.clone(); // 深拷贝
         // s1, s2 在此都可以使用
     }
     // s1, s2 离开作用域在此都被释放

    s2 深拷贝 s1 堆上的数据在内存中的表现形式:

    s2 深拷贝 s1 堆上的数据在内存中的表现形式

    引用

     fn main() {
         let s1 = String::from("hello");
         let len = get_len(&s1);
         println!("str: {}, len: {}", s1, len);
     }
     
     fn get_len(s: &String) -> usize {
         // 使用其值但不获取其所有权
         s.len()
     }
     
     // str: hello, len: 5

    &String s 指向 String s1

    借用

     fn main() {
         let mut s1 = String::from("hello");
         // 不可变引用可以创建多个
         let r0 = &s1;
         let r1 = &s1;
         println!("{}, {}", r0.len(), r1.len());
         // 不可变引用和可变引用不能同时使用,即 r0, r1 不能在 r2 之后使用
     
         // 可变引用限制:在特定作用域中的特定数据只能有一个可变引用,用以避免数据竞争(data race)
         {
             let _r1 = &mut s1;
         } // _r1 在这里离开了作用域可以创建新的可变引用
         let r2 = &mut s1;
         // r2 的作用域内只能有一个 s1 的可变引用
         // let r3 = &mut s1;
     
         change(r2);
         println!("r2: {}", r2);
     
         println!("s1: {}", s1);
     }
     
     // 获取引用作为函数参数称为借用(borrowing)
     fn change(s: &mut String) {
         // s 借用 s1 的所有权并改变 s1 的值
         s.push_str(", world");
     } // 调用完成后 s 归还所有权
     
     /*
     5, 5
     r2: hello, world
     s1: hello, world
     */

    悬垂引用(Dangling References)


    切片


    结构体

     #[derive(Debug)]
     struct Rectangle {
         width: u32,
         height: u32,
     }
     
     impl Rectangle {
         fn create(width: u32, height: u32) -> Rectangle {
             Rectangle { width, height }
         }
     }
     
     fn main() {
         let rect = Rectangle::create(30, 50);
         println!("{:?}", rect);
     }

    单元结构体


    枚举与模式匹配

    enum

     

    match

     fn main() {
         let x = 4;
         match x {
             1 => {
                 println!("one");
             },
             2 => {
                 println!("two");
             },
             5 => {
                 println!("five");
             },
             _ => println!("other")
         }
     }

    if let

     


    项目管理

    Rust 中有三个重要的组织概念:箱、包、模块。

    箱(Crate) "箱"是二进制程序文件或者库文件,存在于"包"中。

    "箱"是树状结构的,它的树根是编译器开始运行时编译的源文件所编译的程序。

    注意:"二进制程序文件"不一定是"二进制可执行文件",只能确定是是包含目标机器语言的文件,文件格式随编译环境的不同而不同。

    包(Package) 当我们使用 Cargo 执行 new 命令创建 Rust 工程时,工程目录下会建立一个 Cargo.toml 文件。工程的实质就是一个包,包必须由一个 Cargo.toml 文件来管理,该文件描述了包的基本信息以及依赖项。

    一个包最多包含一个库"箱",可以包含任意数量的二进制"箱",但是至少包含一个"箱"(不管是库还是二进制"箱")。

    当使用 cargo new 命令创建完包之后,src 目录下会生成一个 main.rs 源文件,Cargo 默认这个文件为二进制箱的根,编译之后的二进制箱将与包名相同。

    模块(Module) 对于一个软件工程来说,我们往往按照所使用的编程语言的组织规范来进行组织,组织模块的主要结构往往是树。Java 组织功能模块的主要单位是类,而 JavaScript 组织模块的主要方式是 function。

    这些先进的语言的组织单位可以层层包含,就像文件系统的目录结构一样。Rust 中的组织单位是模块(Module)。

    Cargo

    镜像源配置 ~/.cargo/config

     [source.crates-io]
     registry = "https://github.com/rust-lang/crates.io-index"
     
     replace-with = 'tuna'
     [source.tuna]
     registry = "https://mirrors.tuna.tsinghua.edu.cn/git/crates.io-index.git"
     
     #replace-with = 'ustc'
     #[source.ustc]
     #registry = "git://mirrors.ustc.edu.cn/crates.io-index"
     
     [net]
     git-fetch-with-cli = true

    Blocking waiting for file lock on package cache

     rm ~/.cargo/.package-cache

    Crate


    集合

    Vector

     

    String

    &str 字符串面量

    字符串由标准库 std:str 提供,字符串面量在编译时就知道值的字符串类型。

    String 对象

    字符串对象是是一个长度可变的集合,它是可变的而且使用 UTF-8 作为底层数据编码格式,字符串对象在 heap 中分配,在运行的时候可以对其进行操作。

     fn main() {
         // 默认类型为 &str
         // 字符串字面量模式是静态的,这意味着字符串字面量从创建时开始会一直保存到程序结束
         let city = "上海";
         // 字符串面量默认就是静态的,也可以通过 static 关键字显式声明
         let url: &'static str = "http://example.com";
         // 将字符串面量转成 Sting 对象
         let host = url.to_string().replace("http://", "");
         println!("city: {1}, url: {0}, host: {host}", url, city, host = host);
     
         // 字符串对象
         let mut phone = String::new();
         phone.push_str("Apple iPhone");
         let iphone = String::from(phone);
         let model = String::from("XS");
         println!("{} {}", iphone, model);
     }
     
     /*
     city: 上海, url: http://example.com, host: example.com
     Apple iPhone XS
     */

    HashMap

     


    错误处理

     


    泛型


    trait


    生命周期


    测试


    智能指针

    Box<T>

    Deref trait

    Rc<T>

    RefCall<T>


    并发

    线程

    消息传递

    共享状态

    Sync/Send


    面向对象


    Unsafe


    宏(macro)



     use std::io;
     use std::cmp::Ordering;
     use rand::{thread_rng, Rng};
     
     fn main() {
         // [1, 101) 随机数
         let rand_num = thread_rng().gen_range(1..101);
         println!("{}", rand_num);
     
         // let mut str = String::new();
         // std::io::stdin().read_line(&mut str).expect("Failed to read line");
         // println!("{}", str);
     
         loop {
             let mut input = String::new();
             match io::stdin().read_line(&mut input) {
                 Ok(n) => {
                     println!("{} bytes read, {}", n, input.trim());
     
                     // parse() 可返回的类型比较多需要明确指定类型
                     let input:u32 = match input.trim().parse() {
                         Ok(num) => num,
                         Err(_) =>  {
                             println!("Please type a number!");
                             continue
                         },
                     };
     
                     // rand_num 类型会根据 input 类型来自动推导为 u32
                     match input.cmp(&rand_num) {
                         Ordering::Less => println!("Too small!"),
                         Ordering::Greater => println!("Too big!"),
                         Ordering::Equal => {
                             println!("equal");
                             break;
                         }
                         // _ => println!("equal")
                     }
                 }
     
                 Err(error) => println!("error: {}", error),
             }
         }
     }



    本文标题:RUST 入坑笔记
    本文链接:https://www.lanseyujie.com/post/rust-learning-notes.html
    版权声明:本文使用「署名-非商业性使用-相同方式共享」创作共享协议,转载或使用请遵守署名协议。
    点赞 0 分享 0