Rust杂七杂八整理

美团点评送外卖
美团点评送外卖   编辑于 2018-11-04 16:51
阅读量: 111

开始发布系列文章,写一个自己的编程语言,也是最近无聊,用Rust来实现,当然用java C/CPP go 之类的实现简直无压力,so,我选择了rust,反正以后的工作也是跟这个语言过不去,那么就入个门写个解释器玩玩。最后一学期了,本学期天天在学校,主要造轮子,兼职刷算法,QQ627459763 随时可+++ 

 

Rust枚举

Rust这个语言的张力很好,很大一部分原因就是很多时候类型系统并没有特别的重要,同时创造新类型的能力也是十分高的,就这个枚举就可以一堆表示和定义的方法。

写一个类似C语言的枚举

pub enum Person{
   Student,
   Teacher,
   Graduate,
}

很普通的就能写一个枚举出来,一般在java内可以用switch 来选择枚举选择对应的操作。Rust同样

pub fn judge_person_type(p:Person){
    match p {
      Person::Student => println!("I'am a student"),
      Person::Teacher => println!("I'am a teacher"),
      Person::Graduate => println!("Go Home"),
    }
}

当然很easy 

其实最好用的是枚举可以放数据,也就是我们可以放入一些数据

pub enum IpKind{
   V4(i32,i32,i32,i32),
   V6(String)
}

可以放进去想要的东西,然后还能在match里面拿出来,这样就实现了使用枚举的时候随时拿出创建时候的数据,比java整一堆getter和setter舒服多了,这里直接diss java ,该淘汰了,所有的开发kotlin都可以胜任,并且还带来很多新的语法特性,Scala还是算了,智商不够就不要挑战了,用啥java。

 

 

Option<T>

估计是写其他的语言写多了,Rust不支持Null或者None 或者Nptr之类的概念,也就是没有这个叫做什么都没有的概念,本来在语言范畴这个就是一个很模糊不清的概念,so我觉得Rust为了安全造了个Option类型也是很正确的。Option的定义在 std::option::Option<T>中定义了一把。

为什么说Null是一个很玄幻的东西呢,举个例子来说,在数据库中一个人的学号为null,那么这个null表达的意思是啥?这个人没有学号?当然也可能这个人刚入学,当然还有可能被开除了,学号清空了,当然都是有可能的,所以一个Null被解读了三种语义,So这个东西有啥用,干脆不用,所以很多时候在工程中都是用常量来替代Null。

Ownership & Borrow 

其实出现这两个奇怪的概念,主要是因为Rust并没有显式的调用如c++的delete和c的free,所有的清理内存的工作都是编译器帮忙做好了,可以认为在生成的中间代码部分已经帮我们填充好了所谓的delte行为。

官方教程上面为了方便其他语言的快速切换,首先介绍了一个所谓的声明周期的概念。还有什么栈上和堆上分配的概念。

fn main(){
   let x = 1;
   let str = "hello";
   let s = String::from("Hello");
   let v = vec![1,2,3,4,5];
}

如同上面的代码一样,x是基本的i32类型那么这个数据其实是在栈上分配的,当main函数推出的时候,自然栈也会被清空,那么String和Vec的空间其实给栈上的只是一个指向堆的指针,真实的数据都是被放在了堆里面。

我们可以认为栈上的数据其实是可以被任意使用的,因为在一个线程内,他的线程栈不可能被其他的线程正常访问,有的库直接用汇编去hook栈来获取其他的栈数据,而堆就不一样了,堆是所有线程共享的空间,那么谁都可以访问到,为了保持安全的内存语义,Rust就搞出了ownership和borrow的概念。

在代码空间中,我们看到的s 这个变量是Hello字符串在堆存储空间的指针,也就是一个简单的拥有关系,同一时间只能有一个变量拥有一块内存资源,而且只在当前块作用域有效,这里最难理解的原因是,如果出现了调用一个函数,并且以s作为参数传入的时候,这个时候其他的语言比如java直接吧引用传进去,CPP直接赋值传入,那么如果有多个线程运行的时候,一起调用的时候,就有意思了,这个s会被传很多次,也会出现很多个s的指针,或者引用,那么s所指向的内存空间并不是线程安全的,java这些语言因为有access内存语义,所以可以保证从用户空间access到堆空间的时候可以有一个状态检查,也就是所谓的锁检查,而Rust就只让在一个作用域内一个所有者只能有一个引用。

作用域的坑

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}

上面的函数并不会执行,比较奇妙吧,感觉没有问题但是不会执行。

Rust认为,当一个变量被显示的传递,不实用ref传递即没有加入&符号或者ref关键字的时候,这个时候,变量的所有权被一移动到了一个新的块中,一个块{} 被称作一个作用域,那么这个作用域的工作不仅仅需要变换一个变量的所有权,同时也是需要销毁变量的,为什么呢?因为Rust也是按值传递,传递的时候传递的是指针,那么如果传完了指针,然后忘记了销毁指针,如CPP中的没有调用delete,那么内存泄漏开始发生了,当然java会有自动的GC,但是Rust设置了这样的一个规定传入的是指针,那么必须要在这个作用域内进行销毁。

函数和返回

Rust 是一个并发安全的语言,其实他的并发安全完全是保证一个函数的返回值是一个新的内存块,而不是返回一个引用,也就是不能在一个函数内创建了一个堆内存对象然后返回他的引用,这个问题理解起来确实比较恶心。

首先我们需要理解为什么要保证不可变,如果可变会发生什么问题,为什么传递引用有的时候是一个很坑的事情。

