Rust变量和类型

我们将探讨Rust程序的基本构建块,如变量和类型。我们将讨论基本类型的变量,它们是否需要声明类型,以及变量的作用域。不可变性,作为Rust安全策略的基石之一,也将被讨论和展示。

我们将涵盖以下主题:

  • 注释
  • 全局常量
  • 值和基本类型
  • 变量绑定值
  • 变量的作用域和遮蔽
  • 类型检查和转换
  • 表达式
  • 栈和堆

我们的代码示例将围绕构建一个基于文本的游戏 Monster Attack

注释

理想情况下,程序应通过使用描述性的变量名和易于阅读的代码自我记录,但总是存在需要额外注释程序结构或算法的情况。Rust遵循C语言的惯例,有:

  • // 行注释;在 // 之后的行内所有内容都是注释,不会被编译
  • /* / 块注释或多行注释;/ 和 */ 之间的所有内容都不会被编译

然而,Rust推荐的风格是只使用 // 注释,即使是多行,如下代码所示:

fn main() { 
  // 这里开始执行游戏。 
  // 我们首先打印欢迎信息: 
  println!("欢迎来到游戏!"); 
}  

只有在注释掉代码时才使用 /* */ 注释。

Rust还有一个文档注释 ///,在需要为客户和开发者提供官方文档的大型项目中很有用。这些注释必须出现在项目(如函数)之前的单独一行上,以记录该项目。在这些注释中,您可以使用Markdown格式语法(参见https://en.wikipedia.org/wiki/Markdown)。

这是一个文档注释:

fn main() { 
}  

我们将在后续代码片段中看到更多 /// 的相关用法。rustdoc工具可以将这些注释编译成项目文档。

全局常量

通常,应用程序需要一些实际上是常量的值,这意味着它们在程序运行过程中不会改变。例如,在我们的游戏中,游戏名称Monster Attack可能是一个常量,最大健康量100也可能是一个常量。我们必须能够在main()函数或程序中的任何其他函数中使用它们,因此它们被放置在代码文件的顶部。它们存在于程序的全局作用域中。这样的常量是用关键字static声明的,如下所示:

static MAX_HEALTH: i32 = 100; 
static GAME_NAME: &str = "Monster Attack"; 
 
fn main() { 
}  

常量的名称必须为大写,可以使用下划线分隔词组。它们的类型也必须指明;变量MAX_HEALTH是一个32位整数(i32),而变量GAME_NAME是字符串(str)类型。正如我们将进一步讨论的,变量的类型声明方式与此完全相同,尽管当编译器能够从代码上下文推断出类型时,这通常是可选的。

记住,Rust是一种低级语言,所以许多事情必须详细指定。& 是对某物的引用(它包含其内存地址),这里是字符串的引用。

编译器给出的警告如下所示:

warning: static item is never used: `MAX_HEALTH`, #[warn(dead_code)] on by default  

这个警告不会阻止编译,所以在这个阶段,我们可以编译成可执行文件constants1.exe。但编译器是对的;这些对象在程序代码中从未被使用(它们被称为死代码),所以,在一个完整的程序中,要么使用它们,要么抛弃它们。

Rust开发者需要一段时间才能开始将Rust编译器视为朋友,而不是一个吐出错误和警告的烦人机器。只要你在编译器输出的最后看到这个信息,error: aborting due to previous errors,就不会产生(新的)已编译可执行文件。但请记住,更正错误可以消除运行时问题,因此这可以节省您追踪烦人错误所浪费的大量时间。错误消息通常伴有有关如何消除错误的有用说明。即使是警告也可以指出代码中的缺陷。Rust甚至会警告我们在接下来的代码中声明但未使用的内容,如未使用的变量、函数、导入的模块等。如果我们使一个变量可变(意味着它的值可以被改变)而它不应该这样,或者代码没有得到执行时,它甚至会警告我们!编译器做得如此出色,以至于当你消除所有错误和警告时,你的程序很可能会正确运行!

除了静态值,我们还可以使用简单的常量值,其值永不改变。常量总是必须被类型化,如下所示:

const MYPI: f32 = 3.14; 

