LEEDOM

Apr 02, 2022

Rust程序设计

  • into()方法可以快速的将当前类型转换为目标参数类型;

基本类型

  • as操作符可以将bool转为整数类型,但是反过来转换不可以;
  • bool在内存中使用了一个字节作为其值;
  • as操作符可以将char转为整数类型,但是目的类型如果小于了32位,高位会被截断;反过来转换仅支持u8转为char;
  • Rust中的指针类型有三种:引用、Box(指向堆)、不安全指针(原始指针);
  • 胖指针(fat pointer):指除了包含指向引用地址的值,还有一些额外的信息,如向量(还包含了大小)、trait(还包含了其实现类的地址);

所有权

  • 对于向量的遍历,一般是使用引用来访问其数据的值,这样不会转移向量中元素的所有权;如下代码示例:

    1
    2
    3
    4
    5
    6
    let mut v = vec!["1".to_string(), "2".to_string()];
    for mut s in v { // 这里的for会将v中的元素的所有权转移到s中
    s.push('!');
    println!("{}", s);
    }
    println!("v's length {}", v.len()); // 报错

    上诉的代码执行会报错

    1
    2
    3
    4
    5
    6
    7
    8
    20  |     for mut s in v {
    | -
    | |
    | `v` moved due to this implicit call to `.into_iter()`
    | help: consider borrowing to avoid moving into the for loop: `&v`
    ...
    24 | println!("v's length {}", v.len());
    | ^^^^^^^ value borrowed here after move

    编译器提示的很清晰了,因为for循环中将v里的元素的所有权都进行了转移到了s中,因此v在for循环代码块之后变成了初始化的状态,这对Rust来说是不允许使用的,因此报错了;将代码修改如下

    1
    2
    3
    4
    5
    6
    7
    let mut v = vec!["1".to_string(), "2".to_string()];
    for mut s in &mut v {
    // *s.push('!');
    s.push('!');
    println!("{}", *s);
    }
    println!("v's length {}; {:?}", v.len(), v);

    注意注释的代码的写法是错误的,因为.操作符本身就隐式带有解引用,所以不需要再额外的写*去解引用

  • Option提供的*take()*方法,会将值置换为None并赋值给左值;

  • 机器整数、浮点型、char和bool类型都是Copy类型,会直接复制值而不会转移;默认情况下,struct和enum都不是Copy类型,但是,如果一个结构体中,仅当所有类型都是Copy类型,是可以通过添加属性来将该结构体标注为Copy类型(如果结构体中存在非Copy类型,即使加了属性也无效),如:

    1
    2
    #[derive(Copy, Clone)]
    struct Label { number: i32 }
  • 函数返回的值的生命周期,如果返回的是&str需要注明为&'static str',但是返回String的话不需要。因为String是一个新的对象,该对象的生命周期直接返回了。

引用

能力不足,怨不得书

​ ——Mark Miller

引用的篇章,都是对这个概念做出大量的解释,所以只有在练习中才能巩固对其的理解;

  • 生命周期的意义在于约束赋值或传参时,对结构体或者函数返回值的生命周期的控制;

  • 在存在**&self方法(非函数)中,Rust默认生命周期是&self**的生命周期;如下

    1
    2
    3
    4
    5
    impl StringTable {
    fn find_by_prefix(&self, prefix: &str) -> Option<&String> {
    ...
    }
    }

    该方法的完整版本应该是这样的fn find_by_prefix<'a, 'b>(&'a self, prefix: &'b str)-> Option<&'a String>

表达式

  • 循环可以通过生命周期标签来控制多层跳转,有点类似Kotlin 里的forEach@ 这种标签写法;

    1
    2
    3
    4
    5
    6
    7
    8
    ‘search:
    for room in apartment {
    for spot in room.hiding_spots {
    if spot.containsKey(keys) {
    break 'search;
    }
    }
    }
  • 关于泛型的方法调用注意:通常用于函数调用或者方法调用的语法不能用于泛型,如Vec<i32>::with_capacity(1000);这种是错误的,正确写法是Vec::<i32>::with_capacity(1000);,当前,还可以让编译器自己推断,从而省略泛型的写法Vec::with_capacity(3000);

  • Rust中的转换一般是显示的,除了解引用强制转换,有三种情况

    • &String 类型的值会被自动转换为&str;
    • &Vec类型的值会被自动转换为&[i32];
    • &Box类型的值会自动转换为&Chessboard;

错误处理

  • 处理特定业务类型的错误代码示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    loop {
    match compile_project() {
    Ok(()) => return Ok(()),
    Err(err) => {
    if let Some(msg) == err.downcast_ref::<MissSemicolonError>() {
    // xxxx
    ...
    continue;
    }
    }
    }
    }

包和模块

  • 没有声明pub的都是模块私有的;

结构体

  • 结构体中所有的变量都必须声明为pub,才能使用结构体表达式构建;否则只能使用对外暴露的API方法进行构建(如Vec::new(););
  • 结构体类型:命名字段结构体(struct Person { name: String })、类元组结构体(struct Bounds(usize, usize);)和类基元结构体(struct OneType;);
  • impl的方法实现中,如果第一个入参是self,而不是引用,则会在调用方法后转移所有权;

枚举与模式

  • 枚举的类型和结构体一样有三种;并且一个枚举定义中可以同时包含这三种类型的变体

  • 模式匹配中,会将结构体中的所有权转移到匹配的变量上,这样原结构体就会变成未初始化状态,因此需要使用ref关键字来获取其引用,示例如下:

    1
    2
    3
    4
    5
    6
    match account {
    Account { ref name, ref language, ..} => {
    ui.greet(name, language);
    ui.show_setting(&account);
    }
    }

    因为在模式匹配中使用ref关键字,所以name和language两个变量仅复制了account中这两个变量的值,而非转移了其所有权,所以在后面的代码仍然可以使用**&account**;

  • 匹配某个范围的值使用的是**…**;

  • 在模式匹配中添加if条件时,不能将变量的值进行转移,如Some(point) if self.distance_to(point) < 10 这个表达式中,后面的条件实际上转移了point的所有权,就会导致后面的分支判断无法使用point了,因此需要修改为借用,即Some(ref point) if self.distance_to(point) < 10;可修改的为ref mut

  • x @ pattern匹配给定的pattern,但是成功之后,不是基于匹配值的元素来创建变量,而是把匹配值整个转移或者复制到一个变量x中;

  • 两个概念的解释:可驳模式(refutable pattern)和不可驳模式(irrefutable pattern),类似Kotlin中的解构就是不可驳模式,其双向的值是确定清晰可转换的,如let (x, y) = point;// point = (3, 4),而match就是一种可驳模式,因为对于一个需要match的值来说,其结果可能是Ok(x)也可能是Err(err),他双向的匹配是一个大于的范围;

特型(Trait)

  • 特型目标,指的是使用trait声明的变量,其只能接受引用,不能直接接受变量,原因是本质上,trait是一个引用,它指向的是实现该trait的具体实现类;

  • 特型目标支持普通引用自动转换,即一个引用是某个trait的实现,在方法或者函数调用时,传入其引用可以自动转换为该trait类型;

  • 特型的impl实现方法中只能包含特型的实现,如果需要其他的实现,需要定义其他的impl块;

  • 扩展特型:指的是当前的特型仅为一个已有的类型添加新的方法;

  • 关联类型,声明迭代器的item的类型限制的写法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    fn dump<I>(iter: I) 
    where I : Iterator, I::Item:Debug
    {
    ...
    }

    // 或者
    fn dum<I>(iter: I)
    where I : Iterator<Item=String>
    {
    ...
    }
  • Default特型是有默认值的类型;

  • Add<Output=i32>这句的理解是,Add+操作符的trait定义,定义了两种数值类型的加法,其定义源码如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    pub trait Add<Rhs = Self> {
    /// The resulting type after applying the `+` operator.
    #[stable(feature = "rust1", since = "1.0.0")]
    type Output;

    /// Performs the `+` operation.
    ///
    /// # Example
    ///
    /// ```
    /// assert_eq!(12 + 1, 13);
    /// ```
    #[must_use]
    #[stable(feature = "rust1", since = "1.0.0")]
    fn add(self, rhs: Rhs) -> Self::Output;
    }

    可以看到是trait中定义的type类型,这个在rust中被称为关联类型,如迭代器的trait中定义的关联类型就是item,那这个的使用场景可以这么理解,如v[i]+s[i];假设这个是某个循环或者函数中的片段,v和s分别是两个数值类型的切片,且在当前的片段中,无法推断出v[i]和s[i]的值的具体类型(在泛型方法中,这个就很常见了),这样执行+就会有问题,比如v[i]是一个i32s[i]是一个f32,这样他们肯定无法执行+,所以这里会有问题,解决的方法就是对其值类型进行type限制,查看一个完整实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    fn dot<N>(v1: &[N], v2: &[N]) -> N
    where N: Add<Output=N> + Mul<Output=N> + Default + Copy
    {
    let mut total = N::default();
    for i in 0..v1.len() {
    total = total + v1[i] * v2[i];
    }
    total
    }

    这个例子中,泛型N如果仅仅是实现了Add特型的类型,就会存在很多类型,如i32和f32,都实现了,这样就没法保证v1[i]v2[i]的值是同一种类型,因此需要限定他们的Output的类型都是同一种;

实用特型

特型 简介
Drop 相当于*C++*中的解构函数,会在清除的时候被动调用
Sized 固定大小的Trait,大多数类型是固定大小的(不太理解这个)
Clone 复制一个独立的副本,简单实现只需要添加#[derive(Clone)]即可
Copy 基于编译器的语义,可以实现实体类型的复制(对象的复制需要定义的字段都能复制才行),逐位拷贝
Deref与DerefMut 实现这两个Trait可以自定义引用*和解引用.操作符上的自定义行为
Default 具备了默认值的Trait,如数值类型默认值是0,字符串默认值是空的
AsRef与AsMut 引用和可修改引用
Borrow与BorrowMut 借用和可修改借用
From与Into 类型转换特型
ToOwned 给定一个引用,产生其引用目标的所有型副本
  • drop()会在值被清除的时候,被隐式调用,该方法无法被显式调用;
  • drop()在面对需要使用unsafe资源进行释放时,是很有用的;
  • 实现了Drop,则不能实现Copy
  • CopyClone一般一起使用#[derive(Clone, Copy)];
  • Copy和Clone的区别;
  • 实现Copy需要考虑清楚,隐式复制的代价很大的
  • 自定义解引用在应对泛型时,需要注意解引用后的类型和解引用前的类型是否有泛型上的差别,否则需要强转,具体解释看Rust程序设计239页解释;
  • str和*[T]*类型都是非固定大小的,而&str这种引用是固定大小的;
  • Rust中闭包的性能是没有开销的,编译器会显示内联优化,减少函数和方法调用;
  • 对于任意类型的TU,如果T: AsRef<U>,则&T: AsRef<U>也成立;
  • Borrow的限制:只有当&T与它所借用的值具有相同的散列和比较特性时,一个类型才可以实现*Borrow*;
  • Clone一个*&T必须返回一个类型T*的值;

闭包

  • fn(&City)->bool是闭包的一种类型,仅接受函数;

  • Fn(&City)->bool是闭包的一种特型, 包括函数和闭包;

  • FnFnOnce的理解:

    • 如果一个闭包中,只有普通的代码,没有传值进行所有权的转移,则其应该是Fn特型的闭包;

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      let my_str3 = "hello3".to_string();
      let block3 = || {
      let a = 1;
      let b = 2;
      if a + b == my_str3.len() {
      println!("equal");
      } else {
      println!("not equal");
      }
      };
      call_twice(block3);
    • 如果一个闭包中,存在了所有权的转移,那么这个闭包仅能调用一次,其是FnOnce特型的闭包,如果对其调用多次,会报错,因为不符合所有权系统的规则(试想一个函数入参是所有权转移,然后会存在返回值,这个返回值的所有权和这个入参一致,如果调用了两次,就会把一个值的所有权赋值给两个变量,这是不符合所有权系统的规则的);

      1
      2
      3
      4
      let my_str = "hello".to_string();
      let block = || drop(my_str); // this block's trait is FnOnce,beacuse drop receive a var, not a ref
      block();
      block(); // error
    • 如果一个闭包中,仅存在借用,那么这个闭包还是Fn特型的闭包;

      1
      2
      3
      let my_str2 = "hello2".to_string();
      let block2 = || println!("this is {}", &my_str2); // this block's trait is Fn,println receive a ref;
      call_twice(block2);
    • 如果一个闭包中,即存在所有权转移,又存在借用,那这个还是FnOnce特型的闭包;

      1
      2
      3
      4
      5
      6
      let my_str4 = "hello2".to_string();
      let block_fn = || {
      println!("this is {} 2", &my_str4);
      drop(my_str4);
      };
      call_twice(block_fn); // error

    总结下来就是,FnOnce闭包是闭包设计中实现所有权的一个方式。需要注意的一点是,在调用FnOnce的闭包时,闭包本身也会被使用掉,这个设计和所有权是如出一辙的。

迭代器

  • 可迭代类型指的是实现了IntoIteratortrait的类型,可以通过into_iter()方法取得其迭代器;

  • 使用into_iter()方法时,需要加上借用,因为这个方法是返回的所有权,而iter()和iter_mut()方法返回的迭代器不需要加上借用;

  • 值传递的集合,在迭代时,消费者会取得其所有权,在迭代器迭代完成后,集合就会清空;

  • vec使用join()方法可以在每个item直接添加一个元素;

  • 对于消费的理解,["earth", "water", "air", "fire"].iter().map(|elt| println!("{}", elt));这句代执行后实际不会打印任何值,因为这句代码里没有产生消费,当在最后加上.next()方法时,会打印第一个值,因为仅消费了一个,这一点和Kotlin是有很大区别的,在Kotlin中调用map()方法就意味着已经开始产生消费了,这里可以这样修改:

    1
    2
    3
    4
    5
    6
    7
    let s = ["earth", "water", "air", "fire"].iter().map(|elt| {
    println!("map:{}", elt);
    elt
    });
    for item in s {
    println!("for:{}", item)
    }

    这里使用for.. in ,实际上是连续调用了next()方法来产生迭代值,不过这里可以猜猜打印的结果是什么。

    答案是**map:earth\nfor:earth\nmap:water\nfor:water….**,是的,最终调用next()时,会将map()的操作一起执行出来;

  • scan()方法,类似map(),区别是其传递给闭包的是可修改的值,并在接受到None值时,会提前终止迭代;

  • take()和take_while()使用方法上和Kotlin没有区别,都是获取指定数目和指定条件的数据;

  • skip()和skip_while()使用方法同Kotlin

  • peekable(),可预测的迭代器,当我们需要对迭代的下一项进行判断才决定消费时,非常有用;需要练习这个;

  • fuse()可以在第一次返回None后一直返回None;实例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    struct Flaky(bool);

    impl Iterator for Flaky {
    type Item = &'static str;
    fn next(&mut self) -> Option<Self::Item> {
    if self.0 {
    self.0 = false;
    Some("Totoal")
    } else {
    self.0 = true;
    None
    }
    }
    }

    #[test]
    fn test_fuse() {
    let mut flaky = Flaky(true);
    assert_eq!(flaky.next(), Some("Totoal"));
    assert_eq!(flaky.next(), None);
    assert_eq!(flaky.next(), Some("Totoal"));

    let mut not_flaky = Flaky(true).fuse();
    assert_eq!(not_flaky.next(), Some("Totoal"));
    assert_eq!(not_flaky.next(), None);
    assert_eq!(not_flaky.next(), None);
    }
  • by_ref()借用迭代器的可修改引用,因为迭代后还可以继续使用原迭代器,这个方法不会取得原迭代器的所有权;但是迭代后该消费的迭代还是会消费掉,也就是说调用了这个方法,你迭代后,还可以继续迭代,因为所有权还在,其他的迭代方法获取所有权,迭代后原迭代器就不能使用了;

  • cycle(),返回一个无休止重复的底层迭代器;

集合

标准集合

集合 说明
Vec 可增数组
VecDeque 双端队列
LinkedList 双向链表
BinaryHeap 最大堆
HashMap<K,V> where K : Eq + Hash 键-值散列表
BTreeMap<K,V> 有序键-值表
HashSet where T: Eq + Hash 散列表
BTreeSet where T:Ord 有序集
  • join()方法需要传入一个Separator类型的参数,其作用是将元素加入分割器后返回,而&T是实现了Separator的类型,因此[[1, 2], [3, 4]].join(&100)的结果是[1, 2, 100, 3, 4];注意机器类型无法使用join();未实现对应的trait
  • join()contact()是用于处理数组的数组;

并发

  • Rust中,move关键字会将当前的变量转移到子线程中,这种转移的开销很小;
  • Rust中,子线程的panic会被主线程当做一个错误进行处理,而不是一个panic;不过这个错误需要我们显示的使用Result::Err来进行处理;
OLDER > < NEWER