锈。通过迭代器借用检查器

哈Ha!

我已经学习了大约一年,在空闲时间,我在书本上写作。我喜欢它的作者如何通过借用的概念解决内存管理问题并免除垃圾收集器的方法。在本文中,我将通过迭代器来实现这个想法。

最近,scala是我的主要语言,因此将与之进行比较,但是其中没有很多比较,而且一切都是直观的,没有魔术:)

这篇文章是为那些听过生锈但又不做细节的人设计的。从这里从这里


拍摄的照片

前言


在jvm语言中,习惯上隐藏带有链接的工作,也就是说,我们几乎总是使用引用数据类型,因此我们决定隐藏“&”号(&)。

在rasta中,有明确的链接,例如到整数-`&i32`,可以通过`*`取消引用链接,也可以有指向该链接的链接,然后需要将其取消引用两次**。

迭代器


编写代码时,通常需要按条件(谓词)过滤集合。在岩石中,即使元素也像这样:

    val vec = Vector(1,2,3,4)
    val result = vec.filter(e => e % 2 == 0)

让我们看一下排序:

  private[scala] def filterImpl(p: A => Boolean, isFlipped: Boolean): Repr = {
    val b = newBuilder
    for (x <- this)
      if (p(x) != isFlipped) b += x

    b.result
  }

无需深入研究newBuilder的细节,很明显,正在创建一个新的集合,我们对旧集合进行迭代,如果谓词返回true,则添加一个元素。尽管该集合是新集合,但实际上它的元素是第一个集合中元素的链接,如果突然之间这些元素是可变的,那么更改它们将是两个集合所共有的。

现在,让我们尝试在rast中执行相同的操作。我将立即给出一个工作示例,然后再考虑差异。

    let v: Vec<i32> = vec![1, 2, 3, 4];
    let result: Vec<&i32> = v.iter().filter(|e| **e % 2 == 0).collect();