编译器会自动在代码中的每个地方替换常量的值。

使用字符串插值进行打印

使用变量的一个明显方法是打印出它们的值,如下所示:

static MAX_HEALTH: i32 = 100; 
static GAME_NAME: &str = "Monster Attack"; 
const MYPI: f32 = 3.14; 
 
fn main() { 
  println!("你正在玩的游戏叫做 {}。", GAME_NAME); 
  println!("你开始时有 {} 点健康值。", MAX_HEALTH); 
}  

这会产生如下输出:

你正在玩的游戏叫做 Monster Attack。
你开始时有 100 点健康值。 

println! 宏的第一个参数是包含占位符 {} 的字面格式字符串。逗号后的常量或变量的值被转换为字符串并放在其位置。可以有多个占位符,并且可以按顺序编号,以便重复使用,如下代码所示:

println!("在游戏 {0} 中你开始时有 {1} % 的健康值,是的,你没看错:{1} 点!", GAME_NAME, MAX_HEALTH); 

这将产生以下输出:

在游戏 Monster Attack 中你开始时有 100 % 的健康值,是的,你没看错:100 点!

占位符也可以包含一个或多个命名参数,如下所示:

println!("你有 {points} % 的健康值", points = 70); 

这将产生以下输出:

你有 70 % 的健康值  

特殊的格式化方式可以在冒号 (😃 后的 {} 内指定,可选地加上位置前缀,如下所示:

println!("MAX_HEALTH 的十六进制表示为 {:x}", MAX_HEALTH); // 

这会产生如下输出:64

println!("MAX_HEALTH 的二进制表示为 {:b}", MAX_HEALTH);   // 

这会产生如下输出:1100100

println!("数字二的二进制表示是 {0:b}", 2);   // 

这会产生如下输出:10

println!("π 的浮点表示是 {:e}", PI);  // 

这会产生如下输出:3.14e0。

以下是格式化的可能性:

  • o:表示八进制
  • x:表示小写的十六进制
  • X:表示大写的十六进制
  • p:表示指针
  • b:表示二进制
  • e:表示小写的指数表示法
  • E:表示大写的指数表示法
  • ?:用于调试目的

format! 宏的参数与 println! 宏相同,并且工作方式相同,但它返回一个字符串而不是打印输出。

有关所有可能性的概览,请参阅 http://doc.rust-lang.org/std/fmt/ 。

值和基本类型

已初始化的常量具有值。值存在于不同的类型中:70 是一个整数,3.14 是一个浮点数,Z 和 θ 是字符类型。字符是 Unicode 值,每个占用四个字节的内存。Godzilla 是 &str 类型的字符串(默认为 Unicode UTF8),true 和 false 是布尔值类型。整数可以用不同的格式书写:

  • 十六进制格式,以 0x 开头,如 0x46 表示 70。

  • 八进制格式,以 0o 开头,如 0o106 表示 70。

  • 二进制格式,以 0b 开头,如 0b1000110。

  • 为了可读性,可以使用下划线,如 1_000_000。有时编译器会要求您更明确地用后缀指示数字的类型,例如(u 或 i 后的数字表示使用的内存位数,即:8、16、32 或 64)。

  • 数字 10usize 表示机器字长的无符号整数(usize),可以是以下类型之一:u8、u16、u32、u64。

  • 数字 10isize 表示机器字长的有符号整数(isize),可以是以下类型之一:i8、i16、i32、i64

  • 在上述情况下,在 64 位操作系统上,usize 实际上是 u64,isize 等同于 i64。

  • 数字 3.14f32 表示 32 位浮点数。

  • 数字 3.14f64 表示 64 位浮点数。

  • 如果没有给出后缀,数值类型 i32 和 f64 是默认值,但在这种情况下,为了区分它们,您必须用 .0 结尾表示 f64 值,如:

let e = 7.0; 

一般来说,建议指明具体类型。

Rust 在不同运算符及其优先级方面与其他 C 类语言相似。然而,请注意,Rust 没有增量(++)或减量(–)运算符。使用 == 比较两个值是否相等,使用 != 测试它们是否不同。

甚至还有零大小的空值 (),它是所谓的单位类型 () 的唯一值。当一个表达式或函数没有返回任何值(无值)时,它用于指示返回值,例如仅向控制台打印的函数。() 不等同于其他语言中的 null 值;() 是无值,而 null 是一个值。

查阅 Rust 文档

找到关于 Rust 主题更详细信息的最快方式是浏览到标准库的文档页面,网址为 http://doc.rust-lang.org/std/。

在左侧,你可以找到所有可用的 crate 列表,你可以浏览以获取更多详情。但最有用的是顶部的搜索框:输入几个字母或一个词,就能得到许多有用的参考。

img

变量绑定值

在 Rust 中,我们不能把所有的值都存储为常量。这不是一个好的选择,因为常量与程序的生命周期一样长,而且不能改变,我们经常需要改变值。我们可以通过使用 let 绑定将一个值绑定到变量。

fn main() { 
  let energy = 5; // 值 5 被绑定到变量 energy 
}  

与 Python 或 Go 等许多其他语言不同,这里需要使用分号 ; 来结束语句。否则,编译器会抛出错误,如下:

error: expected one of `.`, `;`, or an operator, found `}`

我们也只想在程序的其余部分使用绑定时才创建绑定,但不用担心,Rust 编译器会警告我们。警告如下:

values.rs:2:6: 2:7 warning: unused variable: `energy`, #[warn(unused_variables)] on by default

出于原型设计目的,你可以通过在变量名前加一个下划线 _ 来抑制该警告,如 let _energy = 5; 通常 _ 用于我们不需要的变量。

请注意,在上述声明中我们没有指明类型。Rust 会推断 energy 变量的类型是整数,这是由 let 绑定触发的。如果类型不明显,编译器会在代码上下文中搜索变量获取值的地方或它的使用方式。

但是提供类型提示,如 let energy = 5u16; 也是可以的;这样,你就通过指明 energy 的类型(在这个例子中是两字节无符号整数)来帮助编译器。

我们可以通过在表达式中使用变量 energy 来使用它,例如将其分配给另一个变量,或打印它:

let copy_energy = energy; 
println!("Your energy is {}", energy););  

