生命游戏:基础知识
在本章中,你将学习到以下内容:
- 如何使用Cargo创建一个新项目
- 如何在Rust中使用变量
- 如何使用Rust中的基本函数,包括返回值和传递参数
- 基本的控制机制是如何工作的
1970年,英国数学家约翰·霍顿·康威用细胞自动机设计了一个游戏。同年10月,马丁·加德纳在他的《数学游戏》月刊专栏中介绍了这款游戏科学美国人.这是一个规则简单的游戏,可以在纸上玩,但说实话,编写执行游戏的程序更有趣。我们将通过编写一个简单的实现来开始深入研究Rust康威的人生游戏.首先,我们将讨论规则,这样当我们实现它时,你就会知道你在看什么。
想象一个二维空间,它由水平轴和垂直轴上的单元格组成。也许用图表来思考会更简单——一行又一行,一列又一列的小盒子。每一个小盒子都包含,或者至少有可能包含,一个活的生物——一个生活在单细胞中的单细胞有机体。这个游戏是进化的,这意味着我们一代又一代地循环,根据游戏规则决定每个细胞的生存或死亡。说到这些规则,它们是:
- 如果一个细胞目前是活的,但它的邻居少于两个,它将因为缺乏支持而死亡。
- 如果一个细胞目前是活的,并且有两个或三个邻居,它将存活到下一代。
- 如果一个细胞目前是活的,并且有三个以上的邻居,它就会死于人口过剩(缺乏资源)。
- 如果一个单元格目前已经死亡,但刚好有三个邻居,它将会复活。
为了将游戏转化为代码,我们需要做几件事。首先,我们需要一个游戏网格,所有的小单元格都将生活在其中。其次,我们需要用一些活细胞来填充游戏网格。空棋盘不会有什么好结果。一旦我们有了一个游戏板,我们就可以使用这些规则运行几代人。
下面是一个完整的程序,它将创建游戏板,并运行不同细胞是否存活或死亡的检查。别担心,你不必一次就把所有的东西都吸收进去。我们将一步一步地向您介绍Rust。
人生游戏:程序
本节中的程序将创建游戏板康威的人生游戏用第一代来填充它。本程序的这一部分将足以让我们开始讨论如何启动Rust程序。然而,这不是一个完整的程序,因为它不会完全实现一个有用的康威的人生游戏.它主要缺少输出和代函数。
外箱牌;使用标准::{线程,时间};fn人口普查(_world:[[与;75];75])->u16{让狗计数=0;为我在0..74点{为j在0..74点{如果_world[我][j]= =1{数+ =1;}}}数}fn一代(_world:[[与;75];75])->[[与;75];75]{让傻瓜newworld=[[0与;75];75];为我在0..74点{为j在0..74点{让狗计数=0;如果我>0{数=数+_world[我-1][j];}如果我>0& &j>0{数=数+_world[我-1][j-1];}如果我>0& &j<74{数=数+_world[我-1][j+1];}如果我<74& &j>0{数=数+_world[我+1][j-1]}如果我<74{数=数+_world[我+1][j];}如果我<74& &j<74{数=数+_world[我+1][j+1];}如果j>0{数=数+_world[我][j-1];}如果j<74{数=数+_world[我][j+1];}newworld[我][j]=0;如果(数<2)& &(_world[我][j]= =1){newworld[我][j]=0;}如果_world[我][j]= =1& &(数= =2||数= =3.){newworld[我][j]=1;}如果(_world[我][j]= =0)& &(数= =3.){newworld[我][j]=1;}}}newworld}fn主要(){让狗的世界=[[0与;75];75];让狗一代=0;为我在0..74点{为j在0..74点{如果兰德::随机(){世界[我][j]=1;}其他的{世界[我][j]=0;}}}}
从货物开始
虽然你可以只使用Rust编译器,rustc
Rust附带了一个实用程序,可以用来创建必要的文件和目录结构,以构建一个程序,如果需要,可以超越单个文件。首先,我们可以跑步货物新生命
来创造我们最初需要的一切。
您将得到一个名为src
,其中包含一个文件,main.rs
.最初,该文件中有一个简单的hello, world程序,这意味着如果您想做一些有趣的事情,至少需要删除一行代码。但是,该文件确实包含了主函数的主干。如果你熟悉C编程,你就会熟悉main函数。这是程序的入口点。当编译器运行时,生成的可执行文件将指向主函数中任何内容生成的代码块。此函数对于程序的任何操作都是必不可少的,因为编译器将查找它以知道在哪里链接入口点(它只是控件中的一个地址)。text
段的结果汇编语言代码)。
除了src
目录和main.rs
文件,您将在其中进行所有的开发工作最初,有一个Cargo.toml
文件。这是Cargo使用的配置文件,用Tom's Obvious, Minimal Language (TOML)编写。这是一种容易使用的语言,Cargo几乎可以把你需要的所有东西都放入其中。我们最终将对其进行更改,但您最初将看到的是关于结果可执行文件的元数据,包括您的姓名、电子邮件地址和版本号。所有东西都是文本形式的,你可以在这里看到我运行时创建的东西货物新生命
:
[包]的名字=“生命”版本=“0.1.0”作者=[“Ric Messier ” ][依赖关系]
当然,你会得到一些看起来略有不同的东西,因为你既没有我的名字也没有我的电子邮件地址。最初的版本是0.1.0,如果您实际使用生活作为程序的名称,您将在您的Cargo.toml
文件。货物会帮你处理的。
请注意
不要太花哨你的名字。这将是给可执行文件的名称,它是构建程序的结果。如果你太花哨,尝试使用驼色箱,货物会抱怨。它要求简单的命名。如果你不熟悉驼色大小写,驼色大小写大小写混合在一起,通常大写字母出现在单词的中间,比如myProgram
.
Cargo也用于构建项目。要构建可执行文件,只需运行货物建立
.默认情况下,Cargo将构建一个调试版本,它将被放入target/debug文件夹中。如果您想要发布版本而不是调试版本,则必须运行起货-卸货
.这将把你的可执行文件放到target/release目录中。如果构建成功,您可以从那里运行您的程序。您将在目标目录中获得比可执行文件更多的文件。
在这里,你可以看到来自Life程序构建的调试目录的内容:
调试目录列表
kilroy@milobloom: ~ /文档/生锈/生活/美元目标cd调试kilroy@milobloom: ~ /文档/生锈/生活/目标/调试美元ls树立榜样Deps增量生命。d本地
文件名为生活
可执行文件和调试符号是否在文件名中life.dSYM
.这在需要使用调试器执行调试的情况下很有用,调试器将使用这些符号来跟踪它在程序中的位置,这样它不仅可以显示程序的汇编语言表示,而且还可以显示源代码,对大多数人来说,这可能比汇编语言更有意义。出于我们的目的,您不需要调试符号,除非您真的需要它们,因为我已经完成了所有调试,以确保所有代码都能在撰写本文时的当前Rust版本上编译和运行。
把碎片拼在一起
一旦使用Cargo创建了新项目,就可以开始添加代码,通常是添加到main.rs
文件。我们今后所做的一切都会在main.rs
文件,除非另有指定。我们将一次稍微介绍一下这个程序来解释这一切。我们会尽量减少程序的跳跃,尽管会有一些。首先,我们将从文件的顶部开始。
引入外部功能
无论您正在编写什么样的程序,都可能需要从您自己的代码之外引入功能。有几种不同的方法。我们可以在这里谈论它们,因为它们都在我们的生活计划中使用。相关的代码片段显示在这里。您将注意到这里发生的一些不同的事情可能与您在其他编程语言中所习惯的略有不同。
外箱牌;使用std::{线程,时间};
Rust使用名为板条箱存储外部的、可重用的功能。没有人应该在每次编写程序时都重新发明轮子,所以您可能会在编写过程中使用许多板条箱。前两行之间的区别是,第一行引用了一个外部板条箱,这意味着它是一个在标准库之外可用的包。我们在这里使用的库将使我们能够生成随机数。当涉及到在最初的世界创建中填充游戏面板时,你可以(1)作为程序员手工完成,(2)允许用户使用一些配置手工完成,或者(3)使用随机值生成世界。在第一次穿越世界时,我们将选择第三种方法,因此我们需要有能够为我们生成随机值的函数。这不是标准库中包含的功能。的走读生
关键字表示编译器需要在其他地方查找库。
说到标准库,前面代码中的第二行引入了标准库的功能。我们从标准库中引入了两个独立的模块,但不是占用两行代码,而是将其压缩到一行代码中。的{}
都是从Unix中借来的,它们的意思是“插入括号中包含的集合中的每个值,以完成表达式”。我们所做的只是一个速记符号,它将获得与我们写下面两行相同的结果。只有当您从同一位置导入功能时,这才有效。
使用std::线程;使用std::时间;
您可能熟悉导入功能的概念。在像C这样的语言中,你可以使用下面这些行从C库中包含相同的功能:
#包括< threads.h >#包括< time.h >
其他语言也有导入外部功能的相同概念。例如,在Objective-C中,你可以使用@ import
.在Swift中,您只需使用进口
.c++继承了C使用的相同的include语句。C/ c++和其他语言之间的区别之一是C/ c++使用预处理器来替换像这样的指令# include
用实际的C代码。编译器永远不会看到# include
语句,因为它在编译器得到它之前被预处理器替换。c++实际上只是另一个预处理器。所有c++代码都转换为实际的C语言,然后传递给C编译器。并非所有语言都有预处理器。Rust将这些import语句与Cargo结合使用,后者较少地充当预处理程序,而更多地充当协调器。
如前所述,走读生
关键字表明我们正在使用外部库。我们依靠Cargo来确保库已经就位并构建好了,以便在编译程序时,能够成功地解析所有外部引用。这意味着我们需要在我们的Cargo.toml
文件。在(依赖)
section中,我们需要告诉Cargo我们将需要一个库。正如您在下面的代码中所看到的,我们提供了库的名称以及程序运行所需的版本号。最后一部分可以用*
表示任何版本都可以工作,但可能需要特定的版本,因为不同的版本有时具有不同的功能,以及不同的签名。
[依赖关系]兰德=“0.7.2”
签名很重要,因为它确定了函数期望接收的参数以及函数将返回的值。如果我们正在编写的程序没有按照所使用的库版本中指定的方式使用函数,则编译将失败。因此,重要的是要知道您正在使用哪个版本的库,以确保您使用函数的方式与该版本中指定的相同。
名称空间
这就提出了名称空间的概念,尽管Rust并不是这样称呼它们的。尽管它不是Rust使用的术语,但谈论它是一个有用的概念。名称空间是很常见的东西,它们特别用于面向对象的语言,如c#或c++。它们也用于容器,容器是虚拟化应用程序的方式。名称空间实际上只是一个容器。这是一种把很多相关的东西放在同一个地方的方法,以便使这些东西的指代一致。这就是为什么在这里引入名称空间是有意义的。前面,我们从模块中引入了功能。您可以认为这些模块中的所有属性和函数都属于同一个名称空间,我的意思是,为了引用它们,您应该使用相同的命名结构。
编写程序的指导原则之一是,我们试图以一种对使用我们创建的函数和变量编写程序的人有意义的方式命名函数和变量。不幸的是,在这样做的过程中,许多模块或库将具有使用相同名称的函数或属性。我们需要一种方法来区分它们。
考虑一下你的房子。你的房子里有很多房间。每个房间至少有一个电灯开关。如果我告诉你关灯,你怎么知道该关哪个灯?房间提供上下文或名称空间,帮助我们理解请求。然后我可以说把客厅的灯开关关掉,你就知道该怎么做了。在前面的代码中,您已经看到了类似的内容。当我们从标准库引入功能时,我们使用std::线程
举个例子。从本质上讲,该表达式为我们提供了命名空间,以区分标准库之外的线程与来自不同库的线程。
我们可以把这个例子举得更远一点,这也会让我们前进一点。使用Rust语法,我可以告诉Rust使用如下命令关闭客厅的灯厅:switch.off ()
.这在前面提供了上下文或名称空间。我使用厅的
作为我想使用功能的模块。我要换掉厅的
模块,然后调用()
作为开关对象上的函数或方法。
我们必须继续使用命名空间来引用我们正在使用的模块中的任何对象(厅::
),以确保我们清楚地知道我们将使用哪个对象。这样,编译器就没有什么可以猜测的,也许同样重要的是,当其他程序员阅读我们所写的东西时,它是清楚的。这种显式是我们在使用Rust时会经常用到的。所有内容都是显式的,也必须是显式的,这样就不会在我们编写的内容与编译器为我们生成的内容之间存在猜谜游戏或误解。坦白说,这是拉斯特的魅力之一。
生成游戏网格
导入功能后,就可以开始编写程序了。如前所述,从阅读源代码的角度来看,这主要是一个线性过程。我们将尽我们所能,从源代码的顶部到底部。唯一的偏差是我们将从主函数开始,或者程序的入口点。
将main函数放在源代码底部的一个原因是,尽管它实际上是程序的开始,这是对C语言的保留。在C编程语言中,以及许多其他编程语言中,您不能使用没有定义的东西。当你写主函数时,你会调用其他函数。如果你试图在它们被定义或实现(根据定义,这是一个定义)之前调用它们,你会得到一个编译器错误,因为编译器不知道它是什么,以匹配你如何使用它。又是那个签名的事。如果我把一个函数定义为两个整数,但你试图用一个字符数组来调用它,这不会很好地工作。编译器应该标记它,但如果它在使用之前不知道它应该是什么样子,它就不能标记它。
在Rust中,你可以把main函数放在源代码的顶部,因为它会延迟判断你是否正确调用了一个函数,直到它真正看到函数的定义。作为练习,从本章的源代码开始移动主函数fn主要
一直到最后}
把它放在文件的顶部,就在我们要用到的模块的下面。当您构建时,它将成功构建。当我们继续前进,你开始编写自己的Rust程序时,你可以自由地把主函数,或任何函数放在文件中你想要的任何地方。因此,编译器不会在你身上出错。
解剖主要
我们会到文件的底部,看一下主函数,但是是分段的,因为它是一个相当长的函数。这里还有一些main函数的关键组件,所以我们将尽量保持它的缓慢和可管理,这样您不仅可以轻松理解该语言的语法,还可以轻松理解Rust与其他语言不同的重要特性。在有帮助的地方,我们将看看Rust与您可能熟悉的其他常见语言相比如何。
定义函数
函数是当今大多数语言的一个共同特征,尽管您可能听说过这个术语方法有时用于描述同一类型的特征。函数是一种将代码和数据放在一个小块中的方法。当我们创建函数时,我们创造了一种能力,可以反复重用一组代码,而不必在每次想要使用它时重写相同的代码。通常,函数接受参数,也可能返回值。这意味着我们可以将数据传递给函数进行操作,然后函数可以将所做的任何工作的结果返回给调用函数。
Rust需要使用函数,这与您可能熟悉的一些语言不同。例如,Python不要求你使用任何函数。如果你愿意,你可以完全不使用任何函数来编写Python脚本。类似地,其他脚本语言不需要使用任何函数。当然,Rust不像Python那样是一种脚本语言。与Python、Perl或其他脚本语言不同,Rust使用编译器生成可执行文件,当用户想要运行程序时使用。即使你在编写Python程序时确实使用了函数,你也不必创建一个主函数,它会显式地告诉解释器(Rust中的编译器)从哪里开始执行程序。
fn主要(){
在这里,您可以看到Rust中main函数的定义。这是一个基本的定义。我们使用fn
表示将要出现的是一个函数。这类似于Python这样的语言,它使用def
表示函数的定义快速使用函数
表示将要发生什么的是一个函数。尽管这些语言被认为类似于C语言——因为C语言与Swift、Python和rust等语言之间的一些语法和控制结构是相似的——但C语言中的函数定义是不同的。C语言的main函数定义如下:
int主要(int命令行参数个数,字符**argv){
我们从函数将在结束时返回的变量类型开始,而不是预先指出我们所拥有的是一个函数。在C语言中,你必须指定要返回的数据类型,即使它是无效
,为no datatype,表示没有返回值。像Rust这样的语言可能永远不会返回值,如果没有返回值,则没有返回值的指示,正如您在前面的main函数声明中所看到的那样。我们完全可以从我们想要的任何函数返回值,在本章后面,当我们查看程序中的其他函数时,您将看到它是如何工作的。类似地,主函数的C声明包括传递给主函数的命令行参数。这不是必需的,就像在Rust程序中不是必需的一样。如果不需要,我们就不包含它。
函数和其他东西一样,都是作用域定义。当函数中有数据时,一旦传递到函数外部,这些数据就不再可用。这意味着我们需要一种方法来指示函数在哪里开始,在哪里停止。Python喜欢使用空白来清楚地定义作用域。这是语言定义的一部分。Python中没有开始/结束块。你只需要注意缩进的程度。就我个人而言,我不喜欢使用空白作为语法或语言定义的一部分。幸运的是,Rust在这里再次遵循了C。C语言使用花括号(或方括号)来表示任何代码块的开始和结束。 We start a function with a{
用a结尾}
.这可能比Python中使用的空白更难可视化地解析,但您可以使用良好的缩进实践来提供可视化的解析能力,而不必强制使用它。
至此,我们有了main函数的声明以及代码块的开始。我们可以直接看函数的其他部分。
定义变量
有些语言对定义变量的位置非常挑剔。在函数的顶部定义所有变量通常是一个很好的实践,但语言定义或编译器并不要求这样做。如果您确切地知道在哪里查找函数的不同元素,就会更容易理解发生了什么。在函数中间定义变量会使以后调试或读取程序变得更加困难,因为在读取复杂或较长的函数时,您可能会错过声明以了解使用的数据类型。使用这个指导,声明(有一个例外,我们将在后面讨论)在函数的顶部完成。
让狗的世界=[[0与;75];75];让狗一代=0;
我们在主函数中定义了两个变量。其中之一是游戏网格,它是一个多维数组。在我们讨论定义的这一方面之前,我们应该从左边开始讨论它的其余部分。首先,我们使用关键字声明一个变量让
.如果我们愿意,我们可以简单地声明一个变量让count = 0;
.这表明我们有一个名为数
初始值为0。Rust将推断数据类型,因为我们没有指定它。既然它已经定义了,我们就可以继续使用这个变量了数
.
请注意
在命名变量时,可以使用字母、数字或下划线字符。不能在变量名中使用特殊字符。当涉及到命名时,有一些约定,编译器将帮助你,当你没有遵循命名约定时提供建议。变量名的起始字符必须是字母或下划线。值得注意的是,变量名是区分大小写的。Camel大小写在Java和其他语言中常用,但在Rust中仅在特定情况下使用,您将在后面的章节中了解到这一点。
然而,这有点麻烦,这就把我们带到了声明行中的第二个关键字。需要注意的是Rust使用的是开发人员所称的不变的变量默认情况下。你可以像我一样,对不可变变量这个术语吹毛求疵,因为变量,从定义上讲,意味着变化,而不可变意味着不变。术语“不可变变量”指的是某些东西会改变,但不会改变。本质上,如果你有一个不可变变量,你就有一个常数,因为它不会改变。从语言和编译的角度来看,不可变变量不同于常量。
抛开语言上的吹毛求疵不谈,这是语言的一个重要方面。因为这是一个很微妙的东西,你会看到它经常出现在我们讨论不同的变量以及它们在这本书剩下的部分是如何使用的。常量,从语言和编译器的角度来看,本质上是一个别名。编译器,比如C语言中常用的那些编译器,将遍历并简单地替换它所指的术语。例如,再次使用C作为一种简单的方法来演示这个概念,下面是我们如何在C程序中声明常量:
#定义MYCONST42
这表明我们有一项,MYCONST
,即42。C预处理器将遍历应用此定义的所有代码,并替换它找到的任何地方MYCONST
值是42。唯一的目的MYCONST
服务是为了更容易地更改值MYCONST
在任何时候,在整个程序中进行更改。如果给它起一个有意义的名字,它还会提供一些自文档。如果你要用MAX_X
例如,你知道它可能是图上x轴上的最大值。这比原始数字更有用。
不能改变的变量是不同的。首先,您不能将常量设置为函数调用的结果,因为它在编译时是未知的。可以将变量设置为函数调用的返回值,但是一旦值设置好就不能再更改了。一个不能被改变的变量也被保护不被修改,因此您总是可以确保您期望的值会在那里,或者至少在某一点上设置的值没有被破坏。这对任何并发编程都有帮助,因为你可以使用一个变量,而不用担心它在使用过程中被另一个线程修改。
请注意
在编程中有一个概念叫做纯函数。纯函数是在给定相同输入集的情况下,每次调用函数时返回相同值的函数。此外,纯函数不会产生副作用,这意味着不会对变量或参数进行更改。使用不可变变量可以帮助实现纯函数,因为我们可以防止副作用。纯函数,因为它具有可预测的结果,所以可以被“证明”,这意味着我们可以根据输出进行测试,以确保函数按预期工作。这种使用自动化的测试重复性可以产生更健壮的程序。
要在程序执行期间对变量进行更改,我们必须指定它是可变的,这意味着我们期望它会更改。的变量设置了一个可变变量无足轻重的人
关键字。在这个程序的主函数中声明的两个变量都是可变的。其中一个变量就是游戏网格。这个必须是可变的因为我们会不断改变所有的值从一代到另一代。单元格会死亡和诞生,所以我们需要改变数组中每个位置的值。另一个变量是代计数。这并不是一个绝对必要的值,但是当游戏经过一代又一代的迭代时,我们能够追踪我们所处的代数。因为我们要在每一代之后增加这个值,所以它必须是可变的。
总是值得考虑是否必须有一个可变或不可变的值。如果你只设置一次就不再碰它,你不需要让它可变。您可以通过使程序保持不可变来保护程序。这就是编译器有用的地方。如果你在一个变量上设置了一个值,你已经指出它是可变的,然后不改变它,编译器会提示你,它可能应该保持不变。类似地,如果你有一个根本不应该改变的值,让它保持不变,如果程序的任何部分试图改变它,编译器将会抱怨它。
这个编译器错误可以帮助您更快地跟踪错误,因为您的编译将会失败,并且您必须决定变量是否可以更改,或者值是否应该从一开始就不发生变化。如果编译器没有在你身上出错,当你不希望改变的值被改变时,你的程序中就会有一个错误。正是这种显式编程可以导致更健壮的程序——如果您想要更改一个值,您必须考虑它,然后指出该值将在某个时刻更改。
数据类型
游戏网格本身就是我们明确要使用的数据类型的地方。如前所述,Rust可以根据放入变量中的值推断数据类型,但我们也可以显式地对此进行说明,您可以在声明中看到世界
变量。除了是一个数组,我们很快就会讲到,你还可以看到世界
标识符有一个有趣的符号,表示数据类型可以是或应该是。你会看到0与
.Rust是一种强类型语言,您不能随意地从一种类型转换到另一种类型。
08表示我们将用0值填充这个变量但是0值将是一个无符号的8位整数。这允许我们在告诉Rust(在本例中是编译器)期望的数据类型的同时初始化值。这意味着我们从未期望在这个字段中获得大于255的值。因为它是无符号的,所以我们不需要容纳有符号的位,所以我们可以在u8数据类型中接受从0到255的值。正如您所期望的,如果我们可以支持unsigned,那么我们也可以支持signed。一个有符号的8位整数将由i8声明。
这是Rust允许您随心所欲地明确的另一个领域。根据内存需求,可以为整数值选择所需的大小。您可以使用8位、16位、32位、64位或128位值,包括有符号和无符号。这意味着您可以将变量声明为i8、i16、i32、i64、i128、u8、u16、u32、u64或u128。您还可以指定浮点值的大小,尽管您只能选择f32和f64。默认的浮点大小是64位,因为它在现代处理器上没有性能损失,但具有更高的精度。
不过,我们并不局限于数字。我们也可以创造字符值。Rust中的char是一个4字节的值,它允许支持Unicode值以及重音和表情符号字符。在Rust中执行以下操作是完全合法的,前提是你的编辑器允许你输入这个字符:
让emo_char=“☺;
另一种常见的数据类型是布尔值。用于逻辑运算的布尔值将计算为true或false。如果我们想创建一个布尔值并使用显式类型注释,我们可以使用下面的语句:
让yes_no:保龄球=真正的;
该语句允许我们在设置值的同时声明数据类型。习惯了其他语言的人可能会发现很难习惯使用英语变量:数据类型
等号前面的符号,用于设置值。如果您习惯于C、c++、Java、c#和其他需要在变量名前面的左侧指出数据类型的语言,那么最初阅读它也可能具有挑战性。在本例中,Rust使用关键字让
为了表示这里有一个变量,它需要另一种声明变量的方式。用起来可能会更尴尬让
数据类型变量
=
价值
.不管怎样,我们都没有投票权,所以你只能接受让
数据类型变量
=
价值
就像你声明和设置变量的初始值一样。
值得注意的是,我们创建的变量是不可变的,因为您不希望这变成一个陷阱。该值不可修改。这也提高了命名的重要性。使用变量名yes_no
在布尔值被设置为true后就不能改变,这并不是一个很好的命名方式。实际上,它总是会是肯定的,永远不会是否定的。因此,从我们之前的声明中可以得到两点教训。总是要考虑是否要让一个变量成为可变的,然后确保你给了这个变量一个有意义的名字,这样你以后可以阅读和理解它。或者,也许其他人可以阅读并理解它。
数组
我们要处理的变量之一是一个数组。更具体地说,它是一个多维数组。数组本身不是数据类型。这是一个原始的数据结构。有更好的方法来处理紧密相关的数据,并且您希望能够直接处理它,例如遍历整个数据流或直接到特定值。问题在于,其他处理此数据结构的方法都无法处理多维。想象一个一维数组,或者更好一点,看看图1.1,它显示了一个一维数组。这将是一个连续的内存块,您将在其中存储许多值。
图1.1:一维数组
这里需要考虑的一个重要方面是,当我们使用数组时,所有值都将具有相同的数据类型。在我们的例子中,我们有一个无符号8位值的集合。实际上,我们只会用到两个值。我们可以使用一个布尔值数组,真或假,但使用无符号整数意味着我们可以直接用我们拥有的值做算术。这给了我们两种方法来跟踪我们的单元格有多少邻居——我们只是把所有的值加起来,或者我们检查是否有一个值,然后增加。对于我们的“世界”,我们将使用一个多维数组,它在实践中类似于图1.2,尽管实际上它只是内存的一个连续部分,就像一个一维数组一样。
图1.2:多维数组
如果是简单的,我们可以使用以下声明定义一个一维数组。它创建了一个包含15个整数的数组。注意我们指示所使用的数据类型的方式。而不是使用变量:数据类型
正如前面所做的,我们使用方括号(表示它是一个数组[]
).在方括号内包含数据类型,后面跟着分号,然后是数组的大小。如果需要,还可以包含一组初始数据。这可以用方括号内的逗号分隔的值列表来实现,例如[3,6,9,14,2,15,16,90,145]。然后就有了一个初始化的值数组。同样,没有无足轻重的人
关键字,我们不能改变任何值一旦他们已经设置,尽管你不必在声明数组时设置他们。但是,您需要做的一件事是确保您已将数组填充为您所声明的大小。
让列表:[手机等;15];
如果使用下面的代码,就会出现编译器错误。该错误会告诉您定义了一个包含15个元素的数组,但只找到了8个元素。Rust期望将一个固定大小的数组填充到数组的长度。如果你打算只使用8个值,你应该声明一个只有8个值的数组。Rust将数组的声明本质上视为一种数据类型。手机等;15
变量定义为的数据类型。任何不完全匹配的都不能通过类型检查。
让数组:[手机等;15];数组=[3.,43,12,18,90,32,8,19];
在我们的例子中,我们使用的是多维数组。如果你想在C中声明一个多维数组,你可以使用int数组[10][10]
.这表示你有一个数组,宽10个,深10个。如果你想要一个三维数组,你只需要在方括号中加上一个额外的数字。在Swift中,它看起来像var数组= Int[][]
,是一个无界多维数组。两个方向都没有指定大小。在Rust中,我们不需要关闭方括号来创建额外的维度。使用以下代码创建多维数组:
让数组:[15];15];
这是一个没有指定数据类型的数组。如果要指定数据类型,则需要初始化数组。选择一个值,然后选择数据类型,就像我们在这里所做的那样。这意味着[[95 u16;10);10]
如果你想要一个无符号16位整数数组,在所有单元格中都放置值95。另一种选择是不声明数据类型,让Rust在真正初始化它时推断它。我们会讲到一种方法来初始化数组。
要访问数组元素,可以使用[]
符号。如果你想要到达数组中的位置5,你可以使用数组[4]
,请记住,数组是基于0的,这意味着您开始访问索引为0的数组。如果你有一个数组,你定义为[15]
,您将使用值0-14访问这15个元素。尝试使用[15]
在数组中获取一个值会产生一个错误,因为你已经超出了数组的定义边界。
控制结构
任何编程语言都需要有控制结构。程序员不能只靠变量、声明和函数生存。我们需要像条件句这样的东西来进行比较,并根据比较做出决定。这可能是一个如果
语句,例如。我们还需要循环。对于这里的主函数,我们来看一种类型的循环,也就是a为
循环。一个为
循环可以使用一个计数器,该计数器在每次循环时递增。对于数组,我们可以使用循环计数器作为数组的索引。你可以在下面的代码中看到:
为我在0..74点{为j在0..74点{如果兰德::随机(){世界[我][j]=1;}其他的{世界[我][j]=0;}}}
我们先分析其中一个然后再讨论为什么会有两个。这条线是对于I在0..74年{
.的74年0 . .
是从0开始到74结束的所有整数值的集合。的..
取值范围。因为我们要用到这个变量我
作为数组的索引,我们需要从0开始。我们不必因为它是a就从0开始为
循环。这将类似于下面的C循环,它做同样的事情,但只是用小于或大于来表示范围:
为(我=0;我<75;我++){
在编写for循环时,Rust更接近Python,而不是C。在Python中,相同的循环看起来像下面的行。在Rust中,范围用75年0 . .
,其中Python使用关键字范围
,它生成一个从0开始,以传递到的值结束的值范围范围
并且每次都加1为
循环。行为与为
之前用Rust编写的循环。
为我在范围(74):
我们使用嵌套循环,这意味着我们有两个独立的为
循环。如果没有嵌套循环,我们最终会得到一条穿过二维数组的对角线,因为x轴和y轴上使用的值相同。在这种情况下,我们使用我
作为行计数器和j
作为列计数器。对于每一次迭代我
,通过遍历每个可能的列来遍历整行j
变量。说到变量为
语句自动声明并创建我
而且j
对我们来说。您还会注意到无足轻重的人
关键字在这两个变量的创建中是隐式的,因为在值的范围内迭代时,值必须改变。如果循环索引不迭代,循环就不能很好地工作。想想同一个循环的C实现。如果你离开我+ +
,它会增加索引值,循环将无限地进行下去,因为使循环继续进行的条件是(我< 74
)会一直遇到我
不会增加。它保持在0而不增加。
创造世界的核心在于我们所使用的循环。下面是它的代码,它使用随机值来确定细胞在初始生成中是活的还是死的。我们调用函数随机()
从rand crate中取出,我们把它放在程序的顶部。这个函数生成一个布尔值。这就引出了另一种控制结构。我们正在使用如果
作为一个决策点。如果我们能从兰德:随机()
,然后我们将单元格的值设置为1。否则,我们将单元格的值设置为0。的其他的
关键字表示如果第一个条件不为真,则执行所包含的代码块。使用其他的
这样我们就不用再用另一个条件了。我们唯一关心的是其他的
语句是第一个条件是否为真。
如果兰德::随机(){世界[我][j]=1;}其他的{世界[我][j]=0;}
你可能会注意到初始条件周围没有括号。你会发现这在Rust编程中很常见。事实上,如果你包含了它们,Rust编译器会告诉你不需要它们。作为一个几十年来一直在用多种语言编写程序的人,我发现括号的使用澄清了我的表达式的逻辑。有些语言要求将表达式放在括号中。Rust不是其中之一。不要使用括号,除非你必须使用它们才能从复杂的布尔表达式中得到正确的值。
要设置多维数组中每个单元格的值,我们使用两组方括号来表示单元格的行和列。同样,我们使用索引值我
而且j
来表明我们在我们正在创造的“世界”中所处的位置。
虽然这是这个版本的程序中主函数的结尾,但创建一个功能完整的程序还缺少一些部分。我们还有一对函数要讨论,但它们都没有被调用。事实上,您编写的代码从未被调用,也会从Rust编译器生成警告。Rust希望您知道,您可能应该调用花时间编写的函数,以确保将函数放入正确的程序中。然而,即使我们还不打算调用这些函数,我们也将继续讨论它们,以便在本章结束后,您可以使用更广泛的颜色来编写自己的程序。
看更多的函数函数
我们将看看Game of Life程序的两个附加函数。这将带来您需要理解的函数的另外两个特性。第一个是从函数返回值。这是编程语言中函数的一个共同特性。您不能只是调用一个函数来引入一段代码,即使它是您想重用的代码。最终,该函数可能会创建一个需要返回给调用函数的新值。调用函数需要该返回值来做出决定。当然,为了让函数执行有意义的任务,它需要数据。这意味着我们必须能够将数据传递给函数,以便函数能够对数据进行操作。我们必须将参数传递给函数,这是我们在主函数中没有做的事情。
返回值
我们要看的下一个函数叫做人口普查()
,计算世界上所有活细胞的数量。这与Game of Life程序的必要功能没有任何直接关系,但这是一个有用的统计数据。我们想知道我们的世界何时会变得无人居住,是否会达到那个地步。如果我们的世界真的变得完全无人居住,那么就应该停止连续繁殖,因为根据游戏规则,细胞在没有邻居的情况下生长是不可能的。可以检查返回值,看看它什么时候变成0,游戏可以在那一点上停止,就像使用人口普查
从纯粹的游戏玩法角度来看。
就我们的目的而言,它让我们有机会讨论返回值。以下是人口普查
函数,它包括顶部的一行,告诉我们将返回一个值。从这个角度来看,重要的部分是在函数声明的结尾,- > u16
.这告诉我们函数将返回一个无符号的16位整数。这是一个简单的返回类型。实际上,您可以返回任何可以用作变量的值。
fn普查(_world:[[与;75];75])->u16{让我数数=0;为我在0..74点{为j在0..74点{如果_world[我][j]= =1{数+ =1;}}}数}
在像C这样的语言中,必须使用a之类的东西表明返回的是一个值返回
关键字。Rust不使用这个。要从函数返回值,只需在函数末尾的一行中单独提供要返回的值或变量。这是因为Rust被认为是面向表达式的语言。在面向表达式的语言中,每个结构或块都被认为是一个表达式,作为表达式,它产生一个值。因为函数在Rust中是一个表达式,所以它会产生一个值。对于Rust函数,表达式计算的值是该函数的最后一行。
请注意
到目前为止,我们还没有讨论一个重要的构造,但由于它对开发程序不是必需的,我们可以在这里把它作为一个注释。在编写程序时,应该使用注释。这不是一个必要的任务,因为正如你所看到的,到目前为止,没有任何Rust代码被注释在代码中。所有的注释都出现在书的文本中,所以代码注释在这一点上似乎是多余的。编写注释很简单,我们使用一种通用的方法。当您想插入一行注释时,可以使用//
然后在它们后面加上注释。从//
的末尾是一个注释,无论在该行的哪个位置//
放置。你也可以使用///
如果您想使用文档注释。使用文档注释(您可以在其中使用Markdown进行格式化)使您能够仅通过运行就为项目生成文档货物医生
.的货物
实用程序为您创建文档,并将其放入目标/医生
.
可以这样考虑,因为您可能不太熟悉面向表达式的语言。你在Rust中所做的一切都是为了创建某种结果。你所做的所有“事情”——设置变量、引入控制结构、调用函数——当它们返回值时都是表达式。您可能熟悉的大多数编程语言都使用语句。语句不返回值。在Rust中,我们可以并且确实使用语句。表达式和语句之间的一个区别是分号的使用。您可能已经注意到,在这一行的末尾没有分号数
在函数的末尾。这是因为函数是一个表达式,返回值是变量命名中的任何东西数
.因为面向表达式的语言不同于其他语言,或者至少可以不同于其他语言,所以我们将继续回到这个概念,以便您能够理解面向表达式的语言和面向语句的语言之间的区别。
这里提供的其余函数相当简单,特别是因为它包含了我们已经看到的贯穿整个世界或游戏网格的嵌套循环。注意一点,如果你不熟悉的话,就是我们增加变量中存活细胞数量的那一行数
.我们使用数
+= 1,这是一种简写方式Count = Count + 1
.而不是输入额外的字符,特别是重复变量的名称,我们只是使用一个简写符号,它的计算结果是相同的。最后,无论用哪种方式写,结果都是一样的。两种都可以。这是一种在C语言中使用了几十年的增量变量的编写方法,并被其他几种类似C的编程语言所借鉴。
我们在Rust中可以做的一件事是在其他语言(如C)中无法做到的,那就是返回多个值。这是通过使用元组来实现的。一个元组从数学上讲,是一个有限有序的列表。就我们的目的而言,它是一个有限的列表。有序部分只在您需要知道值的顺序时才有意义。这并不是说它必须按照通常的顺序(最小整数到最大或字母数字顺序)进行排序。我们需要做的是将值拉回来,因为我们没有命名它们。
要以元组的形式返回一个值,你可以使用逗号分隔的列表,用圆括号括起来:(val1, val2, val3)
.当需要检索另一端的值时,也就是调用函数的地方,您可以使用与函数末尾相同的方式使用元组。在这里,你可以看到如何从一个返回元组的函数中检索值:
让我:手机等;让b:保龄球;(我,b)=function1();
我们没有讨论的这个函数的另一个方面也在声明行中,但我们可以将其保存到下一节。
传递参数
这将是Life程序的最后一个函数。我们要看看我们是如何在整个世界中根据游戏规则来决定哪些细胞存活和死亡。有几种方法可以解决这个问题。这个实现假设世界是有界的,而不是围绕着它自己。这是主要的情况,因为我根本无法想象如何在连接顶部和底部的同时,将一个二维网格的左端与右端连接起来。这就是使用笛卡尔坐标的问题,假设二维数组的左上角是其他所有东西都相对的不动点。这是我们最终使用较长的实现的原因之一,因为我们总是必须检查是否处于网格的边界。
fn一代(世界:[[与;75];75])->[[与;75];75]{让我去新世界吧=[[0你8;75];75];为我在0..74点{为j在0..74点{让我数数=0;如果我>0{数=数+世界[我-1][j];}如果我>0& &j>0{数=数+世界[我-1][j-1];}如果我>0& &j<74{数=数+世界[我-1][j+1];}如果我<74& &j>0{数=数+世界[我+1][j-1]}如果我<74{数=数+世界[我+1][j];}如果我<74& &j<74{数=数+世界[我+1][j+1];}如果j>0{数=数+世界[我][j-1];}如果j<74{数=数+世界[我][j+1];}newworld[我][j]=0;如果(数<2)& &(世界[我][j]= =1){newworld[我][j]=0;}如果世界[我][j]= =1& &(数= =2||数= =3.){newworld[我][j]=1;}如果(世界[我][j]= =0)& &(数= =3.){newworld[我][j]=1;}}}newworld}
我们将从函数声明开始,因为那是我们传递参数的地方。然而,随着时间的推移,这里会出现一些严重的问题,因为它们是如此复杂的问题。简单地说,要将参数传递给函数,实际上是在函数声明中声明参数。您需要指明传入的变量的名称,以便以后可以通过名称引用它。您还需要指明所使用的数据类型。
当您调用函数时,请记住调用参数(我们在这里讨论的东西)被放置在堆栈上,以便被调用的函数可以访问它们。局部变量和其他重要数据也在堆栈中。我在这里提到这个是因为声明形参的一个原因是,当函数被调用时,编译器知道应该在堆栈上为形参分配多少空间。此外,当然,编译器需要能够将声明的函数(其签名)与函数调用相匹配。如果函数调用中传递的参数与函数的签名不匹配,编译器将生成一个错误。
注意,在声明中,我们不仅将多维数组作为参数,还返回多维数组。这是有原因的。Rust是一种围绕内存安全构建的语言。有些语言使用按引用传递或按值传递的思想。按值传递意味着将值本身传递给函数。引用传递意味着将数据的内存位置传递给函数。传递的值实际上是只读的。对于只有值的函数,不能对数据进行任何更改,因此没有副作用。当函数完成并将执行传递给调用函数时,传递给函数的变量是不变的。
通过引用传递允许被调用的函数对数据进行更改,因为直接访问存储数据的内存位置提供给了被调用的函数。这将允许被调用函数对内存位置进行更改,以便当执行传递回调用函数时,被更改的值在调用函数的变量中可用。由于C编程语言允许这种类型的行为,您可以在图1.3中看到用C语言表达这种思想的简单表示。在本例中,变量名为x
创建一个具有存储位置的对象,如中间的框所示。最初,该方框包含值10。我们传递那个盒子的地址&
在…前面x
表示我们将地址而不是值)传递给函数喷火
.在喷火
时,我们通过添加*
在…前面x
.在函数体中,我们解引用变量,这意味着我们将值15分配给该地址位置。
图1.3:在C语言中通过引用传递
这在Rust中要困难得多,需要几遍解释才能理解其中的含义。在Rust中有很多方法可以来回传递值,但是Rust作为一种语言,有一个基本的设计决策使它变得更加困难。在Rust中,只有一个函数可以拥有一个变量,尽管所有权可以易手。在我们讨论这个程序上下文中所有权的含义之前,您需要更好地理解范围。
范围
作用域通常是一个简单的概念,特别是考虑到大多数(如果不是所有)编程语言都以某种方式实现作用域。简单地说,作用域是指您可以引用变量并使其被理解的空间。前面一个循环的简化版本如下所示。的变量我
这里有一个明确定义的范围。在{}
Block是变量的作用域我
.这意味着我们可以利用这个变量我
,我们的程序将愉快地编译和运行。
为我在0..74点{println!(“{}”,我);}
如果我们试图在括号内的代码块之外使用变量,编译器将生成一个错误,指出没有指定变量我
在它被引用的范围内。方法编译Rust程序时生成的错误为
前面代码中的循环,后面跟着println !(“{}”,我);
声明。代码不仅不能运行,而且不能编译。
错误[E0425]:在此范围内无法找到值“i”-->测验.rs:5:20.|5|println!(“{}”,我);|^在此范围内未找到错误:由于先前的错误而中止
作用域的规则并不总是直截了当的,尽管一旦你学会了它们,它们就很容易记住。通常,您可以说变量包含在由表示的代码块中{}
.在函数中,在函数顶部定义的任何变量在函数结束时都将超出作用域。类中包含的代码块如果
语句中所示的函数名为一代
前面和后面所示的括号如果
语句定义作用域。在本例中,该块中的代码只是数
变量。如果数
变量在括号内定义,则该变量的作用域仅在括号内。正如我们所说,当括号合上时,变量就会超出作用域。
如果我>0{数=数+世界[我-1][j];}
在Rust中,我们有一个额外的复杂性。问题的复杂之处在于所有权的变化。当我们以变量作为参数调用函数时,该变量(更具体地说,存储变量引用的数据的内存位置)成为被调用函数的属性。请记住,一旦函数结束,该函数中的所有变量都会超出作用域,这意味着它们不再可用。在函数定义中一代
,你可以看到我们传入了一个变量世界
.这是一个多维数组。只要函数一代
结束时,为该变量分配的内存空间将自动释放。由于内存已释放,因此无法再访问该内存位置的内容。
在这种情况下,我们可以通过简单地创建一个全新的变量来解决它。这是在函数的顶部完成的,您可以在下面的行中看到。我们创建了一个新的多维数组,它将被下一代游戏网格填充。一旦我们计算完谁会活,谁会死,谁会出生,这一代人就会消失。在我们的情况下,这是一个更好的解决方案,因为我们不可能在不影响世界其他地方的情况下改变当前的世界。如果我们对网格中的任何单元格进行更改,它将改变后续单元格的决定。所以,我们创建了一个全新的游戏网格,一旦我们弄清楚下一代的样子,我们就会把现有的网格换成新的网格。
让我去新世界吧=[[0你8;75];75];
为了将新的游戏网格返回给调用函数,我们将变量单独放在一行上。大多数语言使用显式返回。在C语言中,就像许多其他语言一样,如果我想将一个值从函数传递回调用函数,我使用关键字返回
如返回x
.相反,Rust使用隐式返回。函数中的最后一个值成为返回值。它单独出现在一行中,不包括分号,因为它不是语句。相反,它是一个表达式。表达式不像语句那样使用分号来结束它们。
因为这是Rust中另一个复杂的主题,我们将在接下来的章节中继续讨论它。Rust中还有其他返回值的方法。我们只是不打算在这里讨论它们,所以我们留到以后再说。
编译程序
目前我们有一个可行的计划。我们有工作代码需要被编译成一个程序。Rust不是解释型语言,所以我们需要生成一个可执行文件。在Rust中,有两种不同的方式来处理该任务。首先,Rust附带了一个编译器,可以用来编译任何源代码。Rust编译器是一个名为rustc
.这个程序可以用来从任何基本的Rust源代码文件生成可执行文件。这意味着如果你没有任何外部功能,你可以用Rust编译器编译你的程序,你会得到一个可执行文件。
您可能熟悉一些编译器,它们不使用所需程序的名称作为输出可执行文件的名称。例如,传统上,C编译器将生成一个名为a.o ut
编译时不指定输出文件名。这是可执行格式的产物,当C编译器第一次被开发出来时,通常在Unix操作系统上使用。今天,我们通常不使用thea.o ut
虽然许多C编译器仍然默认使用传统的输出文件名。Rust编译器将生成一个以您正在编译的文件命名的文件。在这里,您可以看到如何使用名为life.rs
:
kilroy@milobloom:~/Documents$ rustc life.rs美元kilroy@milobloom: ~ /文档ls洛杉矶的生活-rwxr-xr-x1热爱旅行的员工2880521月2920.生活:33美元kilroy@milobloom: ~ /文档文件生活生活:Mach-O64-bit可执行文件x86_64
然而,如果你使用货物
要创建您的源代码文件和相关的目录结构,您的源代码文件不会被命名life.rs
.它将被命名为main.rs
默认情况下。我们也可以用货物
为我们创建可执行文件。这是一个无论如何都要养成的好习惯,因为不仅会货物
进行编译,它还将带来所需的所有外部功能。我们将在后面的章节中更多地使用外部功能。但是现在,我们想要从只有一个源文件的项目构建一个可执行文件。如果我们只是逃跑货物建立
在项目目录中,货物
将负责编译和生成可执行文件,正如你在这里看到的:
kilroy@milobloom:~/文件/铁锈/生命$货物建造完成开发[未优化+ debuginfo]目标(年代)在007kilroy@milobloom: ~ /文档/生锈/美元生活cd目标/调试/kilroy@milobloom: ~ /文档/生锈/生活/目标/调试美元ls拉总计1720drwxr-xr-x@12热爱旅行的员工3841月1519: 57.drwxr-xr-x5热爱旅行的员工1601月819: 08年..-rw-r - r -1热爱旅行的员工012月3.20.: 12 .cargo-lockdrwxr-xr-x26热爱旅行的员工83212月519: 40 .fingerprintdrwxr-xr-x8热爱旅行的员工25612月519: 40构建drwxr-xr-x56热爱旅行的员工17921月1519: 57 depsdrwxr-xr-x2热爱旅行的员工6412月3.20.: 12例子drwxr-xr-x5热爱旅行的员工16012月519: 40增量-rwxr-xr-x2热爱旅行的员工8758121月1519: 57岁的生活-rw-r - r -1热爱旅行的员工991月920.: 23 life.dlrwxr-xr-x1热爱旅行的员工3112月519: 45生活。dSYM ->deps / life-1a787212c1e544bc.dSYMdrwxr-xr-x2热爱旅行的员工6412月3.20.: 12本地
如果仔细观察,您会发现dev目标是构建的对象,显示的目录是调试目录。这是默认的构建目标。您可以轻松地构建发布目标,它是一个更清晰的构建目录,如下所示。丢失的是所有的调试信息,包括包含调试符号的文件,如前面的目录清单所示,但不是下面的目录清单:
kilroy@milobloom: ~ /文档/生锈/生活/目标/调试美元cd../释放kilroy@milobloom: ~ /文档/生锈/生活/目标/释放美元ls拉总计608drwxr-xr-x@10热爱旅行的员工3201月819: 08年.drwxr-xr-x5热爱旅行的员工1601月819: 08年..-rw-r - r -1热爱旅行的员工01月819: 08年.cargo-lockdrwxr-xr-x17热爱旅行的员工5441月819: 08年.fingerprintdrwxr-xr-x6热爱旅行的员工1921月819: 08年建立drwxr-xr-x34热爱旅行的员工10881月819: 08年depsdrwxr-xr-x2热爱旅行的员工641月819: 08年的例子drwxr-xr-x2热爱旅行的员工641月819: 08年增量-rwxr-xr-x2热爱旅行的员工3053921月819: 08年生活-rw-r - r -1热爱旅行的员工1011月819: 08年life.d
您可能注意到两个清单都显示了一个增量目录。这是因为Rust编译器能够执行增量构建来加快构建过程。在增量编译中,Rust编译器只在源文件中构建更改。
现在我们有了可工作的源代码和两种将源文件构建为可执行文件的方法。如果您愿意,您可以运行生成它的程序。它不会做任何有趣的事情,但它可以被执行。我们有了一个很好的起点,可以在我们讨论过的不同Rust语言特性的基础上构建更多的项目。
总结
Rust是一种据说是类C语言的语言,但C和Rust之间存在显著差异,就像Rust和其他类C语言(如Python)之间存在差异一样。当我们说类似c语言时,我们的意思是语法相似,这意味着在程序中用于引起行为或操作的关键字本质上是相同的。这包括控制结构,如为
而且如果
.有一些小的差异,虽然不显著。
类c语言和Rust之间最大的区别之一在于变量。首先,当我们声明变量时,我们使用关键字让
而不是只使用数据类型和变量名。更重要的是,默认情况下变量不能被更改。变量声明后,若要对其进行任何更改,需要使用无足轻重的人
关键字。这告诉Rust编译器变量可以稍后更改,这意味着如果编译器看到任何更改变量的尝试,它将不会生成错误。
除此之外,Rust中的一个重要概念是所有权。记住Rust开发中的一个基本概念是内存安全。作为一种在开发时就知道并行处理或并行处理是可能的语言,Rust语言的创造者决定在任何给定时间只有一个函数可以拥有一个变量。这意味着任何变量一旦被传递给另一个函数,本质上就超出了作用域。这并不意味着该值消失了——只是能够使用变量名的别名引用该值。
说到函数,Rust当然也有函数。正如您所期望的,函数可以接受参数并返回值,包括元组,这是值的有序集合。Rust可以是一种面向表达式的语言。说到编程语言,就有语句和表达式。表达式的计算结果是一个值。语句执行操作。语句不会求值。即使设置一个变量也不会得到一个值。一个值被放置到变量名所引用的内存位置,但这与实际求值不同。如果你用这样的语句X = 10;
,值10被放入x
,但没有剩余的结果值。
如果你熟悉Unix命令行,你可能会意识到,如果你要运行一个程序,你会在程序运行结束时得到一个返回值。这会留下一个值,不管它是否被使用。这不是一个完美的类比,但它是相似的。如果X = 10;
留下一个可以检查的值——例如,表示赋值成功或失败的1或0——它可以被认为是一个表达式。
所有这些都说明Rust通过表达式使用隐式返回。没有返回
语句在函数的末尾显式返回一个值。相反,您将任何可以计算为值的内容(包括裸值或变量)单独留在一行中。另外,请记住,表达式不使用分号来结束行。表达式不像语句那样终止。
在下一章中,我们将扩展Life,并花更多时间研究函数调用所有权的影响,并尝试在函数调用后重用变量。
练习
- 将网格的大小更改为75×75以外的大小。请记住,您需要找到您已经声明了生命网格的所有地方。
- 调用
一代
函数一次。记住一代
函数返回一个值,因此需要创建一个变量来保存调用产生的新网格一代
.
额外的资源
- 康威的生命游戏(来自《科学美国人》)-
www.ibiblio.org/lifepatterns/october1970.html
- 康威的人生游戏-
pi.math.cornell.edu/ ~脂肪酶/ mec / lesson6.html
- 生命的游戏是什么?-
www.math.com/students/wonders/life/life.html
- 计算机编程-变量-
www.tutorialspoint.com/computer_programming/computer_programming_variables.htm
- 计算机编程-函数-
www.tutorialspoint.com/computer_programming/computer_programming_functions.htm