问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

Rust科普向:Rust到底难在哪?特色语言特性20min速通攻略

发布网友 发布时间:2024-09-25 14:21

我来回答

1个回答

热心网友 时间:2024-09-29 14:34

本文面向对象:

希望尝试了解Rust相关代码仓库(eg.deno)的Rust零基础同学

希望了解Rust语法特性拓展知识范围的同学

希望对编程语言底层逻辑加深理解的同学

前言我们为什么要学习Rust?

上图是知乎上一个关于Rust的问题

不少人抱有疑问:“为什么人们还要卷一个Rust新语言出来呢?”

这个问题回答的思路可以有多条:

Rust如何解决现有编程语言的内存管理问题痛点

Rust如何兼顾工程化和性能

Rust如何从源头上提升代码质量

你可能还会问:“可是我目前没有Rust落地的场景?”

这个问题就更好解决了,只要你对目前业界最特色的垃圾回收机制感兴趣,那继续读下去肯定会有一定的收获。

你可能的最后一个问题:“听说Rust学习曲线出了名的陡峭,是不是很难入门啊?”

确实,他的学习难度业界闻名(大概比Java还难一个C的程度),但是他的代码review难度却是公认比较简单的,你不好奇为什么会有这样的现象吗?

缺点VS优点那些舒服的地方

看看这红线,像不像小学班主任批改作业的注释?保姆级别的编译报错提示,手把手教你改bug

如果你的代码可以编译成功,那你不需要再考虑内存相关的逻辑,Rust已经完全处理完成,你只需要聚焦于业务内容

可以比肩C/C++的强大性能,底层(操作系统,区块链,WebAssembly等)开发者的利器

不那么舒服的地方

新手写一个小时Rust的报错提示可能就长到翻不完,学习难度客观存在

生态不如其他成熟的编程语言那样完善,但依然是富有活力的发展中

Rust的地狱笑话:为什么不尝试用Rust写个链表呢?(由于语言特性原因,这会非常困难)