以下是其他一些声明:

let level_title = "Level 1"; 
let dead = false; 
let magic_number = 3.14f32; 
let empty = ();  // 单位类型 () 的值  

变量 magic_number 的值也可以写作 3.14_f32_ 用来分隔数字和类型以提高可读性。

声明可以替代同一变量的先前声明。考虑如下语句:

let energy = "Abundant"; 

它现在将变量 energy 绑定到类型为字符串的值 Abundant。旧声明不再能使用,其内存被释放。

可变与不可变变量

假设我们吞下一个健康包,能量值提升到25。但是,如果我们像下面这样给变量 energy 赋值:

energy = 25;

我们会收到如下错误:

error: re-assignment of immutable variable `energy`.

这里出了什么问题?

好吧,Rust 在这里运用了程序员的智慧:很多bug都来自无意或错误的变量更改,所以除非你有意允许,否则不要让代码更改值!

在 Rust 中,变量默认是不可变的,这与函数式语言非常相似(在纯函数式语言中,甚至不允许可变性)。

如果你想要一个可变变量,因为它的值可以在代码执行期间更改,你必须明确地使用 mut 关键字来指示,例如:

  let mut fuel = 34; 
  fuel = 60;  

仅仅声明一个变量 let n; 也是不够的。我们会收到如下错误:

error: type annotations needed, consider giving `energy2` a type, cannot infer type for `_`

确实,编译器需要一个值来推断其类型。

我们可以通过给变量 n 赋一个值来提供这个信息,比如 n = -2; 但正如错误消息所说,我们也可以这样指明其类型:

  let n: i32; 

或者:

let n: i32 = -2; // `n` 是一个类型为 `i32` 和值为 `-2` 的绑定

类型(这里是 i32)跟在变量名后面,后面跟一个冒号 :(我们已经为全局常量展示过了),可选地跟一个初始化。通常类型是这样指示的——n: T,其中 n 是一个变量,T 是一个类型,它读作,变量 nT 类型的。所以,这与 C 或 C++、Java 或 C# 中的做法相反,那里会写成 Tn

