Rust编程语言从入门到放弃

Rust是一门系统编程语言(SystemsProgrammingLanguage),兼顾安全(Safety)、性能(Speed)和并发(Concurrency)。Rust作为一门底层的系统编程语言,理论上,使用C/C++的领域都可以使用Rust实现,例如对硬件需要精细控制的嵌入式编程、对性能要求极高的应用软件(数据库引擎、浏览器引擎,3D渲染引擎等)。相对于C/C++的系统性缺陷(内存管理不当造成的安全漏洞),Rust通过所有权(Ownership)机制在编译期间确保内存安全,无需垃圾回收(GarbageCollection,GC),也不需要手动释放内存。

1.HelloWorld1.1安装Rust

在线安装

Windows:下载,自动引导安装。

Linux:curl–proto‘=https’–|sh

离线安装

下载独立安装包

Windows:下载.msi文件,双击安装即可。

Linux:下载.文件,解压后,执行即可。

查看版本

$(4560ea7882019-11-04)$(1c6ec66d52019-09-30)
1.2第一个Rust程序
fnmain(){println!("Hello,world!");}

使用fn声明函数。与大部分编程语言一致,main()是Rust程序的入口。println!表示打印文本到控制台,!表示这是一个宏(macro),而非一个函数(function)。

保存为hello_,rs为Rust语言的后缀。

编译:rustchello_。

执行:./hello_world(Linux),hello_(Windows)

尝试下println!更多的用法。

