BEGINNING RUST PROGRAMMING
Author: Ric Messier
如果需要电子书的小伙伴,可以留下邮箱,看到了会发送的
Chapter2 Extended Life
UNDERSTANDING OWNERSHIP
首先需要解释 Rust 中的标识符是如何工作的,这与其他语言处理变量的方式类似,只不过它更加明确,而且在某些方面与不同的元素有关。
当我说元素的时候,我们实际上有三个要关注的元素。
- 第一个是标识符的名称;
- 第二个是标识符名称指出的内存位置,这意味着您可以将标识符的名称视为该内存位置的别名。
- 最后,我们得到了存储在内存位置中的值。
let var1 = 42;
比如上面的代码中,var1是一个变量名,也就是个标识符,而第二个元素我们没办法显示地看到,因为它是一个内存地址,比如0x4890ba45这样的,对于我们开发者来说,肯定是变量名更有意义
Rust 与其他编程语言的不同之处在于,在任何时候只能有一个引用代表一个内存地址的别名。此外,无论何时使用这些别名之一,都是通过引用而不是通过值使用它。
let var1 = 42;
let var2;
var2 = var1;
比如上面代码,先是创建了一个变量var1,然后将42存储在var1所指向的内存地址中,然后呢,再创建一个变量var2,但是没有给这个地址存储数据,也就是我们申请了一块内存,但是没有使用它,最后,将var1赋值给var2,如果是在其他语言,那么会说,var1还可以继续使用,var2也有相同的值,但是在Rust中,var1失效了,不可以继续使用,而var2变成了var1指向地址的新的别名
简单说就是,var1本来是A地址的代言人,赋值之后,var2代替var1成为了A地址的代言人,var1失业了,不能继续工作了,如果在新代言人出现之后,还想继续使用var1,那就会有编译错误
这在Rust中被称为move
的语义,而不是copy
语义。
Rust通过这种方法,在任意时刻,任意一块内存只能有一个代言人,这样就避免了引用计数或者垃圾回收的性能消耗,因为只要内存没有代言人,即释放
EXTENDING LIFE
上一章的游戏扩展之后的所有代码
extern crate rand;
extern crate termion;
use std::{env, thread, time};
use std::fs::File;
use std::io::{BufRead, BufReader};
use termion::{clear, color};
fn census(_world: [[u8; 75]; 75]) -> u16
{
let mut count = 0;
for i in 0..74 {
for j in 0..74 {
if _world[i][j] == 1
{
count += 1;
}
}
}
count
}
fn generation(_world: [[u8; 75]; 75]) -> [[u8; 75]; 75]
{
let mut newworld = [[0u8; 75]; 75];
for i in 0..74 {
for j in 0..74 {
let mut count = 0;
if i > 0 {
count = count + _world[i - 1][j];
}
if i > 0 && j > 0 {
count = count + _world[i - 1][j - 1];
}
if i > 0 && j < 74 {
count = count + _world[i - 1][j + 1];
}
if i < 74 && j > 0 {
count = count + _world[i + 1][j - 1]
}
if i < 74 {
count = count + _world[i + 1][j];
}
if i < 74 && j < 74 {
count = count + _world[i + 1][j + 1];
}
if j > 0 {
count = count + _world[i][j - 1];
}
if j < 74 {
count = count + _world[i][j + 1];
}
newworld[i][j] = 0;
if (count < 2) && (_world[i][j] == 1) {
newworld[i][j] = 0;
}
if _world[i][j] == 1 && (count == 2 || count == 3) {
newworld[i][j] = 1;
}
if (_world[i][j] == 0) && (count == 3) {
newworld[i][j] = 1;
}
}
}
newworld
}
fn populate_from_file(filename: String) -> [[u8; 75]; 75]
{
let mut newworld = [[0u8; 75]; 75];
let file = File::open(filename).unwrap();
let reader = BufReader::new(file);
let mut pairs: Vec<(usize, usize)> = Vec::new();
for (index, line) in reader.lines().enumerate() {
let l = line.unwrap();
let mut words = l.split_whitespace();
let left = words.next().unwrap();
let right = words.next().unwrap();
pairs.push((left.parse::<usize>().unwrap(),
right.parse::<usize>().unwrap()));
}
for i in 0..74 {
for j in 0..74 {
newworld[i][j] = 0;
}
}
for (x, y) in pairs {
newworld[x][y] = 1;
}
newworld
}
fn displayworld(world: [[u8; 75]; 75])
{
for i in 0..74 {
for j in 0..74 {
if world[i][j] == 1
{
print!("{red}*", red = color::Fg(color::Red));
} else {
print!(" ");
}
}
println!("");
}
}
fn main() {
let mut world = [[0u8; 75]; 75];
let mut generations = 0;
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
for i in 0..74 {
for j in 0..74 {
if rand::random() {
world[i][j] = 1;
} else {
world[i][j] = 0;
}
}
}
} else {
let filename = env::args().nth(1).unwrap();
world = populate_from_file(filename);
}
println!("Population at generation {} is {}", generations, census(world));
for _gens in 0..100 {
let temp = generation(world);
world = temp;
generations += 1;
println!("{}", clear::All);
displayworld(world);
println!("{blue}Population at generation {g} is {c}", blue =
color::Fg(color::Blue), g = generations, c = census(world));
thread::sleep(time::Duration::from_secs(2));
}
}
Adding Modules
出现的新的依赖
[dependencies]
rand = "0.7.2"
termion = "*"
注意:termion是个TUI的比较底层的库,但是不支持Windows的终端
termion文档:
Supports Redox, Mac OS X, and Linux (or, in general, ANSI terminals).
如果需要在Windows平台运行,那就需要换成支持windows终端的库,比如
[dependencies]
crossterm = "0.23"
本章最后也会贴上Windows版本的代码
Working with Command-Line Arguments
一般,命令行程序都会需要用户在使用的时候,输入一些有意义的参数,方便程序开展工作
let args: Vec<String> = env::args().collect();
在Rust中,可以通过这种方式,收集用户输入的参数
然后它的结果是一个vector
,一个集合类型,然后是内容,这个集合的第一个元素是命令行程序的名字,第二个才是用户输入的参数
在完整代码中,对于用户输入参数的处理中,会有验证用户是否输入了我们想要的参数
if args.len() < 2 {
for i in 0..74 {
for j in 0..74 {
if rand::random() {
world[i][j] = 1;
} else {
world[i][j] = 0;
}
}
}
} else {
let filename = env::args().nth(1).unwrap();
world = populate_from_file(filename);
}
如果没有输入参数,那就手动随机创建,然后是发现在有输入参数的情况下,没有直接使用vector,而是重新从迭代器中拿,因为这涉及一个所有权的问题,在vector中拿出元素是有代价的(猜测),所以书中代码直接从源头拿,方法nth()
可以这样理解n-th
,就是第几个的意思
然后在一个迭代器中拿出元素,一般都不是直接返回你想要的那个结果,而是一个包装之后的结果,这里返回的是Option
,代表可有可无的意思,如果需要真正的结果,需要拆包
Option Types
Option
类型是一个枚举类型。而枚举类型,则是一组预先定义好的有限个数的元素,在使用的时候,需要加上类型的名称,而不可以只有元素的名称,因为元素是在类型的作用域范围之内的,否则会编译器报错
而Option枚举只有两个元素
pub enum Option<T> {
None,
Some(T),
}
None代表没有任何东西返回,Some代表有结果返回,而结果是类型的,< T >代表的是结果的类型,Option< T >是一个完整的类型,也就是结果不一样,类型是不一样的
然后是会在代码中发生,unwrap()
方法,相当于一种暴力拆包的方法,如果是None之类的元素,是会有运行时异常的,而且在许多函数中返回的结果都是一种复杂数据类型包装后的结果,都需要拆包使用
Offensive programming
(我觉得应该叫防御性编程,虽然翻译叫做攻击性编程)是一种开发软件的技术,它不会通过提供太多进入程序内部工作的途径来使软件暴露在漏洞中。在我们使用 Option 的示例中,实际值包装在数据类型中。这意味着我们可以使用 Option 数据类型来移动值,而不会因为试图访问不存在的内容而使程序暴露于生成错误的风险。所有需要做的就是在拆包之前针对变量进行检查,比如方法 is_none ()
let response: Option<String> = Some(String::from("Some value here"));
if response.is_none() {
println!("There is no value");
}
类似地,也有方法is_some()确定有值。Rust通过这样的显式地去检查有可能出现错误的数据,以此避免在运行时出现错误,而不是将程序暴露在一些意想不到的副作用下,虽然这样会让编写工作变得繁琐一点,但是换来了更加安全的代码
READING FROM FILES
let file = File::open(filename).unwrap();
let reader = BufReader::new(file);
我们只考虑读取文件的内容,不需要对每一个字符做处理,我们就一行一行地读取出来,这里使用了Buffer去读取,这种包装器一样形式的文件读取,在其他语言也是很常见的,一般是有一个Stream代表文件流,也即是源头,可是它是一个字节一个字节的形式读取,那么我们就会希望在这个基础之上做一些加工,比如按照不同的编码格式读取字符,或者一行一行地读取文件,或者读取xml,json等特定格式的文件,或者解密文件,那我们就可以选择不同的包装器对相同的文件流进行加工读取,它就像一条流水线,经过不同的步骤,最终我们在成品处看到的结果就不一样
然后上面又出现了unwrap()
方法,但是这次文件打开之后返回的结果不一样,它是Result
枚举
enum Result<T, E> {
Ok(T),
Err(E),
}
含义是,有结果,或者有错误,与Option很相似,但是Result就算是没有结果,也会有错误来解释,为什么没有结果,而Option是没有解释的
Extracting Values
for (index, line) in reader.lines().enumerate() {
let l = line.unwrap();
let mut words = l.split_whitespace();
let left = words.next().unwrap();
let right = words.next().unwrap();
pairs.push((left.parse::<usize>().unwrap(), right.parse::<usize>().unwrap()));
}
line
依然是Result枚举,所以还是要拆包;.split_whitespace()
方法返回拆分后的一个迭代器,所以需要使用.next()
方法来不断获取下一个,当然,结果依然是Result枚举;- 最后拿到的是一个string,但是集合的元素类型是数字,所以还需要转型
.parse::<usize>()
,当然,结果是个Result枚举
OUTPUTTING TO THE TERMINAL
fn displayworld(world: [[u8; 75]; 75])
{
for i in 0..74 {
for j in 0..74 {
if world[i][j] == 1
{
print!("{red}*", red = color::Fg(color::Red));
} else {
print!(" ");
}
}
println!("");
}
}
使用termion
这个库,来对输出到终端的信息添加颜色
println!("{}", clear::All);
这个也是使用termion
这个库,可以清空终端,也就是说,每次更新内容之前,先清空屏幕,然后打印新的信息
thread::sleep(time::Duration::from_secs(2));
然后这个方法,就是Rust式的线程睡眠
windows版本
由于编写这个游戏的时候,在windows11平台,所以书中给出的库不支持Windows的终端,所以换了crossterm
库,主要是修改了信息输入到终端的部分代码,对于该库的更完整使用,可以去这个库的文档看看,还是很简单易懂的,完整的代码如下:
use std::{env, thread, time};
use std::fs::File;
use std::io::{BufRead, BufReader, stdout};
use crossterm::{execute, style::{SetForegroundColor, Print, ResetColor, Color}, terminal};
fn census(_world: [[u8; 75]; 75]) -> u16
{
let mut count = 0;
for i in 0..74 {
for j in 0..74 {
if _world[i][j] == 1
{
count += 1;
}
}
}
count
}
fn generation(_world: [[u8; 75]; 75]) -> [[u8; 75]; 75]
{
let mut newworld = [[0u8; 75]; 75];
for i in 0..74 {
for j in 0..74 {
let mut count = 0;
if i > 0 {
count = count + _world[i - 1][j];
}
if i > 0 && j > 0 {
count = count + _world[i - 1][j - 1];
}
if i > 0 && j < 74 {
count = count + _world[i - 1][j + 1];
}
if i < 74 && j > 0 {
count = count + _world[i + 1][j - 1]
}
if i < 74 {
count = count + _world[i + 1][j];
}
if i < 74 && j < 74 {
count = count + _world[i + 1][j + 1];
}
if j > 0 {
count = count + _world[i][j - 1];
}
if j < 74 {
count = count + _world[i][j + 1];
}
newworld[i][j] = 0;
if (count < 2) && (_world[i][j] == 1) {
newworld[i][j] = 0;
}
if _world[i][j] == 1 && (count == 2 || count == 3) {
newworld[i][j] = 1;
}
if (_world[i][j] == 0) && (count == 3) {
newworld[i][j] = 1;
}
}
}
newworld
}
fn populate_from_file(filename: String) -> [[u8; 75]; 75]
{
let mut newworld = [[0u8; 75]; 75];
let file = File::open(filename).unwrap();
let reader = BufReader::new(file);
let mut pairs: Vec<(usize, usize)> = Vec::new();
for (_, line) in reader.lines().enumerate() {
let l = line.unwrap();
let mut words = l.split_whitespace();
let left = words.next().unwrap();
let right = words.next().unwrap();
pairs.push((left.parse::<usize>().unwrap(),
right.parse::<usize>().unwrap()));
}
for i in 0..74 {
for j in 0..74 {
newworld[i][j] = 0;
}
}
for (x, y) in pairs {
newworld[x][y] = 1;
}
newworld
}
fn displayworld(world: [[u8; 75]; 75])
{
let mut stdout = stdout();
for i in 0..74 {
for j in 0..74 {
if world[i][j] == 1
{
execute!(stdout, SetForegroundColor(Color::Red), Print("*"), ResetColor).expect("");
} else {
execute!(stdout, Print(" ")).expect("");
}
}
execute!(stdout, Print("\r\n")).expect("");
}
}
fn main() {
let mut world = [[0u8; 75]; 75];
let mut generations = 0;
let args: Vec<String> = env::args().collect();
if args.len() < 2 {
for i in 0..74 {
for j in 0..74 {
if rand::random() {
world[i][j] = 1;
} else {
world[i][j] = 0;
}
}
}
} else {
let filename = env::args().nth(1).unwrap();
world = populate_from_file(filename);
}
println!("Population at generation {} is {}", generations, census(world));
for _gens in 0..100 {
let mut stdout = stdout();
let temp = generation(world);
world = temp;
generations += 1;
execute!(stdout, terminal::Clear(terminal::ClearType::All)).expect("");
displayworld(world);
execute!(stdout,
SetForegroundColor(Color::Blue),
Print(format!("Population at generation {} is {}\r\n",generations, census(world))),
ResetColor).expect("");
thread::sleep(time::Duration::from_secs(2));
}
}
代码亲测可用,虽然最后是生成了EXE文件,但是需要在终端中打开,才可以看到效果
标签:count,..,PROGRAMMING,---,75,74,let,world,随记 From: https://www.cnblogs.com/huangwenhao1024/p/16927117.html