对于基本类型,这也可以简单地用后缀来做,如下所示:

let x = 42u8; 
let magic_number = 3.14f64;  

尝试使用未初始化的变量会导致如下错误:

error: use of possibly uninitialized variable

局部变量必须在使用前初始化,以防止未定义行为。当编译器在你的代码中无法识别一个名字(例如,一个函数名)时,你会得到如下错误:

error: not found in this scope error

这很可能只是一个打字错误,但它在编译早期就被捕捉到了,而不是在运行时!

变量的作用域和隐藏

在程序 bindings.rs 中定义的所有变量都有局部作用域,由函数的 { } 界定,这里恰好是 main() 函数,但这适用于任何函数。在结束的 } 之后,它们就超出了作用域,它们的内存分配就被释放了。

我们甚至可以在函数内部通过定义一个代码块来创建一个更有限的作用域,代码块是包含在一对大括号 { } 内的所有代码,如下面的代码片段:

fn main() { 
  let outer = 42; 
   { // 代码块开始 
        let inner = 3.14; 
        println!("区块变量:{}", inner); 
        let outer = 99; // 遮蔽了第一个 outer 变量 
        println!("区块变量 outer:{}", outer); 
    } // 代码块结束 
    println!("外部变量:{}", outer); 
}  

前面的代码输出如下:

    区块变量:3.14
    区块变量 outer:99
    外部变量:42 

在块中定义的变量(如 inner)只在该块内部被知道。块中的变量也可以与外围作用域中的变量同名(如 outer),它被块中的变量替换(隐藏)直到块结束。当你尝试在块之后打印 inner 时,你期望会发生什么?试试看。

为什么你会想要使用一个代码块?在 表达式 部分,我们将看到一个代码块可以返回一个值,这个值可以用 let 绑定绑定到一个变量。代码块也可以为空 { }

类型检查和转换

Rust 需要知道变量的类型,因为这样它可以在编译时检查它们只能按照它们的类型允许的方式使用。这样程序就是类型安全的,可以避免一整类的bug。

这也意味着我们不能在变量的生命周期内改变它的类型,因为静态类型,例如,以下代码片段中的变量 score 不能从整数变为字符串:

// 警告:这段代码不工作! 
fn main() { 
let score: i32 = 100; 
score = "YOU WON!" 
}  

这样,我们就会得到编译器错误,如下:

error: mismatched types: expected i32, found reference

然而,我被允许写成这样:

let score = "YOU WON!"; 

Rust 允许我重新定义变量;每个 let 绑定都创建一个新的变量 score,它隐藏了之前的变量,旧变量从内存中被释放。这实际上是非常有用的,因为变量默认是不可变的。

在 Rust 中不定义使用 + 拼接字符串(如以下代码中的 players):

let player1 = "Rob"; 
let player2 = "Jane"; 
let player3 = player1 + player2;  

这样我们会得到一个错误,说明如下:

error: binary operation `+` cannot be applied to type `&str`

在 Rust 中,你可以使用 to_string() 方法将值转换为 String 类型,像这样:

let player3 = player1.to_string() + player2; 

或者,你可以使用 format! 宏:

let player3 = format!("{}{}", player1, player2); 

在这两种情况下,变量 player3 的值都是 RobJane

让我们看看当你将一个变量的值从一个类型分配给另一个不同类型的变量时会发生什么:

// 参见第2章/code/type_conversions.rs 
fn main() { 
  let points = 10i32; 
  let mut saved_points: u32 = 0; 
   saved_points = points; // 错误! 
}  

这同样是不允许的,因此,我们得到了同样的错误,如下:

error: mismatched types: expected u32, found i32

为了实现最大化的类型检查,Rust 不允许像 C++ 那样自动(或隐式)转换一个类型到另一个类型,从而避免了许多难以发现的bug。例如,当将一个 f32 值转换为 i32 值时,小数点后的数字会丢失;如果自动进行,这可能会导致错误。

然而,我们可以用关键字 as 做一个显式转换(也叫做类型转换),如下:

