rust语言与go语言
正如我在本系列的第一部分中提到的,我真的很喜欢Rust。 这种静态编译的语言是内存安全的,并且与操作系统无关,因此可以在任何计算机上运行。 Rust为您提供了系统语言的速度和低级的好处,而无需像C#和Java这样的讨厌的垃圾收集方法。
没有比真正开始使用语言更好的学习语言的方法。 本文通过向您展示如何使用该语言构建简单的井字游戏,来帮助您使用Rust。 遵循以建立自己的有趣的游戏。
先决条件
首先阅读本系列的第一部分,Rust入门指南。 我将向您展示如何安装和运行Rust,描述其核心功能,并向您介绍入门所需的概念。 在本文中,我不会描述该语言的所有方面,因此您需要掌握该语言的基础知识。
开始项目
首先,您需要设置您的项目。 您可以使用Cargo在终端上创建新的可执行二进制程序:
$ cd ~/Documents
$ cargo new tic_tac_toe –bin
在树形程序中,新的tic_tac_toe目录如下所示:
$ cd tic_tac_toe
$ tree .
.
??? Cargo.toml
??? src
??? main.rs
main.rs文件应包含以下几行:
fn main() {
println!("Hello, world!");
}
如清单1所示,运行程序与创建程序一样容易。
清单1.运行“ Hello,World!”
$ cargo build
Compiling …
Finished …
$ cargo run
Finished …
Running …
Hello, world!
现在,您还需要一个用于游戏模块的文件。 通过执行以下命令行来创建此文件:
$ touch ./src/game.rs
通过项目和目录设置,您可以深入了解游戏概述。
用类型和结构计划游戏
经典的井字游戏由两个主要部分组成:一个棋盘和一个针对每个玩家的回合。 棋盘实质上是一个空的3x3阵列,而转牌则指示哪个玩家必须移动。 要转换此功能,必须编辑在上一节中创建的game.rs文件(请参见清单2)。
清单2.为板和玩家回合修改的Game.rs
type Board = Vec<Vec<String>>;
enum Turn {
Player,
Bot,
}
pub struct Game {
board: Board,
current_turn: Turn,
}
您可能已经在这里注意到了奇怪的语法,但是请不要担心:我将在本文中进行描述。
董事会
要翻译游戏板,请使用type
关键字为名称Board
别名,以使其与Vec<Vec<String>>
类型同义。 现在, Board
是一个二维字符串矢量的简单类型。 我在这里使用char是因为数组中唯一的值将是x
, o
或一个指示未平仓头寸的数字。
转弯
回合只是表明哪个玩家必须选择位置,因此enum
结构可以完美地发挥作用。 在每个回合中,只需匹配Turn
变体即可进行适当的方法调用。
游戏
最后,您必须创建一个Game
对象,该对象包含棋盘和当前正在玩的回合。 可是等等! Game
结构的方法在哪里? 不用担心:那就是下一个。
实施游戏
井字游戏由哪些方法组成? 好吧,有转弯。 在每一回合中,显示棋盘,玩家移动,再次显示棋盘,并检查获胜条件。 如果游戏获胜,游戏会宣布哪个玩家获胜,并要求他或她再次玩。 如果没有人赢得比赛,则游戏将切换当前玩家并进行下一回合。 显然,每个动作内部都有更好的问题,具体取决于玩家,但是您可以从这里潜入。
首先,创建一个嵌套在impl
块中的构造,如清单3所示。
清单3.游戏构造
impl Game {
pub fn new() -> Game {
let first_row = vec![
String::from("1"), String::from("2"),
String::from("3")];
let second_row = vec![
String::from("4"), String::from("5"),
String::from("6")];
let third_row = vec![
String::from("7"), String::from("8"),
String::from("9")];
Game {
board: vec![first_row, second_row, third_row],
current_turn: Turn::Player,
}
}
}
静态方法new
创建并返回Game
结构。 这是Rust中对象构造函数的标准名称。
您必须将board
成员变量与String
对象的2d
向量绑定。 请注意,我没有用空白填充每个位置,而是用一个数字来指示每个动作的可用位置。 接下来,将current_turn
成员变量绑定到Turn::Player
的值。 这条线意味着每个游戏都让玩家先行。
您如何玩游戏?
第一种方法用作程序的映射。 您添加内这种方法impl Grid
块(连同方法在本节的其余部分)。 清单4显示了该方法。
清单4.游戏程序图
pub fn play_game(&mut self) {
let mut finished = false;
while !finished {
self.play_turn();
if self.game_is_won() {
self.print_board();
match self.current_turn {
Turn::Player => println!("You won!"),
Turn::Bot => println!("You lost!"),
};
finished = Self::player_is_finished();
self.reset();
}
self.current_turn = self.get_next_turn();
}
}
很容易看到游戏流程。 使用无限循环,您可以从一圈转向下一圈,交替使用current_turn
。 因此,您可以对self
使用可变借项,因为游戏的内部状态每回合都会改变。
该enum
已经获得回报,因为如果赢得了这场比赛,则会嵌入有关谁赢得比赛的信息。 然后,您让玩家知道他或她赢了或输了。 此外,您还可以将板重置为原始状态,这对于用户再次玩游戏很有用。
请注意,这是除new
之外的唯一pub
方法。 这意味着play_game
和new
是另一个库在使用Game
对象时可以访问的唯一方法。 所有其他静态或静态方法都是私有的。
扭转潮流
play_game
方法中使用的第一个辅助方法是play_turn
。 清单5显示了这个漂亮的小功能。
清单5. play_turn函数
fn play_turn(&mut self) {
self.print_board();
let (token, valid_move) = match self.current_turn {
Turn::Player => (
String::from("X"), self.get_player_move()),
Turn::Bot => (
String::from("O"), self.get_bot_move()),
};
let (row, col) = Self::to_board_location(valid_move);
self.board[row][col] = token;
}
这是一个棘手的问题。 首先,您要打印电路板,以便用户知道可用的位置(即使是在机器人的时候也很有用)。 接下来,根据current_turn
的变体,使用元组valid_move
和match
分配变量token
和valid_move
。
token
是玩家或机器人的String X
或O
valid_move
是1到9的整数,他在板上的位置没有被占用。 然后,使用to_board_location
静态方法将此变量转换为电路板的相应行和列。 ( Self
,以一个大写字母“S”,返回类型的self
-in这种情况下, Game
。)
让我们看看那个板子
现在您已经设置了play_turn
,您需要一种打印方法。 清单6显示了该方法。
清单6.打印游戏板
fn print_board(&self) {
let separator = "+---+---+---+";
println!("\n{}", separator);
for row in &self.board {
println!("| {} |\n{}", row.join(" | "), separator);
}
print!("\n");
}
在这种方法中,使用for
循环在板上打印ASCII表示的行。 临时变量row
是对电路板上每个向量的引用。 使用join
方法,您可以将row
转换为String
并使用附加的分隔符String
打印该新值。
现在可以使用打印功能,您终于可以继续为播放器和机器人获取有效的移动了。
玩家,该你了
到目前为止,该程序是一系列硬编码的返回,没有播放器的输入。 清单7对此进行了更改。
清单7.设置转弯
fn get_player_move(&self) -> u32 {
loop {
let mut player_input = String::new();
println!(
"\nPlease enter your move (an integer between \
1 and 9): ");
match io::stdin().read_line(&mut player_input) {
Err(_) => println!(
"Error reading input, try again!"),
Ok(_) => match self.validate(&player_input) {
Err(err) => println!("{}", err),
Ok(num) => return num,
},
}
}
}
该方法的核心归结为:除非玩家为游戏提供有效的动作,否则它会无限循环。
用户提示后的第一个匹配表达式尝试将用户的输入读入String
( player_input
,并检查这样做是否发生错误。 io
模块提供了此功能。 您必须将此模块导入game.rs文件顶部 。 其stdin().read_line
方法( stdin()
返回当前标准输入的句柄对象)。 这是我导入的io
模块:
use std::io;
同样重要的是要注意,当read_line
方法使给定的String
突变时,它还会返回一个称为Result
的enum
。 我在介绍性文章中没有谈论Result
,因此我在后面进行讨论。
结果枚举
Result
就是所谓的代数类型。 这个enum
有两个变体: Ok
和Err
。 每个变体都可以保存数据,例如String
或i32
。
在read_line
情况下,返回的Result
是io
模块的特殊版本,这意味着Err
是特殊的io::Error
变体。 相反, Ok
与原始Result
变体相同,并且在这种情况下,保留一个整数,该整数代表读取的字节数。 Result
是一个有用的enum
,有助于确保您在编译时而不是运行时处理所有可能的错误。
在Rust中普遍存在的另一个同级enum
是Option
。 代替Ok
和Err
,它的变体是None
(不保存任何数据)和Some
(可以保存)。 Option
在C++
中的nullptr
或Python中的None
有用的方式中很有用。
Option
和Result
什么区别,什么时候应该使用它们? 这是我的最佳答案。 首先,如果您期望函数什么都不会返回,则使用Option
。 将Result
用于您希望一直成功但可能失败的函数,这意味着必须捕获错误。 得到它了? 大。 返回get_player_move
方法。
回到游戏
我停止阅读播放器的输入。 如果在读取用户输入时发生错误,程序将通知用户并要求他或她再次输入。 如果没有错误发生,则程序到达第二个match
表达式。 注意下划线( _
)的使用:它们告诉Rust您没有在Result
的Ok
或Err
变体中绑定数据,而在第二个match表达式中进行了绑定。
该match
表达式检查player_input
变量是否有效。 如果不是,则代码返回错误(游戏会提醒玩家注意),并要求玩家输入有效的信息。 如果player_input
有效,则返回使用validate
方法转换为整数的输入。
验证您的代码
编写了游戏的核心后,最好编写一个validate
函数。 清单8显示了代码。
清单8. validate函数
fn validate(&self, input: &str) -> Result<u32, String> {
match input.trim().parse::<u32>() {
Err(_) => Err(
String::from(
"Please input a valid unsigned integer!")),
Ok(number) => {
if self.is_valid_move(number) {
Ok(number)
} else {
Err(
String::from(
"Please input a number, between \
1 and 9, not already chosen!"))
}
}
}
}
逐行运行此输出,这是方法的要点。
首先,程序返回一个Result enum
。 我没有介绍类型模板,但是基本上,您是在说Result
的Ok
变体必须包含u32
整数,而Err
变体必须包含String
。 为什么Result
在这里返回? 好吧,仅当给定输入为:该方法才有望通过并引发错误:
- 不是整数;
- 由于占用原因,不是有效的地点; 要么
- 无效的位置,因为整数不是1–9。
接下来,程序尝试使用input
的parse
方法将input
转换为u32
。 turbofish, ::<type>
是某些函数的一个特殊方面,它告诉他们要返回什么类型。 在这种情况下,它同时告诉parse
尝试将input
转换为u32
并将Result
的Ok
变量设置为容纳u32
。 如果无法转换input
,则代码将返回错误,指示input
不是无符号整数。 但是,如果转换成功,代码将通过另一个帮助器函数is_valid_move
传递input
。
为什么还有另一个辅助功能用于验证? 在较早的可能错误列表中,数字1是特定于用户的。 机器人将始终给出一个整数。 因此,您仅使用validate
来验证玩家的React。 is_valid_move
检查其他两个可能的错误。
清单9显示了验证代码的最后一部分。
清单9.更多验证
fn is_valid_move(&self, unchecked_move: u32) -> bool {
match unchecked_move {
1...9 => {
let (row, col) = Self::to_board_location(
unchecked_move);
match self.board[row][col].as_str() {
"X" | "O" => false,
_ => true,
}
}
_ => false,
}
}
很简单。 如果给定的unchecked_move
不在1到9(含)之间,则这不是有效的移动。 否则,代码将被强制检查是否已经进行了移动。 像之前在play_turn
,您可以将unchecked_move
转换为板上的相应行和列。 然后,您可以检查该位置是否在板上。 如果位置是X
或O
,则移动无效。
上机器人
在继续编写方法以使机器人行动之前,创建清单10所示的to_board_location
静态方法。
清单10. to_board_location方法
fn to_board_location(game_move: u32) -> (usize, usize) {
let row = (game_move - 1) / 3;
let col = (game_move - 1) % 3;
(row as usize, col as usize)
}
这种方法有点作弊的,因为你知道,当to_board_location
就是所谓的validate
和play_turn
,参数game_move
为1和9(含)之间的整数。 您将此方法设置为静态,因为数学与Game
对象没有联系。 井字游戏板始终为3x3。
聊天机器人
您的代码可以从玩家那里获得成功,但可以考虑使用机器人。 首先,漫游器的举动应该是随机数,这意味着您需要导入第三方板条箱rand
。 其次,使用is_valid_move
方法继续生成此随机移动,直到到达有效位置is_valid_move
。 然后,游戏必须通知玩家机器人采取了什么行动,并退还该行动。
您可以将rand
板条箱导入并安装在名为Cargo.toml的文件中,并将rand
作为依赖项。 清单11显示了该文件。
清单11. Cargo.toml
[package]
name = "tic_tac_toe"
version = "0.1.0"
authors = ["Dylan Hicks <dirtgrub.dylanhicks@gmail.com>"]
[dependencies]
rand = "0.4"
main.js文件告诉Cargo您要使用此依赖项。 我将此命令放在文件的顶部:
extern crate rand;
然后,将此命令放在io
import上方game.rs文件的顶部:
use rand;
使用rand
crate生成一个随机数,您需要一种方法来从机器人中获得成功。 清单12显示了该方法。
清单12. bot_move方法
fn get_bot_move(&self) -> u32 {
let mut bot_move: u32 = rand::random::<u32>() % 9 + 1;
while !self.is_valid_move(bot_move) {
bot_move = rand::random::<u32>() % 9 + 1;
}
println!("Bot played moved at: {}", bot_move);
bot_move
}
那很轻松,对吗?
该方法结束了play_turn
方法的依赖关系。 现在,您需要制定一种方法来检查游戏是否获胜。
我们是冠军
现在,您将快速而轻松地使用布尔代数( 清单13 )。
清单13.布尔布尔代数
fn game_is_won(&self) -> bool {
let mut all_same_row = false;
let mut all_same_col = false;
for index in 0..3 {
all_same_row |=
self.board[index][0] == self.board[index][1]
&& self.board[index][1] == self.board[index][2];
all_same_col |=
self.board[0][index] == self.board[1][index]
&& self.board[1][index] == self.board[2][index];
}
let all_same_diag_1 =
self.board[0][0] == self.board[1][1]
&& self.board[1][1] == self.board[2][2];
let all_same_diag_2 =
self.board[0][2] == self.board[1][1]
&& self.board[1][1] == self.board[2][0];
(all_same_row || all_same_col || all_same_diag_1 ||
all_same_diag_2)
}
在for
循环中,您同时检查行和列以查看是否满足了Tic-Tac-Toe的获胜条件(即,连续三个X或Os)。 您可以使用|=
执行此操作,就像+=
,但是它使用or运算符代替加法运算符。 然后,您检查两个对角线是否都相同。 最后,通过使用布尔布尔代数返回是否满足任何获胜条件。 另外三种方法,您已完成。
您想再玩一次吗?
如果你回头看看play_game
的方法清单4 ,您会看到该代码不断循环,直到finished
是true
。 仅当方法player_is_finished
为true
时,才会发生这种情况。 此方法应基于玩家的响应:是或否( 清单14 )。
清单14. player_is_finished方法
fn player_is_finished() -> bool {
let mut player_input = String::new();
println!("Are you finished playing (y/n)?:");
match io::stdin().read_line(&mut player_input) {
Ok(_) => {
let temp = player_input.to_lowercase();
temp.trim() == "y" || temp.trim() == "yes"
}
Err(_) => false,
}
}
当我最初编写此方法时,我决定最好只处理玩家输入的“是”情况,这意味着所有其他输入都返回false
。 同样,这是一种静态方法,因为它没有使用任何self
携带的数据。
硬重置可修复所有问题
reset
play_game
使用的最后一种方法,如清单15所示。
清单15.重置方法
fn reset(&mut self) {
self.current_turn = Turn::Player;
self.board = vec![
vec![
String::from("1"), String::from("2"),
String::from("3")],
vec![
String::from("4"), String::from("5"),
String::from("6")],
vec![
String::from("7"), String::from("8"),
String::from("9")],
];
}
此方法所做的全部工作就是将游戏的成员变量设置回其默认值。
完成游戏所需的最后一个方法是get_next_turn
,如清单16所示。
清单16. get_next_turn方法
fn get_next_turn(&self) -> Turn {
match self.current_turn {
Turn::Player => Turn::Bot,
Turn::Bot => Turn::Player,
}
}
此方法仅检查打开了哪些self
,然后返回相反的self
。
运行并编译游戏
在game.rs模块完成之后,main.rs现在可以编译和玩游戏了( 清单17 )。
清单17.编译游戏
extern crate rand;
mod game;
use game::Game;
fn main() {
println!("Welcome to Tic-Tac-Toe!");
let mut game = Game::new();
game.play_game();
}
而已。 您只是使用mod
声明了该模块中存在游戏模块,并通过use
将Game
对象引入了作用域。 然后,您使用Game::new()
创建了一个game
对象,并告诉该对象玩游戏。 现在,使用Cargo运行它( 清单18 )。
清单18.运行游戏
$ cargo run
Compiling tic_tac_toe v0.1.0 …
Finished dev [unoptimized + debuginfo] …
Running …
Welcome to Tic-Tac-Toe!
+---+---+---+
| 1 | 2 | 3 |
+---+---+---+
| 4 | 5 | 6 |
+---+---+---+
| 7 | 8 | 9 |
+---+---+---+
Please enter your move (an integer between 1 and 9):
…
最后的想法
正如您在本教程中所学到的那样,Rust是一种通用语言,它易于使用Java, C#
或Python,但具有C
或C++
的速度和功能。 该代码不仅可以快速编译,而且所有内存和错误问题都在编译时而不是在运行时处理,从而减少了代码中可能出现的人为错误。
下一步
- 要查看我为本文创建的代码,请访问我的GitHub存储库
翻译自: https://www.ibm.com/developerworks/opensource/library/os-using-rust/index.html
rust语言与go语言