fnmain(){println!("{},{}!","Hello","world");//Hello,world!println!("{0},{1}!","Hello","world");//Hello,world!println!("{greeting},{name}!",greeting="Hello",name="world");//Hello,world!lety=String::from("Hello,")+"world!";println!("{}",y);//Hello,world!}

以上代码将输出

Hello,world!Hello,world!Hello,world!Hello,world!
1.3使用Cargo

为了方便之后的调试和学习,先介绍Rust内置的包管理和构建系统Cargo,是Rust的社区仓库。

创建新项目:cargonew

编译:cargobuild

运行:cargorun

更新项目依赖:cargoupdate

执行测试:cargotest

生成文档:cargodoc

静态检查:cargocheck

新建二进制(Binary/Executable)项目

$cargonewtutu--bin$cdtututree├──└──src└──

在中写入

fnmain(){println!("Hello,Cargo!");}

在项目目录下执行cargorun

$cargoruntutugit:(master)✗(/xxx/demo/tutu)Finisheddev[unoptimized+debuginfo]target(s)`target/debug/tutu`Hello,Cargo!

新建Library项目

$cargonewtutu--lib$cdtututree├──└──src└──

是工程的描述文件,包含Cargo所需的所有元信息。

src放置源代码。

/是入口文件。

运行cargorun或cargobuild,可执行文件将生成在target/debug/目录,运行cargobuild–release,可执行文件将生成在target/release/。

2基本概念2.1注释
///外部注释modtest{//行注释/*块注释*/}modtest{//!包/模块级别的注释//}
2.2变量

局部变量

Rust中变量默认是不可变的(immutable),称为变量绑定(Variablebindings),使用mut标志为可变(mutable)。

let声明的变量是局部变量,声明时可以不初始化,使用前初始化即可。Rust是静态类型语言,编译时会检查类型,使用let声明变量时可以省略类型,编译时会推断一个合适的类型。

//不可变letc;leta=true;letb:bool=true;let(x,y)=(1,2);c=12345;//可变letmutz=5;z=6;

全局变量

rust中可用static声明全局变量。用static声明的变量的生命周期是整个程序,从启动到退出,它占用的内存空间是固定的,不会在执行过程中回收。另外,static声明语句,必须显式标明类型,不支持类型自动推导。全局变量在声明时必须初始化,且须是简单赋值,不能包括复杂的表达式、语句和函数调用。

//静态变量(不可变)staticN:i32=5;//静态变量(可变)staticmutN:i32=5;

常量

const的生命周期也是整个程序,const与static的最大区别在于,编译器并不一定会给const常量分配内存空间,在编译过程中,它很可能会被内联优化,类似于C语言的宏定义。

constN:i32=5;
2.3函数

使用fn声明函数。

fnmain(){println!("Hello,world!");}

参数需要指定类型

fnprint_sum(a:i8,b:i8){println!("sumis:{}",a+b);}

默认返回值为空(),如果有返回值,需要使用-指定返回类型。

fnplus_one(a:i32)-i32{a+1//等价于returna+1,可省略为a+1}

可以利用元组(tuple)返回多个值

fnplus_one(a:i32)-(i32,i32){(a,a+1)}fnmain(){let(add_num,result)=plus_one(10);println!("{}+1={}",add_num,result);//10+1=11}

函数指针也可以作为变量使用

letb=plus_one;letc=b(5);//6
2.4基本数据类型

布尔值(bool)

字符(char)

有符号整型(i8,i16,i32,i64,i128)

无符号整型(u8,u16,u32,u64,u128)

指针大小的有符号/无符号整型(isize/usize,取决于计算机架构,32bit的系统上,isize等价于i32)

浮点数(f32,f64)

数组(arrays),由相同类型元素构成,长度固定。

leta=[1,2,3];//a[0]=1,a[1]=2,a[2]=3letmutb=[1,2,3];letc:[int;3]=[1,2,3];//[类型;数组长度]letd:["myvalue";3];//["myvalue","myvalue","myvalue"];lete:[i32;0]=[];//空数组println!("{:?}",a);//[1,2,3]

数组(arrays)的长度是可不变的,动态/可变长数组可以使用Vec(非基本数据类型)。

元组(tuples),由相同/不同类型元素构成,长度固定。

leta=(1,1.5,true,'a',"Hello,world!");//=1,=1.5,=true,='a',="Hello,world!"letb:(i32,f64)=(1,1.5);let(c,d)=b;//c=1,d=1.5let(e,_,_,_,f)=a;//e=1,f="Hello,world!",_作为占位符使用,表示忽略该位置的变量letg=(0,);//只包含一个元素的元组leth=(b,(2,4),5);//((1,1.5),(2,4),5)println!("{:?}",a);//(1,1.5,true,'a',"Hello,world!")

元组的长度也是不可变的,更新元组内元素的值时,需要与之前的值的类型相同。

切片(slice),指向一段内存的指针。

切片并没有拷贝原有的数组,只是指向原有数组的一个连续部分,行为同数组。访问切片指向的数组/数据结构,可以使用操作符。

leta:[i32;4]=[1,2,3,4];letb:[i32]=a;//全部letc=a[0..4];//[0,4)letd=a[..];//全部lete=a[1..3];//[2,3]lete=a[1..];//[2,3,4]lete=a[..3];//[1,2,3]

字符串(str)

在Rust中,str是不可变的静态分配的一个未知长度的UTF-8字节序列。str是指向该字符串的切片。

leta="Hello,world!";//a:'staticstrletb:str="你好,世界!";

字符串切片str指向的字符串是静态分配的,在Rust中,有另一个堆分配的,可变长的字符串类型String(非基本数据类型)。通常由字符串切片str通过to_string()或String::from()方法转换得到。

lets1="Hello,world!".to_string();lets2=String::from("Hello,world!");

函数(functions)

函数指针也是基本数据类型,可以赋值给其他的变量。

2.5操作符

算数运算符

+-*/%leta=5;letb=a+1;//6letc=a-1;//4letd=a*2;//10lete=a/2;//⭐️2=a%2;//1letg=5.0/2.0;//2.5

比较运算符

===!===leta=1;letb=2;letc=a==b;//falseletd=a!=b;//truelete=ab;//trueletf=ab;//falseletg=a=a;//trueleth=a=a;//trueleti=truefalse;//trueletj='a''A';//true

逻辑运算符

!||leta=true;letb=false;letc=!a;//falseletd=ab;//falselete=a||b;//true

位运算符

|^leta=1;letb=2;letc=ab;//0(0110-00)letd=a|b;//3(01||10-11)lete=a^b;//3(01!=10-11)letf=ab;//4(左移-'01'+'00'-100)letg=aa;//0(右移-o̶1̶-0)

赋值运算符

letmuta=2;a+=5;//2+5=7a-=2;//7-2=5a*=5;//5*5=25a/=2;//25/2=12%=5;//12%5=2a=2;//1010-10-2a|=5;//010||101-111-7a^=2;//111!=010-101-5a=1;//'101'+'0'-1010-10a=2;//101̶0̶-10-2

类型转换运算符:as

leta=15;letb=(aasf64)/2.0;//7.5

借用(Borrowing)与解引用(Dereference)操作符

Rust引入了所有权(Ownership)的概念,所以在引用(Reference)的基础上衍生了借用(Borrowing)的概念,所有权概念不在这里展开。

简单而言,引用是为已存在变量创建一个别名;获取引用作为函数参数称为借用;解引用是与引用相反的动作,目的是返回引用指向的变量本身。

//解引用:*fnmain(){//获取v的第2个元素的可变引用,并通过解引用修改该元素的值。letv=mut[1,2,3,4,5];{letthird=_mut(2).unwrap();*third+=50;}println!("v={:?}",v);//v=[1,2,53,4,5]}
//解引用:*fnmain(){//获取v的第2个元素的可变引用,并通过解引用修改该元素的值。letv=mut[1,2,3,4,5];{letthird=_mut(2).unwrap();*third+=50;}println!("v={:?}",v);//v=[1,2,53,4,5]}
2.6控制流(ControlFlows)

if-elseif-else

letteam_size=7;ifteam_size5{println!("Small");}elseifteam_size10{println!("Medium");}else{println!("Large");}//条件块中有返回值时,类型需要一致,可替代C语言的三目运算符letis_below_eighteen=ifteam_size18{true}else{false};

match

可替代C语言的switchcase。

lettshirt_width=20;lettshirt_size=matchtshirt_width{16="S",//check1617|18="M",//check17and181921="L",//checkfrom19to21(19,20,21)22="XL",_="NotAvailable",};println!("{}",tshirt_size);//L

_表示匹配剩下的任意情况。

while

letmuta=1;whilea=10{println!("Currentvalue:{}",a);a+=1;//Rust不支持++/--自增自减语法}

loop

类似于C语言的while(1)

letmuta=0;loop{ifa==0{println!("SkipValue:{}",a);a+=1;continue;}elseifa==2{println!("BreakAt:{}",a);break;}println!("CurrentValue:{}",a);a+=1;}//SkipValue:0//CurrentValue:1//BreakAt:2

for

forain0..10{//(a=0;a10;a++)println!("Currentvalue:{}",a);}'outer_for:forc1in1..6{//setlabelouter_for'inner_for:forc2in1..6{println!("CurrentValue:[{}][{}]",c1,c2);ifc1==2c2==2{break'outer_for;}//结束外层循环}}letgroup:[str;4]=["Mark","Larry","Bill","Steve"];(){println!("CurrentPerson:{}",person);}

在for表达式中的break'outer_for,loop和while也有相同的使用方式。

3.其他数据类型3.1结构体(struct)

和元组(tuple)一样,结构体(struct)支持组合不同的数据类型,但不同于元组,结构体需要给每一部分数据命名以标志其含义。因而结构体比元组更加灵活,不依赖顺序来指定或访问实例中的值。

定义结构体

structUser{username:String,email:String,sign_in_count:u64,active:bool,}

创建实例

letuser1=User{email:String::from("someone@"),username:String::from("someusername123"),active:true,sign_in_count:1,};

修改某个字段的值

letmutuser1=User{email:String::from("someone@"),username:String::from("someusername123"),active:true,sign_in_count:1,};=String::from("anotheremail@");

变量与字段名同名的简写语法

structColor(i32,i32,i32);structPoint(i32,i32);letblack=Color(0,0,0);letorigin=Point(3,4);

元组结构体(tuplestructs)

元组结构体有着结构体名称提供的含义,但没有具体的字段名。在参数个数较少时,无字段名称,仅靠下标也有很强的语义时,为每个字段命名就显得多余了。例如:

12345678

VS

structPoint{x:i32y:i32}letorigin=Point{x:3y:4}
structPoint{x:i32y:i32}letorigin=Point{x:3y:4}
3.2枚举(enum)

定义枚举

enumIpAddrKind{V4,V6,}

使用枚举值

letfour=IpAddrKind::V4;fnroute(ip_type:IpAddrKind){}route(four);route(IpAddrKind::V6);

枚举成员关联数据

enumIpAddr{V4(u8,u8,u8,u8),V6(String),}lethome=IpAddr::V4(127,0,0,1);letloopback=IpAddr::V6(String::from("::1"));

更复杂的例子

enumMessage{Quit,//不关联数据Move{x:i32,y:i32},//匿名结构体Write(String),ChangeColor(i32,i32,i32),}

match控制流

enumCoin{Penny,Nickel,Dime,Quarter,}fnvalue_in_cents(coin:Coin)-u32{matchcoin{Coin::Penny={println!("Luckypenny!");1},Coin::Nickel=5,Coin::Dime=10,Coin::Quarter=25,}}

Option

Option是标准库中定义的一个非常重要的枚举类型。Option类型应用广泛因为它编码了一个非常普遍的场景,即一个值要么有值要么没值。对Rust而言,变量在使用前必须要赋予一个有效的值,所以不存在空值(Null),因此在使用任意类型的变量时,编译器确保它总是有一个有效的值,可以自信使用而无需做任何检查。如果一个值可能为空,需要显示地使用OptionT来表示。

Option的定义如下:

pubenumOptionT{Some(T),None,}

OptionT包含2个枚举项:

1)None,表明失败或没有值2)Some(value),元组结构体,封装了一个T类型的值value

得益于Option,Rust不允许一个可能存在空值的值,像一个正常的有效值那样工作,在编译时就能够检查出来。Rust显得更加安全,不用担心出现其他语言运行时才会出现的空指针异常的bug。例如:

letx:i8=5;//Rust没有空值(Null),因此i8只能被赋予一个有效值。lety:Optioni8=Some(5);//y可能为空,需要显示地表示为枚举类型Optionletsum=x+y;

尝试将不可能出现无效值的x:i8与可能出现无效值的y:Optioni8相加时,编译器会报错:

error[E0277]:thetraitbound`i8:std::ops::Addstd::option::Optioni8`isnotsatisfied--|5|letsum=x+y;|^noimplementationfor`i8+std::option::Optioni8`|

总结一下,如果一个值可能为空,必须使用枚举类型OptionT,否则必须赋予有效值。而为了使用OptionT,需要编写处理每个成员的代码,当T为有效值时,才能够从Some(T)中取出T的值来使用,如果T为无效值,可以进行其他的处理,通常使用match来处理这种情况。

例如,当y为有效值时,返回x和y的和;为空值时,返回x。

fnplus(x:i8,y:Optioni8)-i8{matchy{None=x,Some(i)=x+i,}}fnmain(){lety1:Optioni8=Some(5);lety2:Optioni8=None;letz1=plus(10,y1);letz2=plus(10,y2);println!("z1={},z2={}",z1,z2);//z1=15,z2=10}

iflet控制流

match还有一种简单场景,可以简写为iflet。如下,y有值时,打印和,y无值时,啥事也不做。

fnplus(x:i8,y:Optioni8){matchy{Some(i)={println!("x+y={}",x+i)},None={},}}fnmain(){lety1:Optioni8=Some(5);lety2:Optioni8=None;plus(10,y1);//x+y=15plus(10,y2);}

简写为iflet,则是

fnplus(x:i8,y:Optioni8){ifletSome(i)=y{println!("x+y={}",x+i);}}

如果只使用if呢?

fnplus(x:i8,y:Optioni8){_some(){leti=();//获得Some中的T值。println!("x+y={}",x+i);}}

iflet语句也可以包含else。

fnplus(x:i8,y:Optioni8){ifletSome(i)=y{println!("x+y={}",x+i);}else{println!("yisNone");}}//等价于fnplus(x:i8,y:Optioni8){matchy{Some(i)={println!("x+y={}",x+i)},None={println!("yisNone")},}}
3.3实现方法和接口(impltraits)

实现方法(impl)

structRectangle{width:u32,height:u32,}implRectangle{fnarea(self)-u32{*}}implRectangle{fncan_hold(self,other:Rectangle)-bool{}}fnmain(){letrect1=Rectangle{width:30,height:50};println!("Theareaoftherectangleis{}squarepixels.",());}

关联函数(associatedfunctions)

关联函数不以self作为参数,关联函数之所以成为函数而不是方法,是因为关联函数并不作用于一个结构体的实例。我们之前创建字符串类型时,使用过的String::from就是关联函数。关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以提供一个关联函数,它接受一个维度参数并且同时作为宽和高,这样可以更轻松的创建一个正方形Rectangle而不必指定两次同样的值:

implRectangle{fnsquare(size:u32)-Rectangle{Rectangle{width:size,height:size}}}letrect2=Rectangle::square(10);

实现接口(traits)

traitSummary{fnsummarize(self)-String;}implSummaryforRectangle{fnsummarize(self)-String{format!("{width={},height={}}",,)}}//接口也支持继承traitPerson{fnfull_name(self)-String;}traitEmployee:Person{//Employeeinheritfrompersontraitfnjob_title(self)-String;}traitExpat{fnsalary(self)-f32}traitExpatEmployee:Employee+Expat{//多继承,同时继承Employee和Expatfnadditional_tax(self)-f64;}
3.3泛型(Generics)

当我们实现一个函数或者数据结构时,往往希望参数可以支持不同的类型,泛型可以解决这个问题。声明参数类型时,换成大写的字母,例如字母T,同时使用T告诉编译器T是泛型。

函数中使用泛型

fnlargestT(list:[T])-T{letmutlargest=list[0];(){ifitemlargest{largest=item;}}largest}fnmain(){letnumber_list=vec![34,50,25,100,65];letresult=largest(number_list);println!("Thelargestnumberis{}",result);letchar_list=vec!['y','m','a','q'];letresult=largest(char_list);println!("Thelargestcharis{}",result);}

结构体使用泛型

structPointT{x:T,y:T,}fnmain(){letinteger=Point{x:5,y:10};letfloat=Point{x:1.0,y:4.0};}

枚举使用泛型

enumOptionT{Some(T),None,}enumResultT,E{Ok(T),Err(E),}

Result枚举有两个泛型类型,T和E。Result有两个成员:Ok,它存放一个类型T的值,而Err则存放一个类型E的值。这个定义使得Result枚举能很方便的表达任何可能成功(返回T类型的值)也可能失败(返回E类型的值)的操作。回忆一下示例9-3中打开一个文件的场景:当文件被成功打开T被放入了std::fs::File类型而当打开文件出现问题时E被放入了std::io::Error类型。

方法中使用泛型

structPointT{x:T,y:T,}implTPointT{fnx(self)-T{}}fnmain(){letp=Point{x:5,y:10};println!("={}",());}
3.4常见集合Vec

新建

letv:Veci32=Vec::new();//空集合//letv=vec![1,2,3];//含初始值的集合,vec!是为方便初始化Vec提供的宏。println!("第三个元素{}",v[2]);//3println!("第100个元素{}",v[100]);//panicerrorassert_eq!((2),Some(3));assert_eq!((100),None);

(2)和v[2]都能获取到Vec的值,区别在于v[2]返回的是该元素的引用,引用一个不存在的位置,会引发错误。(2)返回的是枚举类型OptionT。(2)返回的是Some(3),(100)返回的是None。

更新

letv:Veci32=Vec::new();(5);(6);(7);(8);()//删除最后一个元素

遍历

letv=vec![100,32,57];foriinv{println!("{}",i);}letmutv2=vec![100,32,57];foriinmutv2{*i+=50;}

iflet控制流

如果我们想修改Vec中第2个元素的值呢?可以这么写:

fnmain(){letmutv=vec![1,2,3,4,5];{letthird=_mut(2).unwrap();*third+=50;}println!("v={:?}",v);//v=[1,2,53,4,5]}

因为_mut()的返回值是OptionT枚举类型,那么可以使用iflet来简化代码。

fnmain(){letmutv=vec![1,2,3,4,5];ifletSome(third)=_mut(2){*third+=50;}println!("v={:?}",v);//v=[1,2,53,4,5]}

whilelet控制流

iflet可以用于单个元素的场景,whilelet就适用于遍历的场景了。

letmutstack=vec![1,2,3,4,5];whileletSome(top)=(){println!("{}",top);//依次打印54321}
3.5常见集合String

Rust的核心语言中只有一种字符串类型:str,字符串切片,它通常以被借用的形式出现,str。这里提到的字符串,是字节的集合,外加一些常用方法实现。因为是集合,增持增删改,长度也可变。

新建

letmuts1=String::new();lets2="initialcontents".to_string();lets3=String::new();

更新

letmuts=String::from("foo");_str("bar");//附加字符串('!')//附加单字符assert_eq!((0),'f');//删除某个位置的字符lets1=String::from("Hello,");lets2=String::from("world!");lets3=s1+s2;

format

lets1=String::from("tic");lets2=String::from("tac");lets3=String::from("toe");lets=format!("{}-{}-{}",s1,s2,s3);println!("{}",s);//tic-tac-toe

索引

letv=String::from("hello");assert_eq!(Some('h'),().nth(0));

遍历

letv=String::from("hello");(){println!("{}",c);}

在Rust内部,String是一个Vecu8的封装,但是有些字符可能会占用超过2个字符,所以String不支持直接索引,如果需要索引需要使用chars()转换后再使用。

3.6常见集合HashMap

新建

usestd::collections::HashMap;letmutscores=HashMap::new();(String::from("Blue"),10);(String::from("Yellow"),50);

这里使用了use引入了HashMap结构体。

访问

usestd::collections::HashMap;letmutscores=HashMap::new();(String::from("Blue"),10);(String::from("Yellow"),50);letteam_name=String::from("Blue");letscore=(team_name);

更新

usestd::collections::HashMap;letmutscores=HashMap::new();(String::from("Blue"),10);//10(String::from("Blue"),25);//25//Blue存在则不更新,不存在则更新,因此scores['Blue']仍为25(String::from("Blue")).or_insert(50);
4错误处理4.1不可恢复错误panic!

Rust有panic!宏。当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出。出现这种场景,一般是出现了一些不知如何处理的场景。

直接调用

fnmain(){panic!("crashandburn");}

执行cargorun将打印出

$(/xxx/demo/tutu)Finisheddev[unoptimized+debuginfo]target(s)`target/debug/tutu`thread'main'panickedat'crashandburn',src/:2:5note:runwith`RUST_BACKTRACE=1`environmentvariabletodisplayabacktrace.

最后2行包含了panic!导致的报错信息,第1行是源码中panic!出现的位置src/:2:5

代码bug引起的错误

fnmain(){letv=vec![1,2,3];v[99];//越界}

和之前一样,cargorun的报错信息只有2行,缺少函数的调用栈,为了便于定位问题,可以设置RUST_BACKTRACE环境变量来获得更多的调用栈信息,Rust中称之为backtrace。通过backtrace,可以看到执行到目前位置所有被调用的函数的列表。

例如执行RUST_BACKTRACE=1cargorun,这种方式的好处在于,环境变量只作用于当前命令。

$RUST_BACKTRACE=1cargorunFinisheddev[unoptimized+debuginfo]target(s)`target/debug/tutu`thread'main'panickedat'indexoutofbounds:thelenis3buttheindexis99',/rustc/xxx/src/libcore/slice/:2717:10stackbacktrace:0:backtrace::backtrace::libunwind::traceat/cargo/registry/src///src/backtrace/:8817:alloc::vec::VecTascore::ops::index::IndexI::indexat/rustc/xxx/src/liballoc/:179618:tutu::mainatsrc/:4note:Somedetailsareomitted,runwith`RUST_BACKTRACE=full`foraverbosebacktrace.

第一行的报错信息,说明了错误的原因,长度越界。紧接着打印出了函数调用栈,src/:4-liballoc/:1796-…

在windows下,可以执行setRUST_BACKTRACE=1cargorun。

release

当出现panic时,程序默认会开始展开(unwinding),这意味着Rust会回溯栈并清理它遇到的每一个函数的数据,不过这个回溯并清理的过程有很多工作。另一种选择是直接终止(abort),这会不清理数据就退出程序。那么程序所使用的内存需要由操作系统来清理。

release模式下,希望程序越小越好,可以在中设置panic为abort。

[]panic="abort"
4.2可恢复错误Result

处理Result

有些错误,希望能够捕获并且做相应的处理,Rust提供了Result机制来处理可恢复错误,类似于其他语言中的trycatch。

这是ResultT,E的定义

enumResultT,E{Ok(T),Err(E),}

有些函数会返回Result,那怎么知道一个函数的返回对象是不是Result呢?很简单!

fnmain(){letf:u32=File::create("");}

当我们编译上面的代码时,将会报错。

=note:expectedtype`u32`foundtype`std::result::Resultstd::fs::File,std::io::Error`

从报错信息可以看出,File::create返回的是一个Resultfs::::Error对象,如果没有异常,我们可以从Result::OkT获取到文件句柄。

下面是一个完整的示例,创建文件,并尝试写入“Hello,world!”。

usestd::fs::File;usestd::io::prelude::*;fnmain(){letf=File::create("");letmutfile=matchf{Ok(file)=file,Err(error)={panic!("Problemcreatethefile:{:?}",error)},};_all(b"Hello,world!"){Ok(())={},Err(error)={panic!("Failedtowrite:{:?}",error)}};}

如果执行成功,可以看到在工程根目录下,多出了文件。

unwrap和expect

Result的处理有时太过于繁琐,Rust提供了一种简洁的处理方式unwrap。即,如果成功,直接返回Result::OkT中的值,如果失败,则直接调用!panic,程序结束。

letf=File::open("").unwrap();//若成功,f则被赋值为文件句柄,失败则结束。

expect是更人性化的处理方式,允许在调用!panic时,返回自定义的提示信息,对调试很有帮助。

letf=File::open("").expect("");

返回Result

我们可以实现类似于File::open这样的函数,让调用者能够自主绝对如何处理成功/失败的场景。

usestd::io;usestd::io::Read;usestd::fs::File;fnread_username_from_file()-ResultString,io::Error{letf=File::open("");letmutf=matchf{Ok(file)=file,Err(e)=returnErr(e),};letmuts=String::new();_to_string(muts){Ok(_)=Ok(s),Err(e)=Err(e),}}

上面的函数如果成功,则返回的文本字符串,失败,则返回io::Error。

更简单的实现方式

usestd::io;usestd::io::Read;usestd::fs::File;fnread_username_from_file()-ResultString,io::Error{letmutf=File::open("")?;letmuts=String::new();_to_string(muts)?;Ok(s)}

这种写法使用了?运算符向调用者返回错误。

作用是:如果Result的值是Ok,则该表达式返回Ok中的值且程序继续执行。如果值是Error,则将Error的值作为整个函数的返回值,好像使用了return关键字一样。这样写,逻辑更为清晰。

5包、crate和模块5.1包和crate
.├──├──├──benches│└──├──examples│└──├──src│├──bin││└──another_│├──│└──└──tests└──

一个Cargo项目即一个包(Package),一个包至少包含一个crate;可以包含零个或多个二进制crate(binarycrate),但只能包含一个库crate(librarycrate)。src/是与包名同名的二进制crate的根,其他的二进制crate的根放置在src/bin目录下;src/是与包名同名的库crate的根。

5.2模块

模块让我们可以将一个crate中的代码进行分组,以提高可读性与重用性。即项是可以被外部代码使用的(public),还是作为一个内部实现的内容,不能被外部代码使用(private)。

声明模块

Rust中使用mod来声明模块,模块允许嵌套,可以使用模块名作为路径使用,例如:

//src/{modbasic{fnplus(x:i32,y:i32)-i32{x+y}fnmul(x:i32,y:i32)-i32{x*y}}}fnmain(){println!("2+3={}",math::basic::plus(2,3));println!("2*3={}",math::basic::mul(2,3));}

引入作用域

使用use可以将路径引入作用域。

我们在src/中声明一个模块,在src/中调用。

//src/{pubfnhello(name:str){println!("Hello,{}",name)}//pub才能外部可见}
//src/;fnmain(){tutu::greeting::hello("Jack");//Hello,Jack}

路径的长度可以自由定义,也可以写成

//src/::greeting;fnmain(){greeting::hello("Jack");}

src/和src/属于不同的crate,所以引入作用域时,需要带上包名tutu。

分隔模块

在src/中可以使用mod声明多个模块,但有时为了可读性,习惯将每个模块写在独立的文件中。

新建src/,写入

//src/(name:str){println!("Hello,{}",name)}

在src/可以这样使用,

//src/;pubfnfunc(){greeting::hello("Tom");}

关键点就在于modgreeting;这一行,modgreeting后面没有具体实现,而是紧跟分号,则声明greeting的模块内容位于src/中。

其他crate,例如src/中的使用方式没有任何变化。

//src/::greeting;fnmain(){greeting::hello("Jack");}
6测试6.1单元测试(unittests)
fnplus(x:i32,y:i32)-i32{x+y}fnmain(){letx=10;lety=20;println!("{}+{}={}",x,y,plus(x,y))}[test]即可,通过cargotest执行用例。
$cargotestrunning1testtestit_worksoktestresult:;0failed;0ignored;0measured;0filteredout

更规范的写法是在每个源文件中,创建包含测试函数的tests模块,测试用例写在tests模块中,并使用cfg(test)标注模块。

fnplus(x:i32,y:i32)-i32{x+y}fnmain(){println!("2+3={}",plus(2,3));}[test]fnit_works(){assert_eq!(4,plus(2,2),);}}

因为内部测试的代码和源码在同一文件,因而需要使用[test]fnit_works(){assert_eq!(4,tutu::plus(2,2));}

运行cargotest,将输出

2running1testtestit_adds_twooktestresult:;0failed;0ignored;0measured;0filteredout
参考

Rust官方指南

Rust官方文档

Cargo官方文档

发布于 2025-04-02
44
目录

    推荐阅读