当前位置: 首页 > 科技观察

半小时上手Rust,这是一场Rust代码风暴

时间:2023-03-14 20:55:10 科技观察

据说很多开发者一天上Python,两天上Go,但是当他们接触到Rust的时候,就会发现画风格隐约是错误的。从语法到特性似乎更复杂。本文介绍Rust。作者表示,通过解析大量代码,你可以在“半小时”内上手Rust。Rust是一种系统编程语言,侧重于安全性,尤其是并发安全性。它支持函数式、命令式、泛型等编程范式的多范式语言,TensorFlow等深度学习框架也将其作为优秀的前端语言。Rust在语法上与C和C++相似,都是用大括号分隔代码块,并且具有相同的控制流关键字,但Rust设计者希望在保证性能的同时提供更好的内存安全性。Rust从2016年开始开源,在各种开发者调查中一直被誉为“最受欢迎的语言”。该开源项目目前拥有42.9K颗星。大多数机器之心的读者都对Python非常熟悉,而Rust则不是那么熟悉。在Amos最近的一篇博客文章中,他表示如果我们阅读他的作品,我们可以在半小时内开始使用Rust。因此,在本文中,我们将介绍这篇博文的主要内容。它不关注一个或几个关键概念。相反,它希望通过代码块来概述Rust的各种特性,包括各种关键字和符号。意义。在HackNews上,很多开发者表示这个入门教程非常实用。Rust入门门槛比较高。如果引入各种复杂的概念和特性,很容易“从入门到说服”。因此,这种从示例代码开始的教程是非常有意义的。说到变量,let可以绑定变量:letx;//declare"x"x=42;//assign42to"x"letx=42;//combinedinoneline可以用:来指定变量的数据类型,和数据类型annotation:letx:i32;//`i32`isasigned32-bitintegerx=42;//有s??i8,i16,i32,i64,i128//还有sou8,u16,u32,u64,u128forunsignedletx:i32=42;//combinedinoneline如果你声明一个变量并在它初始化之前调用它,编译器会报错:letx;foobar(x);//error:borrowofpossibly-uninitializedvariable:`x`x=42;但是,这样做完全没问题:letx;x=42;foobar(x);//thetypeof`x`willbeinferredfromhere下划线表示特殊命名,或者更确切地说是“缺失命名”,有点像Python的用法://thisdoes*nothing*because42isaconstantlet_=42;//thiscalls`get_thing`butthrowsawayitsresultlet_=get_thing();以下划线开头的名称是约定俗成的名称,但编译器不会警告它们未被使用://wemayuse`_x`eventually,butourcodeisawork-in-progress//andwejustwantedtogetridofacompilerwarningfornow.let_x=42;可以单独绑定同名的,第一次绑定的变量会被取消:letx=13;letx=x+3;//using`x`那一行之后只引用第二个`x`,//thefirst`x`nolongerexists.Rust有元组类型,可以认为是“不同数据类型的值的定长集合”。letpair=('a',17);pair.0;//thisis'a'pair.1;//thisis17如果真的要配置pair的数据类型,可以这么写:letpair:(char,i32)=('a',17);元组可以在赋值时被拆解,这意味着它们被分解成单独的字段:let(some_char,some_int)=('a',17);//现在,`some_char`是'a',而`some_int`是17可以当函数返回元组时很有用:let(left,right)=slice.split_at(middle);当然,当解构一个元组时,可以只分离它的一部分:let(_,right)=slice.split_at(middle);分号表示语句结束:letx=3;lety=5;letz=y+x;没有分号意味着语句可以跨越多行:letx=vec![1,2,3,4,5,6,7,8].iter().map(|x|x+3).fold(0,|x,y|x+y);functiontofn声明了一个函数。这是一个空函数:fngreet(){println!("Hithhere!");}这是一个返回32位有符号整数值的函数。箭头表示返回类型:fnfair_dice_roll()->i32{4}大括号表示一个代码块并有自己的作用域://Thisprints"in",then"out"fnmain(){letx="out";{//thisisadifferent`x`letx="in";println!(x);}println!(x);}代码块也是一个表达式,这意味着它的计算结果是一个值。//this:letx=42;//等价于this:letx={42};在一个代码块中,可以有多个语句:letx={lety=1;//firststatementletz=2;//secondstatementy+z//thisisthe*tail*-whatthewholeblockwillevaluateto};这就是为什么“省略函数末尾的分号”相当于加了Retrun,这些是等价的:fnfair_dice_roll()->i32{return4;}fnfair_dice_roll()->i32{4}if条件语句也是一个表达式:fnfair_dice_roll()->i32{iffeeling_lucky{6}else{4}}匹配匹配器也是一个表达式:fnfair_dice_roll()->i32{matchfeeling_lucky{true=>6,false=>4,}}点通常用于访问对象的字段:leta=(10,20);a.0;//thisis10letamos=get_some_struct();amos.nickname;//thisis"fasterthanlime"或者调用对象的方法:letnick="fasterthanlime";nick.len();//thisis14双冒号与此类似,但可以对命名空间进行操作。在这个例子中,std是一个crate(~一个库),cmp是一个模块(~一个源文件),min是一个函数:letleast=std::cmp::min(3,8);//thisis3use指令可用于从其他命名空间“引入作用域”:usestd::cmp::min;letleast=min(7,1);//thisis1在use指令中,花括号还有另外一个含义:“globs”,所以你可以同时导入最小值和最大值://thisworks:usestd::cmp::min;usestd::cmp::max;//thisalsoworks:usestd::cmp::{min,max};//thisalsoworks!使用标准::{cmp::min,cmp::max};通配符(*)允许从命名空间导入符号://thisbrings`min`and`max`inscope,andmanyotherthingsusestd::cmp::*;类型也是命名空间和方法,可以像普通函数一样调用:letx="amos".len();//thisis4letx=str::len("amos");//thisisalso4str是原始数据类型,但默认情况下很多非原始数据类型也在范围内。//`Vec`是regularstruct,notaprimitivetypeletv=Vec::new();//这是一模一样的代码,但是带有*完整*路径的`Vec`letv=std::vec::Vec::new()至于为什么有效,因为Rust的每个模块的开头都插入了:usestd::prelude::v1::*;下面说说使用struct关键字声明结构的结构:structVec2{x:f64,//64-bitfloatingpoint,aka"doubleprecision"y:f64,}可以使用结构语句初始化:letv1=Vec2{x:1.0,y:3.0};letv2=Vec2{y:2.0,x:4.0};//顺序无关紧要,只有名字有快捷方式从另一个结构初始化这个结构的其余字段:letv3=Vec2{x:14.0,..v2};这就是所谓的“结构更新语法”,只能出现在最后一个位置,后面不能跟逗号。注意剩下的字段可以代表所有字段:letv4=Vec2{..v3};结构,如元组,可以被解构。例如一个有效的let模式:let(left,right)=slice.split_at(middle);letv=Vec2{x:3.0,y:6.0};letVec2{x,y}=v;//`x`isnow3。0,`y`isnow`6.0`letVec2{x,..}=v;//thisthrowsaway`v.y`let模式可以作为if中的条件:structNumber{odd:bool,value:i32,}fnmain(){letone=Number{odd:true,value:1};lettwo=Number{odd:false,value:2};print_number(one);print_number(two);}fnprint_number(n:Number){ifletNumber{odd:true,value}=n{println!("Oddnumber:{}",value);}elseifletNumber{odd:false,value}=n{println!("Evennumber:{}",value);}}//thisprints://Oddnumber:1//Evennumber:2多分支匹配也是一种条件模式,就像iflet:fnprint_number(n:Number){matchn{Number{odd:true,value}=>println!("Oddnumber:{}",value),Number{odd:false,value}=>println!("Evennumber:{}",value),}}//这个printsthesameasbeforematch必须覆盖所有情况:至少需要匹配一个条件分支。fnprint_number(n:Number){matchn{Number{value:1,..}=>println!("One"),Number{value:2,..}=>println!("Two"),Number{value,..}=>println!("{}",value),//ifthatlastarmdidn'texist,wewouldgetacompile-timeerror}}如果实现起来非常困难,_可以作为“包罗万象”的模式:fnprint_number(n:Number){matchn.value{1=>println!("一"),2=>println!("二"),_=>println!("{}",n.value),}}我们可以使用的类型别名type关键字声明了另一种类型的别名,然后就可以像使用真实类型一样使用它。比如定义Name的数据类型为字符串,后面就可以直接使用Name的类型了。您可以在方法中声明不同的数据类型:structNumber{odd:bool,value:i32,}implNumber{fnis_strictly_positive(self)->bool{self.value>0}}并照常使用它:fnmain(){letminus_two=Number{odd:false,value:-2,};println!("positive?{}",minus_two.is_strictly_positive());//thisprints"positive?false"}默认情况下,声明变量后会是不可变的,以下奇数不能重新分配:fnmain(){letn=Number{odd:true,value:17,};n.odd=false;//error:cannotassignto`n.odd`,//as`n`isnotdeclaredtobemutable}不可变变量声明在内部也是不可变的,它不能重新分配一个值:fnmain(){letn=Number{odd:true,value:17,};n=Number{odd:false,value:22,};//error:cannotassigntwicetoimmutablevariable`n`}mut可以使变量声明变量:fnmain(){letmutn=Number{odd:true,value:17,}n.value=19;//allgood}Traits描述了多种数据类型的共性:traitSigned{fnis_strictly_negative(self)->bool;}我们可以在我们定义的Type类型中定义Traits:implSignedforNumber{fnis_strictly_negative(self)->bool{self.value<0}}fnmain(){letn=Number{odd:false,value:-44};println!("{}",n.is_strictly_negative());//打印"true"}外部类在类型(外来类型)中定义的特征:implSignedfori32{fnis_strictly_negative(self)->bool{self<0}}fnmain(){letn:i32=-44;println!("{}",n.is_strictly_negative());//prints"true"}impl模块通常有一个Type类型,所以在模块中,Self代表这个类型:implstd::ops::NegforNumber{typeOutput=Self;fnneg(self)->Self{Self{value:-self.value,odd:self.odd,}}}有一些traits只是作为标记,它们并不意味着Type类型实现了某些方法,它只是表明可以通过Type类型做一些事情比如i32实现了Copy,那么下面的代码是可行的:fnmain(){leta:i32=15;letb=a;//`a`iscopiedletc=a;//`a`iscopiedagain}下面的代码也可以运行的:fnprint_i32(x:i32){println!("x={}",x);}fnmain(){leta:i32=15;print_i32(a);//`a`iscopiedprint_i32(a);//`a`iscopiedagain}但是Number结构不能用于Copy,所以下面的代码会报错:fnmain(){letn=Number{odd:true,value:51};letm=n;//`n`ismovedinto`m`leto=n;//error:useofmovedvalue:`n`}下面的代码也不起作用:fnprint_number(n:Number){println!("{}number{}",ifn.odd{"odd"}else{"even"},n.value);}fnmain(){letn=Number{odd:true,value:51};print_number(n);//`n`ismovedprint_number(n);//错误:useofmovedvalue:`n`}但如果print_number有一个不可变引用,那么Copy是可行的:fnprint_number(n:&Number){println!("{}number{}",ifn.odd{"odd"}else{"even"},n.value);}fnmain(){letn=Number{odd:true,value:51};print_number(&n);//`n`isborrowedforthetimeofthecallprint_number(&n);//`n`isborrowedagain}如果函数使用了变量引用,也是可行的,但是需要在变量声明中带上mut。fninvert(n:&mutNumber){n.value=-n.value;}fnprint_number(n:&Number){println!("{}number{}",ifn.odd{"odd"}else{"even"},n.value);}fnmain(){//这一次,`n`ismutableletmutn=Number{odd:true,value:51};print_number(&n);invert(&mutn);//`nisborrowedmutably-everythingisexplicitprint_number(&n);像}Copy这样的标记特征没有方法://note:`Copy`requiresthat`Clone`isimplementedtooimplstd::clone::CloneforNumber{fnclone(&self)->Self{Self{..*self}}}implstd::marker::CopyforNumber{}现在Clone仍然可以使用:fnmain(){letn=Number{odd:true,value:51};letm=n.clone();leto=n.clone();}但是值数字不会被删除:fnmain(){letn=Number{odd:true,value:51};letm=n;//`m`isacopyof`n`leto=n;//same.`n`isneithermovednorborrowed.}有一些traits是很常见的,可以通过derive属性自动实现:`块。整个教程似乎用了很多代码来解释Rust的各种语句和用法。可能大家觉得博客结构不是很清晰,但是实例驱动的代码学习确实效率更高。尤其是对于有一定编程基础的同学,可以快速掌握Rust语言的特点和逻辑。最后,本文并未展示博客的所有内容。读者如果想真正入门Rust语言,建议参考原博客。