首页 > 其他 > 详细

一文学会Rust?

时间:2019-11-15 12:59:02      阅读:106      评论:0      收藏:0      [点我收藏+]

Rust是什么

Rust 是一个系统编程语言,它注重三个方面:安全,速度和并发性。

特征:

1.没有垃圾回收机制,没有运行时,效率超过c++,直逼c语言

2.内存安全,并发安全,没有空指针

3.详细的入门指南和及其丰富的生态 https://github.com/rust-lang/rust ,https://crates.io/

它是如何做到的?

编译时保证对内存生命周期的明确控制

让我们来谈谈Rust中最重要的概念:“所有权”,以及它对并发编程(对程序员来讲通常是非常困难的任务)的启发。

所有权
所有权是Rust的核心概念,也是其独特的功能之一。 “所有权”是指允许哪部分的代码修改内存。

让我们从一些C++代码开始理解这个概念:

int *foo(void)
{
    int i = 1234;
    return &i;
}
 
int bar(void)
{
    int *num = foo();
    return *num + 1;
}

foo函数在栈上分配了一个整型,然后保存给一个变量i,最后返回了这个变量i的引用。这里有一个问题:当函数返回时栈内存变成失效。意味着在函数add_one第二行,指针num指向了垃圾值,我们将无法得到想要的结果。虽然这个一个简单的例子,但是在C++的代码里会经常发生。当堆上的内存使用malloc(或new)分配,然后使用free(或delete)释放时,会出现类似的问题,但是您的代码会尝试使用指向该内存的指针执行某些操作。 更现代的C ++使用RAII和构造函数/析构函数,但它们无法完全避免“野指针”。

Rust就可以不出现这种情况。 我们试试吧:

fn foo() -> &int {
    let i = 1234;
    return &i;
}
 
fn bar() -> int {
    let num = foo();
    return *num + 1;
}

当你尝试编译这个程序时,你会得到一个非常长的错误信息:

temp.rs:3:11: 3:13 error: borrowed value does not live long enough
temp.rs:3     return &i;
 
temp.rs:1:22: 4:1 note: borrowed pointer must be valid for the anonymous lifetime #1 defined on the block at 1:22...
temp.rs:1 fn foo() -> &int {
temp.rs:2     let i = 1234;
temp.rs:3     return &i;
temp.rs:4 }
 
temp.rs:1:22: 4:1 note: ...but borrowed value is only valid for the block at 1:22
temp.rs:1 fn foo() -> &int {      
temp.rs:2     let i = 1234;            
temp.rs:3     return &i;               
temp.rs:4  }                            
error: aborting due to previous error

为了完全理解这个错误信息,我们需要谈谈“拥有”某些东西意味着什么。 所以现在,让我们接受Rust不允许我们用野指针编写代码,一旦我们理解了所有权,我们再回头看这块代码。

我们先打个比方。 我喜欢读书,有时候我真的很喜欢一本书,会推荐给我的朋友们来读。 当我读我的书时,我拥有它:这本书是我所拥有的。 当我把书”借“给你,在特定的一段时间它是属于你的,然后你把它还给我,我又拥有它了。 对吗?

这个概念也直接应用于Rust代码:一些代码“拥有”一个指向内存的特定指针。 它是该指针的唯一所有者。 它还可以暂时将该内存借给其他代码:这些代码“借”了它。 借用它的这一段时间,称为“生命周期”。

这是关于所有权的所有。 那似乎并不那么难,对吧? 让我们回到那条错误信息:error: borrowed value does not live long enough。 我们试图使用Rust的借用指针&,借出一个特定的变量i。 但Rust知道函数返回后该变量无效,因此它告诉我们:

borrowed pointer must be valid for the anonymous lifetime #1
 
... but borrowed value is only valid for the block。 

完美!

这是栈内存的一个很好的例子,但堆内存呢? Rust有第二种指针,一个‘唯一性‘指针,你可以用?创建。 像这样:

fn foo() -> ~int {
    let i = ~1234;
    return i;
}
 
fn bar() -> int {
    let num = foo();
    return *num + 1;
}

此代码将成功编译。 请注意,我们使用指针指向该值而不是将1234分配给栈:~1234。 你可以大致比较这两行:

// rust
let i = ~1234;
 
// C++
int *i = new int;
*i = 1234;

Rust能够推断出类型的大小,然后分配正确的内存大小并将其设置为您要求的值。 这意味着无法分配未初始化的内存:Rust没有null的概念。万岁!

Rust和C ++之间还有另外一个区别:Rust编译器还计算了i的生命周期,然后在它无效后插入相应的free调用,就像C ++中的析构函数一样。 您可以获得手动分配堆内存的所有好处,而无需自己完成所有工作。 此外,所有这些检查都是在编译时完成的,因此没有运行时开销。这意味着如果你编写了正确的C ++代码,你将编写出与C++代码一样的Rust代码,而且由于编译器的帮忙,编写错误的代码在编译时就不会通过。

你已经看到了一种情况,所有权和生命周期有利于防止在别的语言中通常会出现的危险代码。

现在让我们谈谈另一种情况:并发。