saved_points = points as u32; 

当变量 points 包含一个负值时,转换后会丢失符号。同样地,当从更宽的值如浮点数转换为整数时,小数部分会被截断。看下面的例子:

  let f2 = 3.14; 
  saved_points = f2 as u32; // 这里截断为值 3  

此外,值必须能够转换到新类型,例如字符串不能转换为整数:

  let mag = "Gandalf"; 
   saved_points = mag as u32; //  

这会导致如下错误:

error: non-scalar cast:`&str`as`u32`

别名

有时给现有类型一个新的、更具描述性或更简短的名称是很有用的。这是通过关键字 type 来完成的,如下面的例子,我们需要一个特定的(但大小有限的)变量来表示 MagicPower

type MagicPower = u16; 
 
fn main() { 
  let mut run: MagicPower = 7800; 
}  

类型名称以大写字母开头,名称的每个单词部分也是如此。

当我们将值 7800 改为 78000 时会发生什么?编译器通过以下警告检测到这一点:

warning: literal out of range for u16

表达式

Rust 是一个以表达式为中心的语言,这意味着大多数代码片段实际上是表达式,也就是说,它们计算一个值并返回该值。然而,单独的表达式并不构成有意义的代码;它们必须被用在语句中。

如下的 let 绑定是声明语句;它们不是表达式:

let a = 2;    // a 绑定到 2 
let b = 5;    // b 绑定到 5 
let n = a + b;   // n 绑定到 7  

但是,这里的 a + b 是一个表达式,如果我们省略末尾的分号,结果值(这里是值 7)就会返回。这通常在函数需要返回其值时使用(见下一章的例子)。以分号结束的表达式 a + b; 抑制了这种行为,从而丢弃了返回值,使其成为返回单位值 () 的表达式语句。

代码通常是一系列语句,每个语句在一行代码上,Rust 需要知道语句何时结束,这就是为什么几乎每行 Rust 代码都以分号结束。

你认为 m = 42; 是什么?它不是绑定,因为没有 let 绑定(那应该在之前的代码行发生)。这是一个返回单位值 () 的表达式。

let p = q = 3; 这样的复合绑定在 Rust 中是不允许的,它返回以下错误:

error: unresolved name q

然而,你可以像这样链接 let 绑定:

let mut n = 0; 
let mut m = 1; 
let t = m; m = n; n = t; 
println!("{} {} {}", n, m, t); //  

这会输出:1 0 1

代码块也是一个表达式,如果我们省略分号,它返回其最后一个表达式的值。例如,在以下代码片段中,n1 获得值 7,但 n2 没有获得值(或者说,获得了单位值 ()),因为第二个块的返回值被抑制了:

let n1 = { 
let a = 2; 
let b = 5; 
a + b   // <-- 没有分号! 
}; 
  println!("n1 是:{}", n1);  // 打印:n1 是 7 
  let n2 = { 
    let a = 2; 
    let b = 5; 
    a + b; 
  }; 
  println!("n2 是:{:?}", n2);  // 打印:n2 是 ()  

块中声明的变量 a 和 b 只在该块存在期间存在,它们对该块是局部的。注意,块的闭合大括号后面的分号 }; 是必需的。要打印单位值 (),我们需要使用 {:?} 作为格式指定符。

栈和堆

因为在 Rust 中内存分配非常重要,我们必须对正在发生的事情有一个清晰的心理图像。一个程序的内存被分为栈内存和堆内存部分;要了解这些概念的更多背景,请访问 What and where are the stack and heap?

像数字(如图中的 32)、字符或真或假值这样的原始值存储在栈上,但可能增长大小的更复杂对象的值存储在堆内存中。堆值由栈上的变量引用,该变量包含堆上对象的内存地址。

img

虽然栈的大小是有限的,但堆的大小可以根据需要的空间增长。

假设我们运行以下程序并尝试可视化程序的内存:

let health = 32; 
let mut game = "Space Invaders";  

值存储在内存中,因此具有内存地址。变量 health 包含一个整数值 32,它存储在位于 0x23fba4 的栈位置上,而变量 game 包含一个字符串,它存储在从 0x23fb90 开始的堆上(这些是我执行程序时的地址,当你运行程序时它们会不同)。