哇,哇?双指针解引用?只是为了过滤向量?困难:(但是有这样做的原因。

让我们指出一下这段代码与原来的代码有何不同:

  1. 在向量上显式获得迭代器(`iter()`)
  2. 在谓词函数中,由于某种原因,我们两次取消了指针的引用
  3. 调用`collect()`
  4. 它也导致引用类型为Vec <&i32>的向量,而不是普通的整数

借阅检查器


为什么要在集合上显式调用`iter()`?对于任何摇滚乐手来说,很明显,如果调用`.filter(...)`,则需要遍历集合。为什么要在一个光栅中显式地写出可以隐式执行的操作?因为有三个不同的迭代器!



弄清楚为什么要三个?需要触摸借用(借用,借用) 检查器 'a。正是由于这个原因,在没有GC且没有显式内存分配/释放的情况下,光栅也可以正常工作。

为什么需要它?

  1. 为避免多个指针指向同一存储区的情况,您可以对其进行更改。那是比赛条件。
  2. 为了不多次取消分配相同的内存。

如何实现的?

由于所有权的概念。

通常,所有权的概念很简单-只有一个人可以拥有某些东西(甚至是直觉)。

所有者可能会更改,但他总是一个人。当我们写“ let x:i32 = 25”时,这意味着有一个32位int的内存分配,并且某个`x`拥有它。所有权的概念仅存在于编译器的思想中,即借用检查器中。当所有者(在这种情况下)x离开范围(超出范围)时,将清除其拥有的内存。

这是借位检查器不会遗漏的代码:


struct X; // 

fn test_borrow_checker () -> X {
    let first = X; //  
    let second = first; //  
    let third = first; //   ,   first   
//    value used here after move

    return third;
}

“结构X”类似于“案例类X()”-一种无边界的结构。

我认为,这种行为对每个人来说都是违反直觉的。我不知道其他语言无法两次使用相同的``变量''。感到这一刻很重要。首先根本不是X的引用,它是X的所有者更改所有者后,我们会杀死前一个所有者,借阅检查器将不允许其使用。

为什么需要创建自己的结构,为什么不使用正整数?
— (`struct X`), , , integer. , , :


fn test_borrow_checker () -> i32 {
    let first = 32;
    let second = first; 
    let third = first; 

    return third;
}

, borrow checker, , . Copy, . `i32` second , ( ), - third . X Copy, .

. , , «» . Clone, , . copy clone.

回到迭代器。其中的“捕获”概念是IntoIter他“吞下”了该系列,并拥有其元素。在代码中,这种想法将体现为:


let coll_1 = vec![1,2,3];
let coll_2: Vec<i32> = coll_1.into_iter().collect();
//coll_1 doesn't exists anymore

通过在coll_1上调用`into_iter()`,我们将其“变成”一个迭代器,吸收了所有元素,如前面的示例中,“ second`首先吸收”。之后,对coll_1的任何调用都会在编译过程中受到借阅检查器的惩罚。然后我们使用`collect`函数收集这些元素,创建一个新的向量。需要使用`collect`函数来从迭代器中收集集合,为此,您必须明确指定我们要收集的对象的类型。因此,coll_2清楚地指示类型。

好的,通常来说,上面的描述对于一种编程语言来说已经足够了,但是每次我们要传输它们时,复制/克隆数据结构都不会非常有效,并且您还需要能够进行一些更改。因此,我们去看看指针。

指针


我们发现,所有者只能是一个。但是您可以有任意数量的链接。


#[derive(Debug)]
struct Y; // 

fn test_borrow_checker() -> Y {
    let first = Y; //  
    let second: &Y = &first; //   ,     
    let third = &first; //    

// 
    println!("{:?}", second);
    println!("{:?}", third);

    return first;
}


该代码已经有效,因为所有者仍然是一个。仅在编译阶段检查所有所有权逻辑,而不会影响内存分配/移动。此外,您可以看到秒的类型已更改为`&Y`!也就是说,所有权和链接的语义反映在类型中,这使您可以在编译期间进行检查,例如,是否缺少竞争条件。

如何在编译时防止竞争状况?

通过设置可变链接的数量限制!

某一时刻的可变链接可以是一个且只有一个(不可变)。也就是说,一个或几个不可变,或一个可变。代码如下:


// 
struct X {
    x: i32,
} 

fn test_borrow_checker() -> X {
    let mut first = X { x: 20 }; //  
    let second: &mut X = &mut first; //   
    let third: &mut X = &mut first; //    .        `second`        - .
//    second.x = 33;  //    ,             ,    
    third.x = 33;

    return first;
}

让我们来看一下前面相对示例中的更改。首先,我们在结构中添加了一个字段,以便进行某些更改,因为我们需要可变性。其次,mut出现在变量的声明中let mut first = ...,这是编译器关于可变性的标记,例如val和var。第三,所有链接的类型都从“&X”更改为“&mut X”(当然看起来很可怕。这没有生命周期……),现在我们可以更改链接存储的值。

但是我说我们不能创建几个可变链接,他们说借阅检查器不会给出这个链接,但是我自己创建了两个!是的,但是那里的检查非常棘手,这就是为什么有时编译器发誓有时并不明显的原因。他正在努力确保您的程序能够编译,并且如果绝对没有选项可以满足规则,那么会出错,并且可能不是您正在等待的错误,而是违反了他的最后尝试的,这对于初学者来说是最绝望的,也不是显而易见的: )例如,尽管您未在任何地方调用任何副本,但系统仍告知您该结构未实现Copy trait。

在这种情况下,由于我们仅使用一个可变链接,因此允许同时存在两个可变链接,也就是说,第二个可变链接可以扔掉,并且什么都不会改变。也'秒'可用于高达创建一个“第三”,然后一切都会好的。但是,如果您取消注释“ second.x = 33;”,则会发现两个可变链接同时存在,并且您无论如何也不能离开这里-编译时错误。

迭代器


因此,我们有三种传输类型:

  1. 吸收,借用,搬家
  2. 链接
  3. 可变链接

每种类型都需要有自己的迭代器。

  1. IntoIter吸收原始集合中的对象
  2. Iter在对象链接上运行
  3. IterMut在可变对象引用上运行

问题出现了-什么时候使用。没有灵丹妙药-您需要练习,阅读别人的代码,文章。我将举一个例子说明这个想法。

假设有一所学校,其中有一个班级,并且班上有学生。


#[derive(PartialEq, Eq)]
enum Sex {
    Male,
    Female
}

struct Scholar {
    name: String,
    age: i32,
    sex: Sex
}

let scholars: Vec<Scholar> = ...;

例如,我们通过查询数据库来获取小学生的向量。接下来,我需要计算班上女生的人数。如果我们通过“ into_iter()”“吞下”向量,则在计数之后,我们将无法再使用此集合来计数男孩:


fn bad_idea() {
    let scholars: Vec<Scholar> = Vec::new();
    let girls_c = scholars
        .into_iter()
        .filter(|s| (*s).sex == Sex::Female)
        .count();

    let boys_c = scholars 
        .into_iter()
        .filter(|s| (*s).sex == Sex::Male)
        .count();
}

在线计数男孩的行上会出现错误“移动后在此处使用的值”。显而易见,可变的迭代器对我们没有用。这就是为什么它只是`iter()`并使用双重链接的原因:


fn good_idea() {
    let scholars: Vec<Scholar> = Vec::new();
    let girls_c = scholars.iter().filter(|s| (**s).sex == Sex::Female).count();
    let boys_c = scholars.iter().filter(|s| (**s).sex == Sex::Male).count();
}

在这里,为了增加该国潜在的新兵人数,已经需要一个可变的迭代器:


fn very_good_idea() {
    let mut scholars: Vec<Scholar> = Vec::new();
    scholars.iter_mut().for_each(|s| (*s).sex = Sex::Male);
}

提出这个想法,我们可以使士兵脱离“家伙”,并展示“吸收”迭代器:


impl Scholar {
    fn to_soldier(self) -> Soldier {
        Soldier { forgotten_name: self.name, number: some_random_number_generator() }
    }
}

struct Soldier {
    forgotten_name: String,
    number: i32
}

fn good_bright_future() {
    let mut scholars: Vec<Scholar> = Vec::new();
    scholars.iter_mut().for_each(|s| (*s).sex = Sex::Male);
    let soldiers: Vec<Soldier> = scholars.into_iter().map(|s| s.to_soldier()).collect();
    //   scholars,    
}

在这个美妙的音符上,也许仅此而已。

最后一个问题仍然存在-过滤器中的链接的双重解引用从何而来。事实是谓词是一个引用参数的函数(以便不捕获它):


    fn filter<P>(self, predicate: P) -> Filter<Self, P> where
        Self: Sized, P: FnMut(&Self::Item) -> bool,

谓词是FnMut(大致来说是一个函数),它引用其(自身)项目并返回bool。由于我们已经有来自迭代器`.iter()`的链接,因此第二个出现在过滤器中。当被迭代器(`into_iter`)吸收时,对链接的双重取消引用变成了常规链接。

延续性


我没有写文章的丰富经验,所以我很乐意批评。
如果有兴趣,我可以继续。主题选项:

  • 内存释放的方式和时间
  • 链接寿命
  • 异步编程
  • 编写小型Web服务,甚至可以提供api

链接


  • 锈书
  • 由于所有权的概念,诸如链接列表之类的基本内容的实现不再是微不足道的。这里有几种方法来实现它们。

All Articles