一些和现有其他热门语言设计有出入的细节(天国的return/天国的分号/::满天飞/'a满天飞)

与众不同的设计基础概念栈与堆

栈(stack)和堆(heap)都是可以在运行期使用的内存空间。栈是结构规整,每块大小相当,先进先出的数据结构,而堆的结构较为松散,需要使用空间时还需要进行合理的分配。

一些大小固定的数据类型(eg.Int/Char),这些数据往往会存储在栈中,栈大小固定,不需要计算需要分配的空间,时间开销也更小。

而一些大小不固定的数据类型(eg.JavaScript:Object/Rust:String),这些数据的实际内容会存储在堆内,在栈中存储数据在堆中的指针等相关信息。

JavaScript中的堆和栈

得益于CPU高速缓存,使得处理器可以减少对内存的访问,高速缓存和内存的访问速度差异在10倍以上,栈数据往往可以直接存储在CPU高速缓存中,而堆数据只能存储在内存中。访问堆上的数据比访问栈上的数据慢,因为必须先访问栈再通过栈上的指针来访问内存。

试想,如果堆中数据存储得不到释放,一直无限增加下去,我们的程序势必会出现各种异常的现象。

GC(垃圾回收)

如果你是一个一直使用某种高级编程语言(JavaScript/Java/Python等)的开发者,可能会对垃圾回收机制了解的不是很多。

例如JavaScript,由于JavaScript引擎帮你做了一切,所以开发者不需要在代码层面过多关注于内存开销带来的影响。

GC全称GarbageCollection,什么是“垃圾”?我们在程序开发过程中,会使用到一些系统内存,当某块内存使用完毕后,就需要被回收。如果不被回收,这块内存就会一直被占用下去,无法被重复利用,严重的内存泄露会引起程序卡死。

如下图,js堆的大小呈阶梯状上升:

三种垃圾处理方法

全自动回收:JavaScript/Java/Python【彻底解放双手】

手动回收:C/C++【自己动手,丰衣足食】

按特定规则自动回收:Rust【剑走偏锋】

全自动的垃圾回收能力在大部分业务场景看起来都非常美好,它大大减小了开发者对于内存控制的心智负担,我们可以把目光集中在其他需要持续关注的角度上。

而在偏向系统层级的底层技术领域(eg.音视频领域/游戏客户端),我们对内存占用的开销有更高的要求,这时候手动进行内存管理使我们有多大的优化空间来施展拳脚。

C/C++带来“自由”的内存管理是把无情的双刃剑,“自由”也伴随着无尽的bug和更高昂的代码理解成本。这时候,为什么不试试Rust?Rust也不需要开发者自己进行空间申请/释放等操作,为了兼顾更好的性能,它引入了特别的【所有权】和【生命周期】概念,编译器在编译时会根据一系列规则进行检查,在执行时就能保证安全且不带来性能开销。

所有权

所有权是一组控制Rust程序如何管理内存的规则。

基础规则

Rust中的每个值都有一个名为所有者的变量。

一次只能有一个所有者。

当所有者超出范围时,该值将被删除。

简单解释这个规则:我们的每一个量有且只有一个所有者,当所有者失效后,这个值也就不存在了。

让我们来看看Rust的String类型,它比起int/char等类型更为特殊

fn?main()?{????let?s1?=?String::from("hello");????let?s2?=?s1;????println!("{},?world!",?s1);}

String::from:从字符串字面值创建String

上面的逻辑如果出现在js/java中,只是非常简单的赋值过程,顺利执行。而Rust要求每一个量只有一个所有者,在执行到第三行lets2=s1;时,"hello"的所有者变成了s2,这个时候再次打印s1,我们只会收获一个error。

用图形描述为下图:栈中的s1转移对"hello"的所有权到s2身上,且s1失效。

假设,如果s1不失效,在编译阶段回收数据时,Rust会发现有两个指针指向同一片内存,"index"这片内存会被错误的释放两次!两次释放相同的内存会导致内存污染,它可能会导致潜在的安全漏洞。

如果我们想要让s1也正常打印,就必须强拷贝一份"hello",如下图:

Rust永远也不会自动创建复杂数据类型的“深拷贝”,因为这对内存开销实在太大,我们在需要时要手动调用。

如果我们想使用一个Int类型的数据:

fn?main()?{????let?x?=?5;????let?y?=?x;????println!("x?=?{},?y?=?{}",?x,?y);}

以上代码是可以正常执行的,因为x是简单类型(Int),是固定大小可以存储在栈中的数据类型。这时Rust会帮助我们拷贝一份放在栈中,因为在栈中拷贝是非常快速的。

如果我们的所有权只能简单的唯一归属,那势必会加大我们的编码难度,下面我们来看看引用。

引用

引用又被分为可变引用和不可变引用:

let?x?=?10;let?r?=?&x;

r就是x的一个不可变引用(引用是对内存中的另一个值的非拥有(nonowning)指针类型)

当我们想希望用指针指向一个变量且希望改变这个变量时,我们也会用到可变引用(&mut):

fn?main()?{????let?mut?s?=?String::from("hello");????change(&mut?s);}fn?change(some_string:?&mut?String)?{????some_string.push_str(",?world");}

同一时刻,你只能拥有要么一个可变引用,要么任意多个不可变引用(保证绝对安全)

引用必须总是有效的(无效就是空指针了)

由于Rust新老编译器的区别,编译是否成功也不同:

fn?main()?{???let?mut?s?=?String::from("hello");????let?r1?=?&s;?????let?r2?=?&s;?????println!("{}?and?{}",?r1,?r2);????//?新编译器中,r1,r2作用域在这里结束????let?r3?=?&mut?s;?????println!("{}",?r3);}?//?老编译器中,r1、r2、r3作用域在这里结束??//?新编译器中,r3作用域在这里结束

letmuts定义变量s

letr1=&s;r1为s的不可变引用,即指向对应内存的指针

letr3=&muts;r3为s的可变引用,这时s可以更改

因为篇幅原因,这里只简单介绍一下引用的相关知识,希望有更多了解的同学可以阅读后面的深入学习资料。

生命周期基础规则

生命周期,简而言之就是有效作用域。部分情况时,我们无需手动的声明生命周期,因为编译器可以自动进行推导。

fn?main()?{{????let?r;????????????????//?---------+--?'a????{?????????????????????//??????????|????????let?x?=?5;????????//?-+--?'b??|????????r?=?&x;???????????//??|???????/????}?????????????????????//?-+???????|????println!("r:?{}",?r);?//??????????|}?????????????????????????//?---------+}

以上代码会在编译时报错:因为'x'不能存活那么长。

在Rust中,从数据定义到一对大括号的结束,就是一个生命周期范围(见上图的'a和'b),'a是r的生命周期,'b是x的生命周期,而x的生命周期小于r的生命周期,所以在执行r=&x;后,x的生命周期就结束了,这时r指向了一个被回收的数据的地址,变成了一个悬垂指针,所以就出错了。

当的函数入参出现引用类型时,稍有不慎就可能出现悬垂指针,所以Rust编译器比我们更加紧张:

fn?main()?{????let?string1?=?String::from("long?string?is?long");????let?string2?=?String::from("xyz");????let?result?=?longest(string1.as_str(),?string2.as_str());????println!?("The?longest?string?is?{}",?result);}????fn?longest(x:?&str,?y:?&str)?->?&str?{????????if?x.len()?>?y.len()?{????????????x????????}?else?{????????????y????????}????}

上面是一个判断字符串长度的函数,看起来完全ok,但其实也会报错:

这个报错的原因是,Rust无法推断x和y的生命周期谁更长!因为编译器无法分析出要returnx还是y,所以我们要显式声明入参的生命周期。

&i32????????//?一个引用&'a?i32?????//?具有显式生命周期的引用&'a?mut?i32?//?具有显式生命周期的可变引用

生命周期的格式如上面所示,我们来修改一下刚才错误的的代码:

fn?main()?{????let?string1?=?String::from("abcd");????let?string2?=?"xyz";????let?result?=?longest(string1.as_str(),?string2);????println!?("The?longest?string?is?{}",?result);}????fn?longest<'a>?(x:?&'a?str?,?y:?&'a?str)?->?&'a?str{????????if?x.len()?>?y.len()?{????????????x????????}?else?{????????????y????????}????}

现在就可以正常运行了!

首先请牢记:生命周期标注并不会改变任何引用的实际作用域

生命周期标注简单来说就是你在教编译器做事,且它只是起一个指导作用!

因为我们的编译器有时候还是很智慧的,比如它在一些简单的场景面前可以自己推导出生命周期(当只有一个入参是引用类型的时候,如果能正常编译,返回值的生命周期只可能与这一个入参相关),但是复杂场景它往往无法推测出来!

回到之前的例子,我们的标注可以说明:

和泛型一样,使用生命周期参数,需要先声明<'a>

x、y和返回值至少活得和'a一样久(因为返回值要么是x,要么是y,x、y的生命周期相同)

如果我们不添加标注,对Rust编译器来说,其实相当于:

????fn?longest<'a,'b>?(x:?&'a?str?,?y:?&'b?str)?->?&str{//

Rust无法自动推断x返回值的生命周期是'a还是'b,所以我们需要手动“告知”Rust。

欺骗编译器可行吗?

我们简单修改一下刚才例子中的代码,让string1和string2拥有不同的生命周期,但是我们在longest函数中还是把两个入参标注为同样的生命周期:

fn?main()?{????let?x?=?5;????let?y?=?x;????println!("x?=?{},?y?=?{}",?x,?y);}0

string1的生命周期明显大于string2,结果:

哈哈,不出意料的报错了,再次验证开头说的“生命周期标注并不会改变任何引用的实际作用域”

你永远无法欺骗Rust编译器~

如果你想深入学习文档资料

本文只是抛砖引玉!Rust还有很多很多内容值得研究!

以下两篇基本是必读了,本文的部分内容也参考了下面的教程:

Rust程序设计语言-Rust程序设计语言简体中文版

进入Rust编程世界-Rust语言圣经(Rust教程RustCourse)

视频资料

在b站发现的宝藏视频,讲解的上面的第一篇文档。

老师讲的的时候会带代码演示,比纯啃文章好理解多了(老师的东北口音也很带劲,越听越精神)

https://www.bilibili.com/video/BV1hp4y1k7SV

参考

https://juejin.cn/post/6981588276356317214

栈、堆、队列深入理解,面试无忧-掘金

https://juejin.cn/post/6844904106310516744

原文:https://juejin.cn/post/7099362775621140510

Rust科普向:Rust到底难在哪?特色语言特性20min速通攻略

而一些大小不固定的数据类型(eg.JavaScript:Object/Rust:String),这些数据的实际内容会存储在堆内,在栈中存储数据在堆中的指针等相关信息。JavaScript中的堆和栈 得益于CPU高速缓存,使得处理器可以减少对内存的访问,高速缓存和内存的访问速度差异在10倍以上,栈数据往往可以直接存储在CPU高速缓存中,而堆数据只能存储在...

想学编程不知道从哪里开始??

Rust是一门系统编程语言 [1] ,专注于安全 [2] ,尤其是并发安全,支持函数式和命令式以及泛型等编程范式的多范式语言。Rust在语法上和C++类似 [3] ,但是设计者想要在保证性能的同时提供更好的内存安全。 Rust最初是由Mozilla研究院的Graydon Hoare设计创造,然后在Dave Herman, Brendan Eich以及...

Rust科普向:Rust到底难在哪?特色语言特性20min速通攻略

而一些大小不固定的数据类型(eg.JavaScript:Object/Rust:String),这些数据的实际内容会存储在堆内,在栈中存储数据在堆中的指针等相关信息。 JavaScript中的堆和栈 得益于CPU高速缓存,使得处理器可以减少对内存的访问,高速缓存和内存的访问速度差异在10倍以上,栈数据往往可以直接存储在CPU高速缓存中,而堆数据只能存储在...

科普文章的语言特色 科普文语言特色 科普语言的特征 特色科普活动 科普特色课程 社区特色科普活动 社区科普特色名称 特色主题科普活动 科普作品有什么特点
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
柑橘引种应注意什么问题? 男人挣多少钱可以结婚? ...包吃住吃的话点外卖请问一天一个人50块钱标准多吗? ...什么适合的配乐?朗诵时的停顿要注意什么?希望大家多多指教 ...客户扫了一万块信用卡,我应该收多少手续费? 从天津火车站坐几路车到天津市中心,坐到哪一站下车? 爱情里很有哲理的句子 我在天津到190公车始发站坐地铁在哪儿下 红米note3和魅蓝note3哪个好啊 野生桃核什么时候种植 为什么我说 Rust 是靠谱的编程 语言 扎克伯格的妻子长相并不出众,为何有人说他的财富只配得上妻子的... 微信来信息怎么不提示 信息不提示怎么办? 今年92岁属什么的 92岁属什么 现在胃不舒服能吃西红柿炒鸡蛋吗 千寻rtk7参数计算过程 龙抄手怎么做简单又美味? 410文科 今年报成都师范学院会计专业怎么样? 郫县二中高中09年分数线是多少 郫县二中和郫县一中比哪个好些? 脚背崴肿了怎么办 脚崴了2个多月还没好 崴脚了怎样能尽快的好起来啊??? 上学呢,崴脚怎么办,三楼,初二正是关键的时候。急急急 扫码支付能查到对方的微信号吗? 宝宝上呼吸道感染反复发烧怎么治疗 小孩上呼吸道感染反复发烧怎么办 儿童上呼吸道感染反复发烧怎么办 吃鸡蛋有什么说法 上联 三人行 你我他 哪家测量仪公司可以选择? ...方程X�0�5/3-K+Y�0�5/K-1=1表示双曲线的什么条件... 若k属于r,若k&gt;3是方程x^2\k-3-y^2\k+3=1表示双曲线的什么条件。为什么... QQ群的群标如何修改?不是说的和头像一样那个图标哦! 为什么QQ登陆后好友图标全是默认的那种。个性签名也不显示出来。QQ群里... 为什么高级QQ群的自定义图标老是会还原成初始的图标? ...做的是内勤,来的时候老板说给我2000元的工资;试用期两个月,_百度知... ...老板说她亏损。给我们开1400元。原工资2000元。我们怎么? 宝宝细菌感染反复发烧怎么办呢 宝宝上呼吸道感染反复发烧几天能好 如何呼吁家长踊跃参加活动 如何给家长们发微信群通知呢? 求图片 出处, 这人是谁》 ? 你经历过最尴尬的事是怎样的 作为女生,你做过最羞涩的事是什么?晚上男生,我想知道女生的情况 上海六院不建卡可以产检吗?39周了,要去做最后一次产检了,我都需要做... 上海松江第一人民医院不建卡可以产检吗 上海徐泾社区医院不建卡能产检吗