并发
并发是当前软件世界中一个令人头疼的热门话题。 对于计算机科学家来说,它一直是一个有趣的研究领域。随着互联网的使用爆炸式增长,人们希望现有的条件可以服务更多的用户数量。 并发是实现这一目标的方式。 但并发代码有一个很大的缺点:它很难调试,因为它是非确定性的。 编写好的并发代码有几种不同的方法,但让我们来谈谈Rust的所有权和生命周期的概念如何帮助实现正确并且并发的代码。

首先,让我们看一下Rust中的简单并发示例。 Rust允许你启动task,这是轻量级的“绿色”线程(协程)。 这些任务没有任何共享内存,因此,我们使用“通道”在task之间进行通信。 像这样:

fn main() {
    let numbers = [1,2,3];
 
    let (port, chan)  = Chan::new();
    chan.send(numbers);
 
    do spawn {
        let numbers = port.recv();
        println!("{:d}", numbers[0]);
    }
}

在这个例子中,我们创建了一个数字的vector。 然后我们创建一个新的Chan,这是Rust实现通道的包名。 这将返回通道的两个不同端:通道(channel)和端口(port)。 您将数据发送到通道端(channel),它从端口端(port)读出。 spawn函数可以启动一个task。 正如你在代码中看到的那样,我们在task中调用port.recv(),我们在外面调用chan.send(),传入vector。 然后打印vector的第一个元素。

默认情况下,Rust在通过channel发送时会copy vector, 这样即使它是可变的,也不会出现竞争。 但是,如果我们启动了很多task,而我们的数据非常庞大,那么为每个任务都copy副本会使我们的内存使用量膨胀而没有任何实际好处。

引入Arc。 Arc代表“原子引用计数”,它是一种在多个task之间共享不可变数据的方法。 看代码:

extern mod extra;
use extra::arc::Arc;
 
fn main() {
    let numbers = [1,2,3];
 
    let numbers_arc = Arc::new(numbers);
 
    for num in range(0, 3) {
        let (port, chan)  = Chan::new();
        chan.send(numbers_arc.clone());
 
        do spawn {
            let local_arc = port.recv();
            let task_numbers = local_arc.get();
            println!("{:d}", task_numbers[0]);
        }
    }
}

这与我们之前的代码非常相似,除了现在我们循环三次,启动三个task,并在它们之间发送一个Arc。 Arc :: new创建一个新的Arc,.clone()返回Arc的新的引用,而.get()从Arc中获取该值。 因此,我们为每个task创建一个新的引用,将该引用发送到通道,然后使用引用打印出一个数字。 现在我们不copy vector。

Arcs非常适合不可变数据,但可变数据呢? 共享可变状态是并发程序的祸根。 您可以使用互斥锁(mutex)来保护共享的可变状态,但是如果您忘记获取互斥锁(mutex),则可能会发生错误。

Rust为共享可变状态提供了一个工具:RWArc。 Arc的这个变种允许Arc的内容发生变更。 看代码:

extern mod extra;
use extra::arc::RWArc;
 
fn main() {
    let numbers = [1,2,3];
 
    let numbers_arc = RWArc::new(numbers);
 
    for num in range(0, 3) {
        let (port, chan)  = Chan::new();
        chan.send(numbers_arc.clone());
 
        do spawn {
            let local_arc = port.recv();
 
            local_arc.write(|nums| {
                nums[num] += 1
            });
 
            local_arc.read(|nums| {
                println!("{:d}", nums[num]);
            })
        }
    }
}

我们现在使用RWArc包来获取读/写Arc。 RWArc的API与Arc略有不同:有read和write方法允许您读取和写入数据。 这两个方法都将闭包作为参数,并且在写入的情况下,RWArc将获取互斥锁,然后将数据传递给此闭包。 闭包完成后,互斥锁被释放。

你可以看到Rust在你不记得获取锁的情况下是不可能让你改变共享变量的。 我们获得了共享可变状态的便利,同时保持不允许共享可变状态的安全性。

unsafe
虽然Rust编译器非常聪明,并且可以避免你通常犯的错误,但它不是人工智能,我们比编译器更聪明,在某些代码场景中,我们需要打破这种安全检查。 为此,Rust有一个unsafe关键字。 在一个unsafe的代码块里,Rust关闭了许多安全检查。 如果您的程序出现问题,您只需要审核您在不安全范围内所做的事情,而不是整个程序。

如果Rust的主要目标之一是安全,为什么要关闭安全? 嗯,实际上只有三个主要原因:与外部代码连接,例如将FFI写入C库;性能,(在某些情况下)围绕通常不安全的操作提供安全抽象。 我们的Arcs就是后一个目的的而使用unsafe关键字。

我们可以安全地分发对Arc的多个引用,因为我们确信数据是不可变的,因此可以安全地共享。 我们可以分发对RWArc的多个引用,因为我们知道我们已经将数据包装在互斥锁中,因此可以安全地共享。

但Rust编译器无法知道我们已经做出了这些处理,所以在Arc/RWArc的实现中,我们使用unsafe的块来做了(通常来说)一些危险的事情。但是我们暴露了一个安全的接口,这意味着Arc/RWArc不可能被错误地使用。

这就是Rust的类型系统如何让你不会犯一些使并发编程变得不可控的错误,同时也能获得像C ++等语言一样的效率。

 

一文学会Rust?

原文:https://www.cnblogs.com/mignet/p/do_you_need_rust.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!