变量是值的绑定,是指向这些值的指针或引用。变量 game 是对 “Space Invaders” 的引用。值的地址由 & 运算符给出。所以,&health 指针是存储值 32 的地址,&game 指针是存储值 “Space Invaders” 的地址。

我们可以使用格式字符串 {:p} 来打印这些地址,像这样:

println!("health-value 的地址: {:p}", &health); 
// 打印 0x23fba4 
println!("game-value 的地址: {:p}", &game); // 打印 0x23fb90 
println!("game-value: {}", game); // 打印 "Space Invaders"  

现在,我们在内存中有以下情况(每次执行时内存地址将不同):

img

我们可以创建一个别名,这是另一个引用,它指向内存中的同一个位置,像这样:

let game2 = &game; 
println!("{:p}", game2); // 打印 0x23fb90  

为了获取被引用的值而不是引用 game2 本身,使用星号 * 操作符进行解引用,像这样:

println!("{}", *game2); // 打印 "Space Invaders"

println! 宏很聪明,所以 println!("{}", game2); 也会打印出相同的值,如下面的语句所做的那样:

println!("game: {}", &game); 

上述故事有点简化了,因为 Rust 会尽可能地在栈上分配大小不会改变的值,但它旨在帮助你更好地理解对一个值的引用意味着什么。

我们已经知道,let 绑定是不可变的,所以值不能被改变,如 health = 33;

这会导致以下错误:

error: re-assignment of immutable variable `health` 

如果变量 y 被声明为 let y = &health;,那么引用 *y 就是值 32。引用变量也可以给定一个类型,如 let x: &i64;,这样的引用可以在代码中传递。在这个 let 绑定之后,x 实际上还没有真正指向一个值,它不包含一个内存地址。在 Rust 中,没有办法创建一个空指针,就像你在其他语言中可以做的那样,试图给 x 赋值为 nilnull 甚至单位值 () 都会导致错误。仅这一点就为 Rust 程序员节省了无数的错误。此外,试图在表达式中使用变量 x,例如语句:

println!("{:?}", x); 

这会导致以下错误:

error: use of possibly uninitialized variable: `x`

对不可变变量的可变引用(表示为 &mut 指针)是禁止的,否则不可变变量可以通过其可变引用被改变:

  let tricks = 10; 
  let reftricks = &mut tricks;  

这会导致以下错误:

cannot borrow immutable local variable `tricks` as mutable

对可变变量 score 的引用可以是不可变的或可变的,就像下面例子中的 score2score3 变量一样:

  let mut score = 0; 
  let score2 = &score;  

但是你不能通过不可变引用 score2 改变 score 的值,因为这会导致错误,如下:

*score2 = 5;

这将导致错误:不能对不可变的借用内容 *score2 进行赋值
通过可变引用如 score3 才能改变变量 score 的值:

let mut score = 0; 
let score3 = &mut score; 
*score3 = 5;  

由于我们稍后会看到的原因,你只能对一个可变变量创建一个可变引用:

let score4 = &mut score; 

如果你这样做,会抛出如下错误:

error: cannot borrow `score` as mutable more than once at a time

在这里,我们接触到了 Rust 内存安全系统的核心,借用变量是其关键概念之一。我们将在[第7章]《确保内存安全和指针》中更详细地探讨这个问题。

堆是一个比栈大得多的内存部分,所以当内存位置不再需要时,尽快释放它们是很重要的。在 Rust 中,每个变量都有一定的生命周期,它说明了变量在程序内存中存在多久。Rust 编译器会看到变量的生命周期何时结束(换句话说,变量超出作用域),并在编译时插入代码,以在执行该代码时释放其内存。这种行为是 Rust 独有的,在其他常用语言中不会这样做。

栈上的值可以通过创建一个围绕它们的 Box 来装箱,即分配到堆上,就像下面 x 的值一样:

let x = Box::new(5i32); 

Box 对象引用堆上的一个值。我们也将在《确保内存安全和指针》的Boxes部分更仔细地看看它。

  • 22
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值