我们假设一个场景,当两个线程并发的对一个堆内存容器进行写入的时候,会发生的问题就是瞎鸡儿乱写的问题,因为index并不能随时的被保证正好是下一个位置,因为SMA模型的关系,因为当前的值是在寄存器上的,而并不是在主存,也就是内存,所以为什么又volatile 语义,就是为了保证每次都从内存取,这样的话只能保证值是可见的,但是并不能保证一个顺序的语义,所以自然就出现了生命周期的问题。

pub fn get_ref() -> &Vec{
   let mut v = vec![1,2,3,4,5];
   &v
}

这个函数绝对会报错,原因很简单,当前作用域,也就是这个函数的大括号内是有效的,但是出了以后就会被销毁,那么自然引用也就失效了,rust为了保证这个悬垂的问题,所以不让传出一个引用,当然如果有传入的过程当然没问题。所以只要返回一个不可变的Vec 对象就可以啦

首先要明确一个生命周期的概念,生命周期这个东西确实是个很难理解的问题,理解这个问题之前我们需要首先明白为什么需要销毁对象,在没有GC的CPP 或者Rust是怎么做的,CPP就是显示的delete,但是很容易出现一些悬垂的问题。Rust并不能在Runtime时间避免悬垂问题,但是可以在编译阶段避免悬垂。

首先我们需要明白的是,基础的数据结构什么问题都没有,因为他们完全是按值传递,并且在对形参进行赋值的时候,完全是copy的模式,主要的问题在于引用这种类型,也就是我们在堆上分配的对象需要进行引用的时候会出现问题,相当于指针,但不是指针,因为指针这个东西,是地址,而引用根本不是地址是栈上的一个内存块,只不过里面有个字段是保存了真正的堆地址而已。

那么Rust的释放内存的操作确实比较难理解。

mutable & inmutable Object

关于可变和不可变对象,真的理解为什么会有这两种语义,首先要明白一个生产者消费者模型之类的并发模式,才能真的明白为啥有这两种语义。主要是因为内存的访问控制的,为什么内存需要有两种语义呢,一种是move 一种是 reference 语义,两种语义有着不同的含义保持了内存的完整。

应该是Rust最完整的内存语义整理

为了避免一些不必要的问题,避免显示的释放内存,和C/C++一样,同时又要保证一个可靠的内存回收机制,避免GC的延迟问题,所以Rust设计了一个所有权系统,和租借管理器,来在编译器检验是否符合某些规则。

真的是所有权了

这个绝对是Rust里面最好的东西,当然也是最恶心的问题,写了很久的GC语言,到了手工释放的时候开始我是拒绝的,那么首先我们需要考虑以下几个并发问题,也是GC和显示回收都需要面对的问题。

  1. 共享内存,导致的寄存器隐藏,因为一个CPU核在执行的时候是在本地的rgister上进行的数据计算
  2. 空指针,回收的大坑
  3. 悬垂指针

首先先说一下悬垂的问题,也是为什么C++ 既要保持指针还要保持引用,这个十分尴尬的问题,因为C++的引用你delete 的时候只是把运行栈上的引用内存删除了,但是并没有真的将引用所引用的内存区域释放掉,但是释放指针的时候可以将内存区域释放掉,所以C++ 开发的时候一般都是传递引用,但是这样同样引入了一个问题,当应用处理完了以后,那么怎么释放真正的内存呢,不释放自然就泄漏了,所以后面又加入了RAII语义,和几种智能指针,光在内存管理上C++ 已经给出了好几种操作了,自然这个语言就十分的难了。

对C++ 感兴趣可以自己去研究C++ 的智能指针,我们直接开始研究Rust的所有权系统。

C++ 带来的问题是到处传递引用传递完了以后,并不能真的释放掉内存,并且那个内存可能从此不可用了,也就是内存泄漏,为了保持一个良好的内存管理状态,并且方便,所以引入了所有权系统。

我们只需要知道一个{} 内部的就是一个作用域就好了,当然特殊的是 if else 的两个 {} 才可以称作一个作用域,当一个引用离开作用域的时候,或者一个变量离开的时候,他应该符合RAII语义,应该立即被释放。

看这样的一个Rust代码,我们的padovan属于一个作用域的变量,如果我们又GC,那么这个时候padovan就会被标记等待回收,然后GC把内存释放掉,但是Rust直接在作用域结束的时候释放了,其实就是一个立即和稍后的两个释放语义而已。

什么是所有权,通过vec宏创建的vector是在堆内存上进行的分配,所以需要一个好像指针一样的东西指向他他才能用,同样才能被释放,然后Rust引入的概念就是padovan变量拥有vec宏申请的内存空间,这就是所有权的概念,也就是变量拥有内存,不仅仅是一个指向的语义了。

我想更清楚的描述应该是,如果使用let 声明了一个变量之后,那么这个变量如果是vector这类的复合数据结构,也就是会产生递归的数据结构,那么这个变量,在当前的作用域内都应该是有效的,除非发生所有权的移动和从作用域内扩散出去。也就是我下面说的所有权移动的问题。

既然我们很清楚的明白了所有权的概念,那么移动的概念也应该不难,所有权就是对栈上变量来说的,栈上变量拥有一个堆内存空间,那么这个变量是这个堆上空间的所有者,然后如果当我们使用了

类似 a = b 的语法的时候,a 就变成了这个堆内存的新的拥有者。这个时候rust会把b变成一个未初始化的内存空间。

我们看到C++ 实现这个所谓的指向语义的时候,其实是维护了一个深拷贝的内存语义,那么后面我们会发现这样的操作对C++来说确实是个坑,因为这么搞会让内存一直在分配和撤销分配的过程,很麻烦。

但是Rust其实是这么干的,当触发了 t = s 的时候s就不能用了,也就是这个时候s 不是任何的堆内存的所有权。

 

收藏 转发 评论