使用Rust的编译器rustc可以从源程序生成可执行文件:
$rustchello.rs使用rustc编译后将得到可执行文件hello。
这些宏都以相同的做法解析文本。有个额外优点是格式化的正确性会在编译时检查。
上例使用了fmt::Display,因为标准库提供了那些类型的实现。若要打印自定义类型的文本,需要更多的步骤。
所有的类型,若想用std::fmt的格式化打印,都要求实现至少一个可打印的traits。仅有一些类型提供了自动实现,比如std库中的类型。所有其他类型都必须手动实现。
fmt::Debug这个trait使这项工作变得相当简单。所有类型都能推导(derive,即自动创建)fmt::Debug的实现。但是fmt::Display需要手动实现。
#![allow(unused)]fnmain(){//这个结构体不能使用`fmt::Display`或`fmt::Debug`来进行打印。structUnPrintable(i32);//`derive`属性会自动创建所需的实现,使这个`struct`能使用`fmt::Debug`打印。#[derive(Debug)]structDebugPrintable(i32);}所有std库类型都天生可以使用{:}来打印:
//推导`Structure`的`fmt::Debug`实现。//`Structure`是一个包含单个`i32`的结构体。#[derive(Debug)]structStructure(i32);//将`Structure`放到结构体`Deep`中。然后使`Deep`也能够打印。#[derive(Debug)]structDeep(Structure);fnmain(){//使用`{:}`打印和使用`{}`类似。println!("{:}monthsinayear.",12);println!("{1:}{0:}isthe{actor:}name.","Slater","Christian",actor="actor's");//`Structure`也可以打印!println!("Now{:}willprint!",Structure(3));//使用`derive`的一个问题是不能控制输出的形式。//假如我只想展示一个`7`怎么办?println!("Now{:}willprint!",Deep(Structure(7)));}所以fmt::Debug确实使这些内容可以打印,但是牺牲了一些美感。Rust也通过{:#}提供了“美化打印”的功能:
#[derive(Debug)]structPerson<'a>{name:&'astr,age:u8}fnmain(){letname="Peter";letage=27;letpeter=Person{name,age};//美化打印println!("{:#}",peter);}你可以通过手动实现fmt::Display来控制显示效果。
#![allow(unused)]fnmain(){//(使用`use`)导入`fmt`模块使`fmt::Display`可用usestd::fmt;//定义一个结构体,咱们会为它实现`fmt::Display`。以下是个简单的元组结构体//`Structure`,包含一个`i32`元素。structStructure(i32);//为了使用`{}`标记,必须手动为类型实现`fmt::Display`trait。implfmt::DisplayforStructure{//这个trait要求`fmt`使用与下面的函数完全一致的函数签名fnfmt(&self,f:&mutfmt::Formatter)->fmt::Result{//仅将self的第一个元素写入到给定的输出流`f`。返回`fmt:Result`,此//结果表明操作成功或失败。注意`write!`的用法和`println!`很相似。write!(f,"{}",self.0)}}}fmt::Display的效果可能比fmt::Debug简洁,但对于std库来说,这就有一个问题。模棱两可的类型该如何显示呢?举个例子,假设标准库对所有的Vec
我们没有这样做,因为没有一种合适的样式适用于所有类型,标准库也并不擅自规定一种样式。对于Vec
这并不是一个问题,因为对于任何非泛型的容器类型,fmt::Display都能够实现。
检验上面例子的输出,然后在示例程序中,仿照Point2D结构体增加一个复数结构体。使用一样的方式打印,输出结果要求是这个样子:
对一个结构体实现fmt::Display,其中的元素需要一个接一个地处理到,这可能会很麻烦。问题在于每个write!都要生成一个fmt::Result。正确的实现需要处理所有的Result。Rust专门为解决这个问题提供了操作符。
在write!上使用会像是这样:
//对`write!`进行尝试(try),观察是否出错。若发生错误,返回相应的错误。//否则(没有出错)继续执行后面的语句。write!(f,"{}",value);另外,你也可以使用try!宏,它和是一样的。这种写法比较罗嗦,故不再推荐,但在老一些的Rust代码中仍会看到。使用try!看起来像这样:
try!(write!(f,"{}",value));有了,对一个Vec实现fmt::Display就很简单了:
我们已经看到,格式化的方式是通过格式字符串来指定的:
根据使用的参数类型是X、o还是未指定,同样的变量(foo)能够格式化成不同的形式。
这个格式化的功能是通过trait实现的,每种参数类型都对应一种trait。最常见的格式化trait就是Display,它可以处理参数类型为未指定的情况,比如{}。
为上面的Color结构体实现fmt::Display,应得到如下的输出结果:
RGB(128,255,90)0x80FF5ARGB(0,3,254)0x0003FERGB(0,0,0)0x000000如果感到疑惑,可看下面两条提示:
Rust提供了多种原生类型(primitives),包括:
尽管单元类型的值是个元组,它却并不被认为是复合类型,因为并不包含多个值。
整数1、浮点数1.2、字符'a'、字符串"abc"、布尔值true和单元类型()可以用数字、文字或符号之类的“字面量”(literal)来表示。
另外,通过加前缀0x、0o、0b,数字可以用十六进制、八进制或二进制记法表示。
为了改善可读性,可以在数值字面量中插入下划线,比如:1_000等同于1000,0.000_001等同于0.000001。
我们需要把字面量的类型告诉编译器。如前面学过的,我们使用u32后缀来表明字面量是一个32位无符号整数,i32后缀表明字面量是一个32位有符号整数。
切片(slice)类型和数组类似,但其大小在编译时是不确定的。相反,切片是一个双字对象(two-wordobject),第一个字是一个指向数据的指针,第二个字是切片的长度。这个“字”的宽度和usize相同,由处理器架构决定,比如在x86-64平台上就是64位。slice可以用来借用数组的一部分。slice的类型标记为&[T]。
而常量(constant)可以通过const和static关键字来创建。
结构体(structure,缩写成struct)有3种类型,使用struct关键字来创建:
enum关键字允许创建一个从数个不同取值中选其一的枚举类型(enumeration)。任何一个在struct中合法的取值在enum中也合法。
enumVeryVerboseEnumOfThingsToDoWithNumbers{Add,Subtract,}//创建一个类型别名typeOperations=VeryVerboseEnumOfThingsToDoWithNumbers;fnmain(){//我们可以通过别名引用每个枚举变量,避免使用又长又不方便的枚举名字letx=Operations::Add;}最常见的情况就是在impl块中使用Self别名。
enum也可以像C语言风格的枚举类型那样使用。
enum的一个常见用法就是创建链表(linked-list):
使用let绑定操作可以将值(比如字面量)绑定(bind)到变量。
fnmain(){let_immutable_binding=1;letmutmutable_binding=1;println!("Beforemutation:{}",mutable_binding);//正确代码mutable_binding+=1;println!("Aftermutation:{}",mutable_binding);//错误!_immutable_binding+=1;//改正^将此行注释掉}编译器会给出关于变量可变性的详细诊断信息。
当数据被相同的名称不变地绑定时,它还会冻结(freeze)。在不可变绑定超出作用域之前,无法修改已冻结的数据:
Rust不提供原生类型之间的隐式类型转换(coercion),但可以使用as关键字进行显式类型转换(casting)。
整型之间的转换大体遵循C语言的惯例,除了C会产生未定义行为的情形。在Rust中所有整型转换都是定义良好的。
无后缀的数值字面量,其类型取决于怎样使用它们。如果没有限制,编译器会对整数使用i32,对浮点数使用f64。
fnmain(){//带后缀的字面量,其类型在初始化时已经知道了。letx=1u8;lety=2u32;letz=3f32;//无后缀的字面量,其类型取决于如何使用它们。leti=1;letf=1.0;//`size_of_val`返回一个变量所占的字节数println!("sizeof`x`inbytes:{}",std::mem::size_of_val(&x));println!("sizeof`y`inbytes:{}",std::mem::size_of_val(&y));println!("sizeof`z`inbytes:{}",std::mem::size_of_val(&z));println!("sizeof`i`inbytes:{}",std::mem::size_of_val(&i));println!("sizeof`f`inbytes:{}",std::mem::size_of_val(&f));}上面的代码使用了一些还没有讨论过的概念。心急的读者可以看看下面的简短解释:
fnmain(){//因为有类型说明,编译器知道`elem`的类型是u8。letelem=5u8;//创建一个空向量(vector,即不定长的,可以增长的数组)。letmutvec=Vec::new();//现在编译器还不知道`vec`的具体类型,只知道它是某种东西构成的向量(`Vec<_>`)//在向量中插入`elem`。vec.push(elem);//啊哈!现在编译器知道`vec`是u8的向量了(`Vec
可以用type语句给已有的类型取个新的名字。类型的名字必须遵循驼峰命名法(像是CamelCase这样),否则编译器将给出警告。原生类型是例外,比如:usize、f32,等等。
//`NanoSecond`是`u64`的新名字。typeNanoSecond=u64;typeInch=u64;//通过这个属性屏蔽警告。#[allow(non_camel_case_types)]typeu64_t=u64;//试一试^移除上面那个属性fnmain(){//`NanoSecond`=`Inch`=`u64_t`=`u64`.letnanoseconds:NanoSecond=5asu64_t;letinches:Inch=2asu64_t;//注意类型别名*并不能*提供额外的类型安全,因为别名*并不是*新的类型。println!("{}nanoseconds+{}inches={}unit",nanoseconds,inches,nanoseconds+inches);}别名的主要用途是避免写出冗长的模板化代码(boilerplatecode)。如IoResult
比如,可以很容易地把str转换成String:
#![allow(unused)]fnmain(){letmy_str="hello";letmy_string=String::from(my_str);}也可以为我们自己的类型定义转换机制:
使用Intotrait通常要求指明要转换到的类型,因为编译器大多数时候不能推断它。不过考虑到我们免费获得了Into,这点代价不值一提。
usestd::fmt;structCircle{radius:i32}implfmt::DisplayforCircle{fnfmt(&self,f:&mutfmt::Formatter)->fmt::Result{write!(f,"Circleofradius{}",self.radius)}}fnmain(){letcircle=Circle{radius:6};println!("{}",circle.to_string());}译注:一个实现ToString的例子
fnmain(){//变量绑定letx=5;//表达式;x;x+1;15;}代码块也是表达式,所以它们可以用作赋值中的值。代码块中的最后一个表达式将赋给适当的表达式,例如局部变量。但是,如果代码块的最后一个表达式结尾处有分号,则返回值为()(译注:代码块中的最后一个语句是代码块中实际执行的最后一个语句,而不一定是代码块中最后一行的语句)。
if-else分支判断和其他语言类似。不同的是,Rust语言中的布尔判断条件不必使用小括号包裹,且每个条件后面都跟着一个代码块。if-else条件选择是一个表达式,并且所有分支都必须返回相同的类型。
可以使用break语句在任何时候退出一个循环,还可以使用continue跳过循环体的剩余部分并开始下一轮循环。
让我们使用for代替while来写FizzBuzz程序。
fnmain(){//`n`将在每次迭代中分别取1,2,...,100fornin1..101{ifn%15==0{println!("fizzbuzz");}elseifn%3==0{println!("fizz");}elseifn%5==0{println!("buzz");}else{println!("{}",n);}}}或者,可以使用a..=b表示两端都包含在内的范围。上面的代码可以写成:
这三个函数会以不同的方式返回集合中的数据。
fnmain(){letnames=vec!["Bob","Frank","Ferris"];fornameinnames.into_iter(){matchname{"Ferris"=>println!("Thereisarustaceanamongus!"),_=>println!("Hello{}",name),}}}fnmain(){letmutnames=vec!["Bob","Frank","Ferris"];fornameinnames.iter_mut(){*name=matchname{&mut"Ferris"=>"Thereisarustaceanamongus!",_=>"Hello",}}println!("names:{:}",names);}在上面这些代码中,注意match的分支中所写的类型不同,这是不同迭代方式的关键区别。因为类型不同,能够执行的操作当然也不同。
Rust通过match关键字来提供模式匹配,和C语言的switch用法类似。第一个匹配分支会被比对,并且所有可能的值都必须被覆盖。
元组可以在match中解构,如下所示:
和前面相似,解构enum的方式如下:
对指针来说,解构(destructure)和解引用(dereference)要区分开,因为这两者的概念是不同的,和C那样的语言用法不一样。
类似地,解构struct如下所示:
可以加上match卫语句(guard)来过滤分支。
在match中,若间接地访问一个变量,则不经过重新绑定就无法在分支中再使用它。match提供了@符号来绑定变量到名称:
//`age`函数,返回一个`u32`值。fnage()->u32{15}fnmain(){println!("Tellmewhattypeofpersonyouare");matchage(){0=>println!("Ihaven'tcelebratedmyfirstbirthdayyet"),//可以直接匹配(`match`)1..=12,但那样的话孩子会是几岁?//相反,在1..=12分支中绑定匹配值到`n`。现在年龄就可以读取了。n@1..=12=>println!("I'machildofage{:}",n),n@13..=19=>println!("I'mateenofage{:}",n),//不符合上面的范围。返回结果。n=>println!("I'manoldpersonofage{:}",n),}}你也可以使用绑定来“解构”enum变体,例如Option:
在一些场合下,用match匹配枚举类型并不优雅。比如:
#![allow(unused)]fnmain(){//将`optional`定为`Option
fnmain(){//全部都是`Option
//以这个enum类型为例enumFoo{Bar,Baz,Qux(u32)}fnmain(){//创建变量leta=Foo::Bar;letb=Foo::Baz;letc=Foo::Qux(100);//变量a匹配到了Foo::BarifletFoo::Bar=a{println!("aisfoobar");}//变量b没有匹配到Foo::Bar,因此什么也不会打印。ifletFoo::Bar=b{println!("bisfoobar");}//变量c匹配到了Foo::Qux,它带有一个值,就和上面例子中的Some()类似。ifletFoo::Qux(value)=c{println!("cis{}",value);}}另一个好处是:iflet允许匹配枚举非参数化的变量,即枚举未注明#[derive(PartialEq)],我们也没有为其实现PartialEq。在这种情况下,通常ifFoo::Bar==a会出错,因为此类枚举的实例不具有可比性。但是,iflet是可行的。
你想挑战一下吗?使用iflet修复以下示例:
和iflet类似,whilelet也可以把别扭的match改写得好看一些。考虑下面这段使i不断增加的代码:
#![allow(unused)]fnmain(){//将`optional`设为`Option
函数最后的表达式将作为返回值。也可以在函数内使用return语句来提前返一个值,甚至可以在循环或if内部使用。
让我们使用函数来重写FizzBuzz程序吧!
|val|val+x它们的语法和能力使它们在临时(onthefly)使用时相当方便。调用一个闭包和调用一个函数完全相同,不过调用闭包时,输入和返回类型两者都可以自动推导,而输入变量名必须指明。
其他的特点包括:
闭包优先通过引用来捕获变量,并且仅在需要时使用其他方式。
虽然Rust无需类型说明就能在大多数时候完成变量捕获,但在编写函数时,这种模糊写法是不允许的。当以闭包作为输入参数时,必须指出闭包的完整类型,它是通过使用以下trait中的一种来指定的。其受限制程度按以下顺序递减:
译注:顺序之所以是这样,是因为&T只是获取了不可变的引用,&mutT则可以改变变量,T则是拿到了变量的所有权而非借用。
对闭包所要捕获的每个变量,编译器都将以限制最少的方式来捕获。
译注:这句可能说得不对,事实上是在满足使用需求的前提下尽量以限制最多的方式捕获。
例如用一个类型说明为FnOnce的闭包作为参数。这说明闭包可能采取&T,&mutT或T中的一种捕获方式,但编译器最终是根据所捕获变量在闭包里的使用情况决定捕获方式。
这是因为如果能以移动的方式捕获变量,则闭包也有能力使用其他方式借用变量。注意反过来就不再成立:如果参数的类型说明是Fn,那么不允许该闭包通过&mutT或T捕获变量。
在下面的例子中,试着分别用一用Fn、FnMut和FnOnce,看看会发生什么:
#![allow(unused)]fnmain(){//`F`必须是泛型的。fnapply
若使用闭包作为函数参数,由于这个结构体的类型未知,任何的用法都要求是泛型的。然而,使用未限定类型的参数
//定义一个函数,可以接受一个由`Fn`限定的泛型`F`参数并调用它。fncall_me
闭包作为输入参数是可能的,所以返回闭包作为输出参数(outputparameter)也应该是可能的。然而返回闭包类型会有问题,因为目前Rust只支持返回具体(非泛型)的类型。按照定义,匿名的闭包的类型是未知的,所以只有使用implTrait才能返回一个闭包。
返回闭包的有效特征是:
除此之外,还必须使用move关键字,它表明所有的捕获都是通过值进行的。这是必须的,因为在函数退出时,任何通过引用的捕获都被丢弃,在闭包中留下无效的引用。
本小节列出几个标准库中使用闭包的例子。
Iterator::any是一个函数,若传给它一个迭代器(iterator),当其中任一元素满足谓词(predicate)时它将返回true,否则返回false(译注:谓词是闭包规定的,true/false是闭包作用在元素上的返回值)。它的签名如下:
Iterator::find是一个函数,在传给它一个迭代器时,将用Option类型返回第一个满足谓词的元素。它的签名如下:
Rust提供了高阶函数(HigherOrderFunction,HOF),指那些输入一个或多个函数,并且/或者产生一个更有用的函数的函数。HOF和惰性迭代器(lazyiterator)给Rust带来了函数式(functional)编程的风格。
发散函数(divergingfunction)绝不会返回。它们使用!标记,这是一个空类型。
#![allow(unused)]fnmain(){fnfoo()->!{panic!("Thiscallneverreturns.");}}和所有其他类型相反,这个类型无法实例化,因为此类型可能具有的所有可能值的集合为空。注意,它与()类型不同,后者只有一个可能的值。
如下面例子,虽然返回值中没有信息,但此函数会照常返回。
fnsome_fn(){()}fnmain(){leta:()=some_fn();println!("Thisfunctionreturnsandyoucanseethisline.")}下面这个函数相反,这个函数永远不会将控制内容返回给调用者。
#![feature(never_type)]fnmain(){letx:!=panic!("Thiscallneverreturns.");println!("Youwillneverseethisline!");}虽然这看起来像是一个抽象的概念,但实际上这非常有用且方便。这种类型的主要优点是它可以被转换为任何其他类型,从而可以在需要精确类型的地方使用,例如在match匹配分支。这允许我们编写如下代码:
fnmain(){fnsum_odd_numbers(up_to:u32)->u32{letmutacc=0;foriin0..up_to{//注意这个match表达式的返回值必须为u32,//因为“addition”变量是这个类型。letaddition:u32=matchi%2==1{//“i”变量的类型为u32,这毫无问题。true=>i,//另一方面,“continue”表达式不返回u32,但它仍然没有问题,//因为它永远不会返回,因此不会违反匹配表达式的类型要求。false=>continue,};acc+=addition;}acc}println!("Sumofoddnumbersupto9(excluding):{}",sum_odd_numbers(9));}这也是永远循环(如loop{})的函数(如网络服务器)或终止进程的函数(如exit())的返回类型。
Rust提供了一套强大的模块(module)系统,可以将代码按层次分成多个逻辑单元(模块),并管理这些模块之间的可见性(公有(public)或私有(private))。
模块是项(item)的集合,项可以是:函数,结构体,trait,impl块,甚至其它模块。
默认情况下,模块中的项拥有私有的可见性(privatevisibility),不过可以加上pub修饰语来重载这一行为。模块中只有公有的(public)项可以从模块外的作用域访问。
$tree..|--my||--inaccessible.rs||--mod.rs|`--nested.rs`--split.rssplit.rs的内容:
//类似地,`modinaccessible`和`modnested`将找到`nested.rs`和//`inaccessible.rs`文件,并在它们放到各自的模块中。modinaccessible;pubmodnested;pubfnfunction(){println!("called`my::function()`");}fnprivate_function(){println!("called`my::private_function()`");}pubfnindirect_access(){print!("called`my::indirect_access()`,that\n>");private_function();}my/nested.rs的内容:
pubfnfunction(){println!("called`my::nested::function()`");}#[allow(dead_code)]fnprivate_function(){println!("called`my::nested::private_function()`");}my/inaccessible.rs的内容:
#[allow(dead_code)]pubfnpublic_function(){println!("called`my::inaccessible::public_function()`");}我们看到代码仍然正常运行,就和前面的一样:
crate可以编译成二进制可执行文件(binary)或库文件(library)。默认情况下,rustc将从crate产生二进制可执行文件。这种行为可以通过rustc的选项--crate-type重载。
让我们创建一个库,然后看看如何把它链接到另一个crate。
要将一个crate链接到上节新建的库,可以使用rustc的--extern选项。然后将所有的物件导入到与库名相同的模块下。此模块的操作通常与任何其他模块相同。
大多数程序都会依赖于某些库。如果你曾经手动管理过库依赖,那么你就知道这会带来的极大的痛苦。幸运的是,Rust的生态链标配cargo工具!cargo可以管理项目的依赖关系。
下面创建一个新的Rust项目:
#二进制可执行文件cargonewfoo#或者库cargonew--libfoo对于本章的其余部分,我们选定创建的都是二进制可执行文件而不是库,但所有的概念都是相同的。
完成上述命令后,将看到如下内容:
foo├──Cargo.toml└──src└──main.rsmain.rs是新项目的入口源文件——这里没什么新东西。Cargo.toml是本项目(foo)的cargo的配置文件。浏览Cargo.toml文件,将看到类似以下的的内容:
[package]name="foo"version="0.1.0"authors=["mark"][dependencies]package下面的name字段表明项目的名称。如果您发布crate(后面将做更多介绍),那么crates.io将使用此字段标明的名称。这也是编译时输出的二进制可执行文件的名称。
authors字段表明发布crate时的作者列表。
dependencies这部分可以让你为项目添加依赖。
要构建我们的项目,我们可以在项目目录中的任何位置(包括子目录!)执行cargobuild。我们也可以执行cargorun来构建和运行。请注意,这些命令将处理所有依赖,在需要时下载crate,并构建所有内容,包括crate。(请注意,它只重新构建尚未构建的内容,这和make类似)。
瞧!这里的所有都和cargo有关!
在上一小节中,我们看到了以下目录层次结构:
foo├──Cargo.toml└──src└──main.rs假设我们要在同一个项目中有两个二进制可执行文件。那要怎样做呢?
很显然,cargo支持这一点。正如我们之前看到的,默认二进制名称是main,但可以通过将文件放在bin/目录中来添加其他二进制可执行文件:
foo├──Cargo.toml└──src├──main.rs└──bin└──my_other_bin.rs为了使得cargo编译或运行这个二进制可执行文件而不是默认或其他二进制可执行文件,我们只需给cargo增加一个参数--binmy_other_bin,其中my_other_bin是我们想要使用的二进制可执行文件的名称。
在下一节中,我们将更仔细地学习测试的内容。
通过上面链接的关于测试章节,我们看到了如何编写单元测试和集成测试。在代码目录组织上,我们可以将单元测试放在需要测试的模块中,并将集成测试放在源码中tests/目录中:
foo├──Cargo.toml├──src│└──main.rs└──tests├──my_test.rs└──my_other_test.rstests目录下的每个文件都是一个单独的集成测试。
cargo很自然地提供了一种便捷的方法来运行所有测试!
cargotest你将会看到像这样的输出:
$cargotestCompilingblahv0.1.0(file:///nobackup/blah)Finisheddev[unoptimized+debuginfo]target(s)in0.89secsRunningtarget/debug/deps/blah-d3b32b97275ec472running3teststesttest_bar...oktesttest_baz...oktesttest_foo_bar...oktesttest_foo...oktestresult:ok.3passed;0failed;0ignored;0measured;0filteredout你还可以运行如下测试,其中名称匹配一个模式:
cargotesttest_foo$cargotesttest_fooCompilingblahv0.1.0(file:///nobackup/blah)Finisheddev[unoptimized+debuginfo]target(s)in0.35secsRunningtarget/debug/deps/blah-d3b32b97275ec472running2teststesttest_foo...oktesttest_foo_bar...oktestresult:ok.2passed;0failed;0ignored;0measured;2filteredout需要注意的一点是:cargo可能同时进行多项测试,因此请确保它们不会相互竞争。例如,如果它们都输出到文件,则应该将它们写入不同的文件。
有时使用cargo正常构建还是不够的。也许你的crate在cargo成功编译之前需要一些先决条件,比如代码生成或者需要编译的一些本地代码。为了解决这个问题,我们构建了cargo可以运行的脚本。
要向包中添加构建脚本,可以在Cargo.toml中指定它,如下所示:
[package]...build="build.rs"跟默认情况不同,这里cargo将在项目目录中优先查找build.rs文件。(本句采用意译,英文原文为:OtherwiseCargowilllookforabuild.rsfileintheprojectdirectorybydefault.)
构建脚本只是另一个Rust文件,此文件将在编译包中的任何其他内容之前,优先进行编译和调用。因此,此文件可实现满足crate的先决条件。
此脚本通过stdout(标准输出)提供输出。打印的所有行都写入到target/debug/build/
属性是应用于某些模块、crate或项的元数据(metadata)。这元数据可以用来:
当属性作用于整个crate时,它们的语法为#![crate_attribute],当它们用于模块或项时,语法为#[item_attribute](注意少了感叹号!)。
属性可以接受参数,有不同的语法形式:
属性可以多个值,它们可以分开到多行中:
fnused_function(){}//`#[allow(dead_code)]`属性可以禁用`dead_code`lint#[allow(dead_code)]fnunused_function(){}fnnoisy_unused_function(){}//改正^增加一个属性来消除警告fnmain(){used_function();}注意在实际程序中,需要将死代码清除掉。由于本书的例子是交互性的,因而其中需要允许一些死代码的出现。
crate_type属性可以告知编译器crate是一个二进制的可执行文件还是一个库(甚至是哪种类型的库),crate_name属性可以设定crate的名称。
不过,一定要注意在使用cargo时,这两种类型时都没有作用。由于大多数Rust工程都使用cargo,这意味着crate_type和crate_name的作用事实上很有限。
//这个crate是一个库文件#![crate_type="lib"]//库的名称为“rary”#![crate_name="rary"]pubfnpublic_function(){println!("calledrary's`public_function()`");}fnprivate_function(){println!("calledrary's`private_function()`");}pubfnindirect_access(){print!("calledrary's`indirect_access()`,that\n>");private_function();}当用到crate_type属性时,就不再需要给rustc命令加上--crate-type标记。
两种形式使用的参数语法都相同。
有部分条件如target_os是由rustc隐式地提供的,但是自定义条件必须使用--cfg标记来传给rustc。
#[cfg(some_condition)]fnconditional_function(){println!("conditionmet!")}fnmain(){conditional_function();}试试不使用自定义的cfg标记会发生什么:
$rustccustom.rs&&./customNosuchfileordirectory(oserror2)使用自定义的cfg标记:
译注:定义泛型类型或泛型函数之类的东西时,我们会用或者
例如定义一个名为foo的泛型函数,它可接受类型为T的任何参数arg:
fnfoo
下面例子展示了泛型语法的使用:
同样的规则也适用于函数:在使用类型T前给出
调用泛型函数有时需要显式地指明类型参量。这可能是因为调用了返回类型是泛型的函数,或者编译器没有足够的信息来推断类型参数。
调用函数时,使用显式指定的类型参数会像是这样:fun::()。
和函数类似,impl块要想实现泛型,也需要很仔细。
当然trait也可以是泛型的。我们在这里定义了一个trait,它把Droptrait作为泛型方法实现了,可以drop(丢弃)调用者本身和一个输入参数。
在使用泛型时,类型参数常常必须使用trait作为约束(bound)来明确规定类型应实现哪些功能。例如下面的例子用到了Displaytrait来打印,所以它用Display来约束T,也就是说T必须实现Display。
//定义一个函数`printer`,接受一个类型为泛型`T`的参数,//其中`T`必须实现`Display`trait。fnprinter
structS
约束的工作机制会产生这样的效果:即使一个trait不包含任何功能,你仍然可以用它作为约束。标准库中的Eq和Ord就是这样的trait。
多重约束(multiplebounds)可以用+连接。和平常一样,类型之间使用,隔开。
约束也可以使用where分句来表达,它放在{的前面,而不需写在类型第一次出现之前。另外where从句可以用于任意类型的限定,而不局限于类型参数本身。
where在下面一些情况下很有用:
newtype惯用法(译注:即为不同种类的数据分别定义新的类型)能保证在编译时,提供给程序的都是正确的类型。
比如说,实现一个“年龄认证”函数,它要求输入必须是Years类型。
structYears(i64);structDays(i64);implYears{pubfnto_days(&self)->Days{Days(self.0*365)}}implDays{///舍去不满一年的部分pubfnto_years(&self)->Years{Years(self.0/365)}}fnold_enough(age:&Years)->bool{age.0>=18}fnmain(){letage=Years(5);letage_days=age.to_days();println!("Oldenough{}",old_enough(&age));println!("Oldenough{}",old_enough(&age_days.to_years()));//println!("Oldenough{}",old_enough(&age_days));}取消最后一行的注释,就可以发现提供给old_enough的必须是Years类型。
一个这样的项就叫做一个关联类型。当trait对于实现了它的容器类型是泛型的,关联项就提供了简单的使用方法。
译注:“关联项”这个说法实际上只在RFC里出现了,官方的《TheRustProgrammingLanguage》第一版和第二版都只有“关联类型”的说法。如果觉得这里的说法很别扭的话不要理会就是了。TRPL对关联类型的定义是:“一种将类型占位符与trait联系起来的做法,这样trait中的方法签名中就可以使用这些占位符类型。trait的实现会指定在该实现中那些占位符对应什么具体类型。”等看完这一节再回头看这个定义就很明白了。
trait如果对实现了它的容器类型是泛型的,则须遵守类型规范要求——trait的使用者必须指出trait的全部泛型类型。
在下面例子中,Containstrait允许使用泛型类型A和B。然后我们为Container类型实现了这个trait,将A和B指定为i32,这样就可以对它们使用difference()函数。
因为Contains是泛型的,我们必须在fndifference()中显式地指出所有的泛型类型。但实际上,我们想要表达,A和B究竟是什么类型是由输入C决定的。在下一节会看到,关联类型恰好提供了这样的功能。
通过把容器内部的类型放到trait中作为输出类型,使用“关联类型”增加了代码的可读性。这样的trait的定义语法如下:
#![allow(unused)]fnmain(){//`A`和`B`在trait里面通过`type`关键字来定义。//(注意:此处的`type`不同于为类型取别名时的`type`)。traitContains{typeA;typeB; //这种语法能够泛型地表示这些新类型。fncontains(&self,_:&Self::A,_:&Self::B)->bool;}}注意使用了Containstrait的函数就不需要写出A或B了:
//不使用关联类型fndifference(container:&C)->i32whereC:Contains{...}//使用关联类型fndifference
可以用额外的泛型类型参数指定数据类型,该类型可以充当标记,也可以供编译时类型检查使用。这些额外的参数没有存储值,也没有运行时行为。
通过实现一个带虚类型参数的Addtrait可以实现单位检查。这种Addtrait的代码如下:
//这个`trait`会要求`Self+RHS=Output`。`
作用域在所有权(ownership)、借用(borrow)和生命周期(lifetime)中起着重要作用。也就是说,作用域告诉编译器什么时候借用是合法的、什么时候资源可以释放、以及变量何时被创建或销毁。
这种行为避免了资源泄漏(resourceleak),所以你再也不用手动释放内存或者担心内存泄漏(memoryleak)!下面是个快速入门示例:
在进行赋值(letx=y)或通过值来传递函数参数(foo(x))的时候,资源的所有权(ownership)会发生转移。按照Rust的说法,这被称为资源的移动(move)。
在移动资源之后,原来的所有者不能再被使用,这可避免悬挂指针(danglingpointer)的产生。
多数情况下,我们更希望能访问数据,同时不取得其所有权。为实现这点,Rust使用了借用(borrowing)机制。对象可以通过引用(&T)来传递,从而取代通过值(T)来传递。
编译器(通过借用检查)静态地保证了引用总是指向有效的对象。也就是说,当存在引用指向一个对象时,该对象不能被销毁。
在下面的例子和本章节剩下的内容里,我们将看到生命周期和作用域的联系与区别。
译注:如果代码中的生命周期示意图乱掉了,请把它复制到任何编辑器中,用等宽字体查看。为避免中文的显示问题,下面一些注释没有翻译。
//下面使用连线来标注各个变量的创建和销毁,从而显示出生命周期。//`i`的生命周期最长,因为它的作用域完全覆盖了`borrow1`和//`borrow2`的。`borrow1`和`borrow2`的周期没有关联,//因为它们各不相交。fnmain(){leti=3;//Lifetimefor`i`starts.────────────────┐//│{//│letborrow1=&i;//`borrow1`lifetimestarts.──┐│//││println!("borrow1:{}",borrow1);//││}//`borrow1ends.──────────────────────────────────┘│//│//│{//│letborrow2=&i;//`borrow2`lifetimestarts.──┐│//││println!("borrow2:{}",borrow2);//││}//`borrow2`ends.─────────────────────────────────┘│//│}//Lifetimeends.─────────────────────────────────────┘注意到这里没有用到名称或类型来标注生命周期,这限制了生命周期的用法,在后面我们将会看到生命周期更强大的功能。
foo<'a,'b>//`foo`带有生命周期参数`'a`和`'b`在上面这种情形中,foo的生命周期不能超出'a和'b中任一个的周期。
看下面的例子,了解显式生命周期标注的运用:
另外要注意,如果没有输入的函数返回引用,有时会导致返回的引用指向无效数据,这种情况下禁止它返回这样的引用。下面例子展示了一些合法的带有生命周期的函数:
方法的标注和函数类似:
在结构体中标注生命周期也和函数的类似:
trait方法中生命期的标注基本上与函数类似。注意,impl也可能有生命周期的标注。
就如泛型类型能够被约束一样,生命周期(它们本身就是泛型)也可以使用约束。:字符的意义在这里稍微有些不同,不过+是相同的。注意下面的说明:
下面例子展示了上述语法的实际应用:
看下面的例子,了解列举到的各个方法:
有些生命周期的模式太常用了,所以借用检查器将会隐式地添加它们以减少程序输入量和增强可读性。这种隐式添加生命周期的过程称为省略(elision)。在Rust使用省略仅仅是因为这些模式太普遍了。
trait是对未知类型Self定义的方法集。该类型也可以访问同一个trait中定义的其他方法。
对任何数据类型都可以实现trait。在下面例子中,我们定义了包含一系列方法的Animal。然后针对Sheep数据类型实现Animaltrait,因而Sheep的实例可以使用Animal中的所有方法。
下面是可以自动派生的trait:
Rust编译器需要知道每个函数的返回类型需要多少空间。这意味着所有函数都必须返回一个具体类型。与其他语言不同,如果你有个像Animal那样的的trait,则不能编写返回Animal的函数,因为其不同的实现将需要不同的内存量。
但是,有一个简单的解决方法。相比于直接返回一个trait对象,我们的函数返回一个包含一些Animal的Box。box只是对堆中某些内存的引用。因为引用的大小是静态已知的,并且编译器可以保证引用指向已分配的堆Animal,所以我们可以从函数中返回trait!
每当在堆上分配内存时,Rust都会尝试尽可能明确。因此,如果你的函数以这种方式返回指向堆的trait指针,则需要使用dyn关键字编写返回类型,例如Box
Box,Vec,String,File,以及Process是一些实现了Droptrait来释放资源的类型。Droptrait也可以为任何自定义数据类型手动实现。
下面示例给drop函数增加了打印到控制台的功能,用于宣布它在什么时候被调用。
这个trait只需定义一个返回next(下一个)元素的方法,这可手动在impl代码块中定义,或者自动定义(比如在数组或区间中)。
usestd::iter;usestd::vec::IntoIter;//该函数组合了两个`Vec
//返回一个将输入和`y`相加的函数fnmake_adder_function(y:i32)->implFn(i32)->i32{letclosure=move|x:i32|{x+y};closure}fnmain(){letplus_one=make_adder_function(1);assert_eq!(plus_one(2),3);}您还可以使用implTrait返回使用map或filter闭包的迭代器!这使得使用map和filter更容易。因为闭包类型没有名称,所以如果函数返回带闭包的迭代器,则无法写出显式的返回类型。但是有了implTrait,你就可以轻松地做到这一点:
一个类型可以实现许多不同的trait。如果两个trait都需要相同的名称怎么办?例如,许多trait可能拥有名为get()的方法。他们甚至可能有不同的返回类型!
有个好消息:由于每个trait实现都有自己的impl块,因此很清楚您要实现哪个trait的get方法。
何时需要调用这些方法呢?为了消除它们之间的歧义,我们必须使用完全限定语法(FullyQualifiedSyntax)。
Rust提供了一个强大的宏系统,可进行元编程(metaprogramming)。你已经在前面的章节中看到,宏看起来和函数很像,只不过名称末尾有一个感叹号!。宏并不产生函数调用,而是展开成源码,并和程序的其余部分一起被编译。Rust又有一点和C以及其他语言都不同,那就是Rust的宏会展开为抽象语法树(AST,abstractsyntaxtree),而不是像字符串预处理那样直接替换成代码,这样就不会产生无法预料的优先权错误。
宏是通过macro_rules!宏来创建的。
//这是一个简单的宏,名为`say_hello`。macro_rules!say_hello{//`()`表示此宏不接受任何参数。()=>(//此宏将会展开成这个代码块里面的内容。println!("Hello!");)}fnmain(){//这个调用将会展开成`println("Hello");`!say_hello!()}为什么宏是有用的?
在下面的小节中,我们将展示如何在Rust中定义宏。基本的概念有三个:
宏的参数使用一个美元符号$作为前缀,并使用一个指示符(designator)来注明类型:
macro_rules!create_function{//此宏接受一个`ident`指示符表示的参数,并创建一个名为`$func_name`的函数。//`ident`指示符用于变量名或函数名($func_name:ident)=>(fn$func_name(){//`stringify!`宏把`ident`转换成字符串。println!("Youcalled{:}()",stringify!($func_name))})}//借助上述宏来创建名为`foo`和`bar`的函数。create_function!(foo);create_function!(bar);macro_rules!print_result{//此宏接受一个`expr`类型的表达式,并将它作为字符串,连同其结果一起//打印出来。//`expr`指示符表示表达式。($expression:expr)=>(//`stringify!`把表达式*原样*转换成一个字符串。println!("{:}={:}",stringify!($expression),$expression))}fnmain(){foo();bar();print_result!(1u32+1);//回想一下,代码块也是表达式!print_result!({letx=1u32;x*x+2*x-1});}这里列出全部指示符:
宏可以重载,从而接受不同的参数组合。在这方面,macro_rules!的作用类似于匹配(match)代码块:
在下面例子中,把模式这样:$(...),+包围起来,就可以匹配一个或多个用逗号隔开的表达式。另外注意到,宏定义的最后一个分支可以不用分号作为结束。
比如说我想要定义一套小的计算器API,可以传给它表达式,它会把结果打印到控制台上。
macro_rules!calculate{(eval$e:expr)=>{{{letval:usize=$e;//强制类型为整型println!("{}={}",stringify!{$e},val);}}};}fnmain(){calculate!{eval1+2//看到了吧,`eval`可并不是Rust的关键字!}calculate!{eval(1+2)*(3/4)}}输出:
可变参数接口可以接受任意数目的参数。比如说println就可以,其参数的数目是由格式化字符串指定的。
我们可以把之前的calculate!宏改写成可变参数接口:
macro_rules!calculate{//单个`eval`的模式(eval$e:expr)=>{{{letval:usize=$e;//Forcetypestobeintegersprintln!("{}={}",stringify!{$e},val);}}};//递归地拆解多重的`eval`(eval$e:expr,$(eval$es:expr),+)=>{{calculate!{eval$e}calculate!{$(eval$es),+}}};}fnmain(){calculate!{//妈妈快看,可变参数的`calculate!`!eval1+2,eval3+4,eval(2*3)+1}}输出:
在Rust中有多种处理错误的方式,在接下来的小节中会一一介绍。它们多少有些区别,使用场景也不尽相同。总的来说:
我们将要看到的最简单的错误处理机制就是panic。它会打印一个错误消息,开始回退(unwind)任务,且通常会退出程序。这里我们显式地在错误条件下调用panic:
我们可以检查空字符串(""),就像处理蛇那样。但既然我们在用Rust,不如让编译器辨别没有礼物的情况。
在标准库(std)中有个叫做Option
这些选项可以通过match显式地处理,或使用unwrap隐式地处理。隐式处理要么返回Some内部的元素,要么就panic。
fnnext_birthday(current_age:Option
Option有一个内置方法map(),这个组合算子可用于Some->Some和None->None这样的简单映射。多个不同的map()调用可以串起来,这样更加灵活。
在下面例子中,process()轻松取代了前面的所有函数,且更加紧凑。
map()以链式调用的方式来简化match语句。然而,如果以返回类型是Option
and_then()使用被Option包裹的值来调用其输入函数并返回结果。如果Option是None,那么它返回None。
在下面例子中,cookable_v2()会产生一个Option
也就是说,Result
按照约定,预期结果是“Ok”,而意外结果是“Err”。
Result有很多类似Option的方法。例如unwrap(),它要么举出元素T,要么就panic。对于事件的处理,Result和Option有很多相同的组合算子。
我们来看看当parse()字符串成功和失败时会发生什么:
fnmultiply(first_number_str:&str,second_number_str:&str)->i32{//我们试着用`unwrap()`把数字放出来。它会咬我们一口吗?letfirst_number=first_number_str.parse::
为了改善错误消息的质量,我们应该更具体地了解返回类型并考虑显式地处理错误。
上一节的multiply函数的panic设计不是健壮的(robust)。一般地,我们希望把错误返回给调用者,这样它可以决定回应错误的正确方式。
译注:原文没有具体讲如何确定Err的类型。由于目前用于获取类型的函数仍然是不稳定的,我们可以用间接的方法。使用下面的代码:
fnmain(){leti:()="t".parse::
note:expectedtype`()`foundtype`std::result::Result
在下面的例子中,使用简单的match语句导致了更加繁琐的代码。
下面给出一个简短的示例来展示语法:
在上一个例子中,我们显式地使用组合算子处理了错误。另一种处理错误的方式是使用match语句和提前返回(earlyreturn)的结合。
这也就是说,如果发生错误,我们可以停止函数的执行然后返回错误。对有些人来说,这样的代码更好写,更易读。这次我们使用提前返回改写之前的例子:
usestd::num::ParseIntError;fnmultiply(first_number_str:&str,second_number_str:&str)->Result
在下一部分,我们将看到,当只是需要unwrap并且不产生panic时,可以使用来达到同样的效果。
有时我们只是想unwrap且避免产生panic。到现在为止,对unwrap的错误处理都在强迫我们一层层地嵌套,然而我们只是想把里面的变量拿出来。正是为这种情况准备的。
当找到一个Err时,可以采取两种行动:
前面出现的例子都是很方便的情况;都是Result和其他Result交互,还有Option和其他Option交互。
有时Option需要和Result进行交互,或是Result
在下面代码中,unwrap的两个实例生成了不同的错误类型。Vec::first返回一个Option,而parse::
fndouble_first(vec:Vec<&str>)->i32{letfirst=vec.first().unwrap();//生成错误12*first.parse::
处理混合错误类型的最基本的手段就是让它们互相包含。
Rust允许我们定义自己的错误类型。一般来说,一个“好的”错误类型应当:
注意在上一个例子中,我们调用parse后总是立即将错误从标准库的错误map(映射)到装箱错误。
.and_then(|s|s.parse::
之前被解释为要么unwrap,要么returnErr(err),这只是在大多数情况下是正确的。实际上是指unwrap或returnErr(From::from(err))。由于From::from是不同类型之间的转换工具,也就是说,如果在错误可转换成返回类型地方使用,它将自动转换成返回类型。
我们在这里使用重写之前的例子。重写后,只要为我们的错误类型实现From::from,就可以不再使用map_err。
usestd::error;usestd::fmt;//为`Box
把错误装箱这种做法也可以改成把它包裹到你自己的错误类型中。
Iter::map操作可能失败,比如:
fnmain(){letstrings=vec!["tofu","93","18"];letnumbers:Vec<_>=strings.into_iter().map(|s|s.parse::
filter_map会调用一个函数,过滤掉为None的所有结果。
fnmain(){letstrings=vec!["tofu","93","18"];letnumbers:Result
fnmain(){letstrings=vec!["tofu","93","18"];let(numbers,errors):(Vec<_>,Vec<_>)=strings.into_iter().map(|s|s.parse::
在Rust中,所有值默认都是栈分配的。通过创建Box
被装箱的值可以使用*运算符进行解引用;这会移除掉一层装箱。
Rust中有两种字符串类型:String和&str。
String被存储为由字节组成的vector(Vec
&str是一个总是指向有效UTF-8序列的切片(&[u8]),并可用来查看String的内容,就如同&[T]是Vec
书写含有特殊字符的字符串字面量有很多种方法。它们都会产生类似的&str,所以最好选择最方便的写法。类似地,字节串(bytestring)字面量也有多种写法,它们都会产生&[u8;N]类型。
通常特殊字符是使用反斜杠字符\来转义的,这样你就可以在字符串中写入各种各样的字符,甚至是不可打印的字符以及你不知道如何输入的字符。如果你需要反斜杠字符,再用另一个反斜杠来转义它就可以,像这样:\\。
字面量中出现的字符串或字符定界符必须转义:"\""、'\''。
fnmain(){//通过转义,可以用十六进制值来表示字节。letbyte_escape="I'mwriting\x52\x75\x73\x74!";println!("Whatareyoudoing\x3F(\\x3Fmeans){}",byte_escape);//也可以使用Unicode码位表示。letunicode_codepoint="\u{211D}";letcharacter_name="\"DOUBLE-STRUCKCAPITALR\"";println!("Unicodecharacter{}(U+211D)iscalled{}",unicode_codepoint,character_name);letlong_string="Stringliteralscanspanmultiplelines.Thelinebreakandindentationhere->\<-canbeescapedtoo!";println!("{}",long_string);}有时会有太多需要转义的字符,或者是直接原样写出会更便利。这时可以使用原始字符串(rawstring)。
fnmain(){letraw_str=r"Escapesdon'tworkhere:\x3F\u{211D}";println!("{}",raw_str);//如果你要在原始字符串中写引号,请在两边加一对#letquotes=r#"AndthenIsaid:"Thereisnoescape!""#;println!("{}",quotes);//如果字符串中需要写"#,那就在定界符中使用更多的#。//可使用的#的数目没有限制。letlonger_delimiter=r###"Astringwith"#init.Andeven"##!"###;println!("{}",longer_delimiter);}想要非UTF-8字符串(记住,&str和String都必须是合法的UTF-8序列),或者需要一个字节数组,其中大部分是文本?请使用字节串(bytestring)!
有时候想要捕捉到程序某部分的失败信息,而不是调用panic!;这可使用Option枚举类型来实现。
Option
Result
panic!宏可用于产生一个panic(恐慌),并开始回退(unwind)它的栈。在回退栈的同时,运行时将会释放该线程所拥有的所有资源,这是通过调用线程中所有对象的析构函数完成的。
因为我们正在处理的程序只有一个线程,panic!将会引发程序报告panic消息并退出。
//整型除法(/)的重新实现fndivision(dividend:i32,divisor:i32)->i32{ifdivisor==0{//除以0会引发panicpanic!("divisionbyzero");}else{dividend/divisor}}//`main`任务fnmain(){//堆分配的整数let_x=Box::new(0i32);//此操作将会引发一个任务失败division(3,0);println!("Thispointwon'tbereached!");//`_x`应当会在此处被销毁}可以看到,panic!不会泄露内存:
和vector类似,HashMap也是可增长的,但HashMap在占据了多余空间时还可以缩小自己。可以使用HashMap::with_capacity(unit)创建具有一定初始容量的HashMap,也可以使用HashMap::new()来获得一个带有默认初始容量的HashMap(这是推荐方式)。
任何实现了Eq和Hashtrait的类型都可以充当HashMap的键。这包括:
对于所有的集合类(collectionclass),如果它们包含的类型都分别实现了Eq和Hash,那么这些集合类也就实现了Eq和Hash。例如,若T实现了Hash,则Vec
对自定义类型可以轻松地实现Eq和Hash,只需加上一行代码:#[derive(PartialEq,Eq,Hash)]。
编译器将会完成余下的工作。如果你想控制更多的细节,你可以手动实现Eq和/或Hash。本指南不包含实现Hash的细节内容。
你可能会问:“这有什么意义呢?我完全可以将键存储到一个Vec中呀。”
如果插入的值已经存在于HashSet中(也就是,新值等于已存在的值,并且拥有相同的散列值),那么新值将会替换旧的值。
如果你不想要一样东西出现多于一次,或者你要判断一样东西是不是已经存在,这种做法就很有用了。
不过集合(set)可以做更多的事。
集合(set)拥有4种基本操作(下面的调用全部都返回一个迭代器):
在下面的例子中尝试使用这些操作。
当需要多个所有权时,可以使用Rc(引用计数,ReferenceCounting缩写)。Rc跟踪引用的数量,这相当于包裹在Rc值的所有者的数量.
每当克隆一个Rc时,Rc的引用计数就会增加1,而每当克隆得到的Rc退出作用域时,引用计数就会减少1。当Rc的引用计数变为0时,这意味着已经没有所有者,Rc和值两者都将被删除。
克隆Rc从不执行深拷贝。克隆只创建另一个指向包裹值的指针,并增加计数。
当线程之间所有权需要共享时,可以使用Arc(共享引用计数,AtomicReferenceCounted缩写)可以使用。这个结构通过Clone实现可以为内存堆中的值的位置创建一个引用指针,同时增加引用计数器。由于它在线程之间共享所有权,因此当指向某个值的最后一个引用指针退出作用域时,该变量将被删除。
Rust通过spawn函数提供了创建本地操作系统(nativeOS)线程的机制,该函数的参数是一个通过值捕获变量的闭包(movingclosure)。
usestd::thread;staticNTHREADS:i32=10;//这是主(`main`)线程fnmain(){//提供一个vector来存放所创建的子线程(children)。letmutchildren=vec![];foriin0..NTHREADS{//启动(spinup)另一个线程children.push(thread::spawn(move||{println!("thisisthreadnumber{}",i)}));}forchildinchildren{//等待线程结束。返回一个结果。let_=child.join();}}这些线程由操作系统调度(schedule)。
Rust使数据的并行化处理非常简单,在Rust中你无需面对并行处理的很多传统难题。
标准库提供了开箱即用的线程类型,把它和Rust的所有权概念与别名规则结合起来,可以自动地避免数据竞争(datarace)。
当某状态对某线程是可见的,别名规则(即一个可变引用XOR一些只读引用。译注:XOR是异或的意思,即「二者仅居其一」)就自动地避免了别的线程对它的操作。(当需要同步处理时,请使用Mutex或Channel这样的同步类型。)
在本例中,我们将会计算一堆数字中每一位的和。我们将把它们分成几块,放入不同的线程。每个线程会把自己那一块数字的每一位加起来,之后我们再把每个线程提供的结果再加起来。
注意到,虽然我们在线程之间传递了引用,但Rust理解我们是在传递只读的引用,因此不会发生数据竞争等不安全的事情。另外,因为我们把数据块move到了线程中,Rust会保证数据存活至线程退出,因此不会产生悬挂指针。
请修改程序,使得数据总是被分成有限数目的段,这个数目是由程序开头的静态常量决定的。
Rust为线程之间的通信提供了异步的通道(channel)。通道允许两个端点之间信息的单向流动:Sender(发送端)和Receiver(接收端)。
译注:prelude是Rust自动地在每个程序中导入的一些通用的东西,这样我们就不必每写一个程序就手动导入一番。
Path可从OsStr类型创建,并且它提供数种方法,用于获取路径指向的文件/目录的信息。
注意Path在内部并不是用UTF-8字符串表示的,而是存储为若干字节(Vec
usestd::path::Path;fnmain(){//从`&'staticstr`创建一个`Path`letpath=Path::new(".");//`display`方法返回一个可显示(showable)的结构体letdisplay=path.display();//`join`使用操作系统特定的分隔符来合并路径到一个字节容器,并返回新的路径letnew_path=path.join("a").join("b");//将路径转换成一个字符串切片matchnew_path.to_str(){None=>panic!("newpathisnotavalidUTF-8sequence"),Some(s)=>println!("newpathis{}",s),}}记得看看其他的Path方法(posix::Path或windows::Path的),还有Metadata结构体类型。
File结构体表示一个被打开的文件(它包裹了一个文件描述符),并赋予了对所表示的文件的读写能力。
由于在进行文件I/O(输入/输出)操作时可能出现各种错误,因此File的所有方法都返回io::Result
这使得所有I/O操作的失败都变成显式的。借助这点,程序员可以看到所有的失败路径,并被鼓励主动地处理这些情形。
open静态方法能够以只读模式(read-onlymode)打开一个文件。
File拥有资源,即文件描述符(filedescriptor),它会在自身被drop时关闭文件。
usestd::fs::File;usestd::io::prelude::*;usestd::path::Path;fnmain(){//创建指向所需的文件的路径letpath=Path::new("hello.txt");letdisplay=path.display();//以只读方式打开路径,返回`io::Result
$echo"HelloWorld!">hello.txt$rustcopen.rs&&./openhello.txtcontains:HelloWorld!(我们鼓励您在不同的失败条件下测试前面的例子:hello.txt不存在,或hello.txt不可读,等等。)
create静态方法以只写模式(write-onlymode)打开一个文件。若文件已经存在,则旧内容将被销毁。否则,将创建一个新文件。
staticLOREM_IPSUM:&'staticstr="Loremipsumdolorsitamet,consecteturadipisicingelit,seddoeiusmodtemporincididuntutlaboreetdoloremagnaaliqua.Utenimadminimveniam,quisnostrudexercitationullamcolaborisnisiutaliquipexeacommodoconsequat.Duisauteiruredolorinreprehenderitinvoluptatevelitessecillumdoloreeufugiatnullapariatur.Excepteursintoccaecatcupidatatnonproident,suntinculpaquiofficiadeseruntmollitanimidestlaborum.";usestd::io::prelude::*;usestd::fs::File;usestd::path::Path;fnmain(){letpath=Path::new("out/lorem_ipsum.txt");letdisplay=path.display();//以只写模式打开文件,返回`io::Result
$mkdirout$rustccreate.rs&&./createsuccessfullywrotetoout/lorem_ipsum.txt$catout/lorem_ipsum.txtLoremipsumdolorsitamet,consecteturadipisicingelit,seddoeiusmodtemporincididuntutlaboreetdoloremagnaaliqua.Utenimadminimveniam,quisnostrudexercitationullamcolaborisnisiutaliquipexeacommodoconsequat.Duisauteiruredolorinreprehenderitinvoluptatevelitessecillumdoloreeufugiatnullapariatur.Excepteursintoccaecatcupidatatnonproident,suntinculpaquiofficiadeseruntmollitanimidestlaborum.(和前面例子一样,我们鼓励你在失败条件下测试这个例子。)
还有一个更通用的open_mode方法,这能够以其他方式来来打开文件,如:read+write(读+写),追加(append),等等。
方法lines()在文件的行上返回一个迭代器。
File::open需要一个泛型AsRef
usestd::fs::File;usestd::io::{self,BufRead};usestd::path::Path;fnmain(){//在生成输出之前,文件主机必须存在于当前路径中ifletOk(lines)=read_lines("./hosts"){//使用迭代器,返回一个(可选)字符串forlineinlines{ifletOk(ip)=line{println!("{}",ip);}}}}//输出包裹在Result中以允许匹配错误,//将迭代器返回给文件行的读取器(Reader)。fnread_lines
(filename:P)->io::Result
$echo-e"127.0.0.1\n192.168.0.1\n">hosts$rustcread_lines.rs&&./read_lines127.0.0.1192.168.0.1这个过程比在内存中创建String更有效,特别是处理更大的文件。
process::Output结构体表示已结束的子进程(childprocess)的输出,而process::Command结构体是一个进程创建者(processbuilder)。
usestd::process::Command;fnmain(){letoutput=Command::new("rustc").arg("--version").output().unwrap_or_else(|e|{panic!("failedtoexecuteprocess:{}",e)});ifoutput.status.success(){lets=String::from_utf8_lossy(&output.stdout);print!("rustcsucceededandstdoutwas:\n{}",s);}else{lets=String::from_utf8_lossy(&output.stderr);print!("rustcfailedandstderrwas:\n{}",s);}}(再试试上面的例子,给rustc命令传入一个错误的flag)
std::Child结构体代表了一个正在运行的子进程,它暴露了stdin(标准输入),stdout(标准输出)和stderr(标准错误)句柄,从而可以通过管道与所代表的进程交互。
$rustcfs.rs&&./fs`mkdira``echohello>a/b.txt``mkdir-pa/c/d``toucha/c/e.txt``ln-s../b.txta/c/b.txt``cata/c/b.txt`>hello`lsa`>"a/b.txt">"a/c"`rma/c/e.txt``rmdira/c/d`且a目录的最终状态为:
$treeaa|--b.txt`--c`--b.txt->../b.txt1directory,2files另一种定义cat函数的方式是使用标记:
命令行参数可使用std::env::args进行接收,这将返回一个迭代器,该迭代器会对每个参数举出一个字符串。
可以用模式匹配来解析简单的参数:
测试有三种风格:
Rust也支持在测试中指定额外的依赖:
测试(test)是这样一种Rust函数:它保证其他部分的代码按照所希望的行为正常运行。测试函数的函数体通常会进行一些配置,运行我们想要测试的代码,然后断言(assert)结果是不是我们所期望的。
pubfnadd(a:i32,b:i32)->i32{a+b}//这个加法函数写得很差,本例中我们会使它失败。#[allow(dead_code)]fnbad_add(a:i32,b:i32)->i32{a-b}#[cfg(test)]modtests{//注意这个惯用法:在tests模块中,从外部作用域导入所有名字。usesuper::*;#[test]fntest_add(){assert_eq!(add(1,2),3);}#[test]fntest_bad_add(){//这个断言会导致测试失败。注意私有的函数也可以被测试!assert_eq!(bad_add(1,2),3);}}可以使用cargotest来运行测试。
pubfndivide_non_zero_result(a:u32,b:u32)->u32{ifb==0{panic!("Divide-by-zeroerror");}elseifa
$cargotesttest_any_panicrunning1testtesttests::test_any_panic...oktestresult:ok.1passed;0failed;0ignored;0measured;2filteredoutDoc-teststmp-test-should-panicrunning0teststestresult:ok.0passed;0failed;0ignored;0measured;0filteredout要运行多个测试,可以仅指定测试名称中的一部分,用它来匹配所有要运行的测试。
///第一行是对函数的简短描述。//////接下来数行是详细文档。代码块用三个反引号开启,Rust会隐式地在其中添加///`fnmain()`和`externcrate
cargo在与src同级别的tests目录寻找集成测试。
文件src/lib.rs:
//在一个叫做'adder'的crate中定义此函数。pubfnadd(a:i32,b:i32)->i32{a+b}包含测试的文件:tests/integration_test.rs:
#[test]fntest_add(){assert_eq!(adder::add(3,2),5);}使用cargotest命令:
$cargotestrunning0teststestresult:ok.0passed;0failed;0ignored;0measured;0filteredoutRunningtarget/debug/deps/integration_test-bcd60824f5fbfe19running1testtesttest_add...oktestresult:ok.1passed;0failed;0ignored;0measured;0filteredoutDoc-testsadderrunning0teststestresult:ok.0passed;0failed;0ignored;0measured;0filteredouttests目录中的每一个Rust源文件都被编译成一个单独的crate。在集成测试中要想共享代码,一种方式是创建具有公用函数的模块,然后在测试中导入并使用它。
文件tests/common.rs:
pubfnsetup(){//一些配置代码,比如创建文件/目录,开启服务器等等。}包含测试的文件:tests/integration_test.rs
比如说使用pretty_assertions,这是扩展了标准的assert!宏的一个crate。
文件Cargo.toml:
#这里省略了标准的crate数据[dev-dependencies]pretty_assertions="1"文件src/lib.rs:
原始指针(rawpointer,裸指针)*和引用&T有类似的功能,但引用总是安全的,因为借用检查器保证了它指向一个有效的数据。解引用一个裸指针只能通过不安全代码块执行。
usestd::slice;fnmain(){letsome_vector=vec![1,2,3,4];letpointer=some_vector.as_ptr();letlength=some_vector.len();unsafe{letmy_slice:&[u32]=slice::from_raw_parts(pointer,length);assert_eq!(some_vector.as_slice(),my_slice);}}slice::from_raw_parts假设传入的指针指向有效的内存,且被指向的内存具有正确的数据类型,我们必须满足这一假设,否则程序的行为是未定义的(undefined),于是我们就不能预测会发生些什么了。
Rust语言正在快速发展,因此尽管努力确保尽可能向前兼容,但仍可能出现某些兼容性问题。
与许多编程语言一样,Rust拥有“关键字”的概念。这些标识符对语言有特定意义,所以不能在变量名、函数名和其他位置使用它们。原始标识符允许你使用通常不允许的关键字。当Rust引入新关键字时,使用旧版Rust的库拥有与新版本中引入的关键字同名的变量或函数,这一点就特别有用。
举个例子,使用2015版Rust编译的cratefoo,它导出一个名为try的函数。此关键字(try)在2018版本的新功能中保留下来,因此如果没有原始标识符,我们将无法命名该功能。
externcratefoo;fnmain(){foo::try();}将得到如下错误:
error:expectedidentifier,foundkeyword`try`-->src/main.rs:4:4|4|foo::try();|^^^expectedidentifier,foundkeyword使用原始标志符重写上述代码:
用cargodoc构建文档到target/doc。
用cargotest运行所有测试(包括文档测试),用cargotest--doc仅运行文档测试。
这些命令会恰当地按需调用rustdoc(以及rustc)。
#![crate_name="doc"]///这里给出一个“人”的表示pubstructPerson{///一个人必须有名字(不管Juliet多讨厌她自己的名字)。name:String,}implPerson{///返回具有指定名字的一个人//////#参数//////*`name`-字符串切片,代表人的名字//////#示例//////```/////在文档注释中,你可以书写代码块/////如果向`rustdoc`传递--test参数,它还会帮你测试注释文档中的代码!///usedoc::Person;///letperson=Person::new("name");///```pubfnnew(name:&str)->Person{Person{name:name.to_string(),}}///给一个友好的问候!///对被叫到的`Person`说"Hello,[name]"。pubfnhello(&self){println!("Hello,{}!",self.name);}}fnmain(){letjohn=Person::new("John");john.hello();}要运行测试,首先将代码构建为库,然后告诉rustdoc在哪里找到库,这样它就可以使每个文档中的程序链接到库:
用于内联文档,而不是链接到单独的页面。
fnmain(){println!("HelloWorld!");}这使读者既可以运行你的代码示例,也可以对其进行修改和调整。此处的关键是将单词添加editable到代码块中,并用逗号分隔。
```rust,editable//...将你的代码写在这里```此外,如果想要mdbook在构建和测试时跳过该代码,则可以添加ignore。