第3章 | 基本数据类型 | 布尔类型,字符,元组,指针类型

暴走萝莉·金克丝

3.2 布尔类型

Rust 的布尔类型 bool 具有此类型常用的两个值 truefalse==< 等比较运算符会生成 bool 结果,比如 2 < 5 的值为 true

笔记

Rust 的 bool 类型和 javascript 中的类似,因为 Rust 强类型语言,所以不需要 === 来减少类型隐式转换开销从而提高程序性能

许多语言对在要求布尔值的上下文中使用其他类型的值持宽松态度,比如 C 和 C++ 会把字符、整数、浮点数和指针隐式转换成布尔值,因此它们可以直接用作 if 语句或 while 语句中的条件。Python 允许在布尔上下文中使用字符串、列表、字典甚至 Set,如果这些值是非空的,则将它们视为 true。然而,Rust 非常严格:像 ifwhile 这样的控制结构要求它们的条件必须是 bool 表达式,短路逻辑运算符 &&|| 也是如此。你必须写成 if x != 0 { ... },而不能只写成 if x { ... }

Rust 的 as 运算符可以将 bool 值转换为整型:

rust 复制代码
assert_eq!(false as i32, 0);
assert_eq!(true  as i32, 1);

但是,as 无法进行另一个方向(从数值类型到 bool)的转换。相反,你必须显式地写出比较表达式,比如 x != 0

尽管 bool 只需要用一个位来表示,但 Rust 在内存中会使用整字节来表示 bool 值,因此可以创建指向它的指针。

3.3 字符

Rust 的字符类型 char 会以 32 位值表示单个 Unicode 字符。

Rust 会对单独的字符使用 char 类型,但对字符串和文本流使用 UTF-8 编码。因此,String 会将其文本表示为 UTF-8 字节序列,而不是字符数组。

字符字面量是用单引号括起来的字符,比如 '8''!'。还可以使用全角 Unicode 字符:' 錆 ' 是一个 char 字面量,表示日文汉字中的 sabi(rust)。

与字节字面量一样,有些字符需要用反斜杠转义,如表 3-10 所示。

表 3-10:需要用反斜杠转义的字符

字符 Rust 字符字面量
单引号(' '\''
反斜杠(\ '\\'
换行(lf '\n'
回车(cr '\r'
制表(tab '\t'

如果你愿意,还可以用十六进制写出字符的 Unicode 码点。

  • 如果字符的码点在 U+0000 到 U+007F 范围内(也就是说,如果它是从 ASCII 字符集中提取的),就可以把字符写为 '\xHH',其中 HH 是两个十六进制数。例如,字符字面量 '*''\x2A' 是等效的,因为字符 * 的码点是 42 或十六进制的 2A。
  • 可以将任何 Unicode 字符写为 '\u{HHHHHH}' 形式,其中 HHHHHH 是最多 6 个十六进制数,可以像往常一样用下划线进行分组。例如,字符字面量 '\u{CA0}' 表示字符"ಠ",这是 Unicode 中用于表示反对的卡纳达语字符"ಠ_ಠ"。同样的字面量也可以简写成 'ಠ'

笔记

使用十六进制写字符的操作很秀,这里想到了一个乐子:如果把项目中的所有的中文使用十六进制表示,是不是也大幅度提升了不可替代的能力

char 总是包含 0x0000 到 0xD7FF 或 0xE000 到 0x10FFFF 范围内的 Unicode 码点。char 永远不会是"半代用区"中的码点(0xD800 到 0xDFFF 范围内的码点,它们不能单独使用)或 Unicode 码点空间之外的值(大于 0x10FFFF 的值)。Rust 使用类型系统和动态检查来确保 char 值始终在允许的范围内。

Rust 不会在 char 和任何其他类型之间进行隐式转换。可以使用 as 转换运算符将 char 转换为整型,对于小于 32 位的类型,该字符值的高位会被截断:

rust 复制代码
assert_eq!('*' as i32, 42);
assert_eq!('ಠ' as u16, 0xca0);
assert_eq!('ಠ' as i8, -0x60); // U+0CA0截断到8位,有符号

从另一个方向来看,u8 是唯一能通过 as 运算符转换为 char 的类型,因为 Rust 刻意让 as 运算符只执行开销极低且可靠的转换,但是除 u8 之外的每个整型都可能包含 Unicode 码点之外的值,所以这些转换都要做运行期检查。作为替代方案,标准库函数 std::char::from_u32 可以接受任何 u32 值并返回一个 Option<char>:如果此 u32 不是允许的 Unicode 码点,那么 from_u32 就会返回 None,否则,它会返回 Some(c),其中 c 是转换成 char 后的结果。

标准库为字符提供了一些有用的方法,你可以在"char(原始类型)"和模块"std::char"下的在线文档中找到这些方法。

rust 复制代码
assert_eq!('*'.is_alphabetic(), false);
assert_eq!('β'.is_alphabetic(), true);
assert_eq!('8'.to_digit(10), Some(8));
assert_eq!('ಠ'.len_utf8(), 3);
assert_eq!(std::char::from_digit(2, 10), Some('2'));

孤立的字符自然不如字符串和文本流那么有用。3.7 节会讲解 Rust 的标准 String 类型和文本处理。

3.4 元组

元组 是各种类型值的值对或三元组、四元组、五元组等(因此称为 n-元组元组 )。可以将元组编写为一个元素序列,用逗号隔开并包裹在一对圆括号中。例如,("Brazil", 1985) 是一个元组,其第一个元素是一个静态分配的字符串,第二个元素是一个整数,它的类型是 (&str, i32)。给定一个元组值 t,可以通过 t.0t.1 等访问其元素。

元组有点儿类似于数组,即这两种类型都表示值的有序序列。许多编程语言混用或结合了这两个概念,但在 Rust 中,它们是截然不同的。一方面,元组的每个元素可以有不同的类型,而数组的元素必须都是相同的类型。另一方面,元组只允许用常量作为索引,比如 t.4。不能通过写成 t.it[i] 的形式来获取第 i 个元素。

Rust 代码通常会用元组类型从一个函数返回多个值。例如,字符串切片上的 split_at 方法会将字符串分成两半并返回它们,其声明如下所示:

rust 复制代码
fn split_at(&self, mid: usize) -> (&str, &str);

返回类型 (&str, &str) 是两个字符串切片构成的元组。可以用模式匹配语法将返回值的每个元素赋值给不同的变量:

rust 复制代码
let text = "I see the eigenvalue in thine eye";
let (head, tail) = text.split_at(21);
assert_eq!(head, "I see the eigenvalue ");
assert_eq!(tail, "in thine eye");

这样比其等效写法更易读:

rust 复制代码
let text = "I see the eigenvalue in thine eye";
let temp = text.split_at(21);
let head = temp.0;
let tail = temp.1;
assert_eq!(head, "I see the eigenvalue ");
assert_eq!(tail, "in thine eye");

你还会看到元组被用作一种超级小巧的结构体类型。例如,在第 2 章的曼德博程序中,我们要将图像的宽度和高度传给绘制它的函数并将其写入磁盘。为此可以声明一个具有 width 成员和 height 成员的结构体,但对如此显而易见的事情来说,这种写法相当烦琐,所以我们只用了一个元组:

rust 复制代码
/// 把`pixels`缓冲区(其尺寸由`bounds`给出)写入名为`filename`的文件中
fn write_image(filename: &str, pixels: &[u8], bounds: (usize, usize))
    -> Result<(), std::io::Error>
{ ... }

bounds 参数的类型是 (usize, usize),这是一个包含两个 usize 值的元组。当然也可以写成单独的 width 参数和 height 参数,并且最终的机器码也基本一样。但重点在于思路的清晰度。应该把大小看作一个值,而不是两个,使用元组能更准确地记述这种意图。

另一种常用的元组类型是零元组 ()。传统上,这叫作单元类型 ,因为此类型只有一个值,写作 ()。当无法携带任何有意义的值但其上下文仍然要求传入某种类型时,Rust 就会使用单元类型。

例如,不返回值的函数的返回类型为 ()。标准库的 std::mem::swap 函数就没有任何有意义的返回值,它只会交换两个参数的值。std::mem::swap 的声明如下所示:

rust 复制代码
fn swap<T>(x: &mut T, y: &mut T);

这个 <T> 意味着 swap泛型 的:可以将对任意类型 T 的值的引用传给它。但此签名完全省略了 swap 的返回类型,它是以下完整写法的简写形式:

rust 复制代码
fn swap<T>(x: &mut T, y: &mut T) -> ();

类似地,前面提到过的 write_image 示例的返回类型是 Result<(), std::io::Error>,这意味着该函数在出错时会返回一个 std::io::Error 值,但成功时不会返回任何值。

如果你愿意,可以在元组的最后一个元素之后跟上一个逗号:类型 (&str, i32,)(&str, i32) 是等效的,表达式 ("Brazil", 1985,)("Brazil", 1985) 是等效的。Rust 始终允许在所有能用逗号的地方(函数参数、数组、结构体和枚举定义,等等)添加额外的尾随逗号。这对人类读者来说可能很奇怪,不过一旦在多行列表末尾添加或移除了条目(entry),在显示差异时就会更容易阅读。

为了保持一致性,甚至有包含单个值的元组。字面量 ("lonely hearts",) 就是一个包含单个字符串的元组,它的类型是 (&str,)。在这里,值后面的逗号是必需的,以用于区分单值元组和简单的括号表达式。

笔记

JavaScript 中没有元组的概念,有点js中的数组和对象的一些功能特性的结合体,元组的设计使程序功能更明确清晰

3.5 指针类型

Rust 有多种表示内存地址的类型。

这是 Rust 和大多数具有垃圾回收功能的语言之间一个重大的差异。在 Java 中,如果 class Rectangle 包含字段 Vector2D upperLeft;,那么 upperLeft 就是对另一个单独创建的 Vector2D 对象的引用。在 Java 中,一个对象永远不会包含其他对象的实际内容。

但 Rust 不一样。该语言旨在帮你将内存分配保持在最低限度。默认情况下值会嵌套。值 ((0, 0), (1440, 900)) 会存储为 4 个相邻的整数。如果将它存储在一个局部变量中,则会得到 4 倍于整数宽度的局部变量。堆中没有分配任何内容。

这可以帮我们高效利用内存,但代价是,当 Rust 程序需要让一些值指向其他值时,必须显式使用指针类型。好消息是,当使用这些指针类型时,安全的 Rust 会对其进行约束,以消除未定义的行为,因此指针在 Rust 中比在 C++ 中更容易正确使用。

接下来将讨论 3 种指针类型:引用、Box 和不安全指针。

3.5.1 引用

&String 类型的值(读作"ref String")是对 String 值的引用,&i32 是对 i32 的引用,以此类推。

最简单的方式是将引用视为 Rust 中的基本指针类型。在运行期间,对 i32 的引用是一个保存着 i32 地址的机器字,这个地址可能位于栈或堆中。表达式 &x 会生成一个对 x 的引用,在 Rust 术语中,我们会说它借用了对 x 的引用 。给定一个引用 r,表达式 *r 会引用 r 指向的值。它们非常像 CC++ 中的 & 运算符和 * 运算符,并且和 C 中的指针一样,当超出作用域时引用不会自动释放任何资源。

然而,与 C 指针不同,Rust 的引用永远不会为空:在安全的 Rust 中根本没有办法生成空引用。与 C 不同,Rust 会跟踪值的所有权和生命周期,因此早在编译期间就排除了悬空指针、双重释放和指针失效等错误。

Rust 引用有两种形式。

&T

一个不可变的共享引用。你可以同时拥有多个对给定值的共享引用,但它们是只读的:禁止修改它们所指向的值,就像 C 中的 const T* 一样。

&mut T

一个可变的、独占的引用。你可以读取和修改它指向的值,就像 C 中的 T* 一样。但是只要该引用还存在,就不能对该值有任何类型的其他引用。事实上,访问该值的唯一途径就是使用这个可变引用。

Rust 利用共享引用和可变引用之间的"二选一"机制来强制执行"单个写入者多个读取者"规则:你或者独占读写一个值,或者让任意数量的读取者共享,但二者只能选择其一。这种由编译期检查强制执行的"二选一"规则是 Rust 安全保障的核心。第 5 章会解释 Rust 的安全引用的使用规则。

3.5.2 Box

在堆中分配值的最简单方式是使用 Box::new

rust 复制代码
let t = (12, "eggs");
let b = Box::new(t);  // 在堆中分配一个元组

t 的类型是 (i32, &str),所以 b 的类型是 Box<(i32, &str)>。对 Box::new 的调用会分配足够的内存以在堆上容纳此元组。当 b 超出作用域时,内存会立即被释放,除非 b 已被移动(move),比如返回它。移动对于 Rust 处理在堆上分配的值的方式至关重要,第 4 章会对此进行详细解释。

3.5.3 裸指针

Rust 也有裸指针类型 *mut T*const T。裸指针实际上和 C++ 中的指针很像。使用裸指针是不安全的,因为 Rust 不会跟踪它指向的内容。例如,裸指针可能为空,或者它们可能指向已释放的内存或现在包含不同类型的值。C++ 的所有经典指针错误都可能"借尸还魂"。

但是,你只能在 unsafe 块中对裸指针解引用(dereference)。unsafe 块是 Rust 高级语言特性中的可选机制,其安全性取决于你自己。如果代码中没有 unsafe 块(或者虽然有但编写正确),那么本书中强调的安全保证就仍然有效。有关详细信息,请参阅第 22 章。

笔记

《JavaScript高级程序设计(第4版)》 中JavaScript中没有单独的指针相关介绍,关于变量引用值,引用值的特点里提到了指针

引用值是对象,存储在堆内存上。包含引用值的变量实际上只包含指向相应对象的一个指针,而不是对象本身。从一个变量到另一个变量复制引用值只会复制指针,因此结果是两个变量都指向同一个对象。

相关推荐
qq. 28040339845 分钟前
react hooks
前端·javascript·react.js
用户40993225021212 分钟前
PostgreSQL 查询慢?是不是忘了优化 GROUP BY、ORDER BY 和窗口函数?
后端·ai编程·trae
半夏知半秋14 分钟前
skynet.newservice接口分析
笔记·后端·学习·安全架构
陈小桔29 分钟前
Springboot之常用注解
java·spring boot·后端
LHX sir1 小时前
什么是UIOTOS?
前端·前端框架·编辑器·团队开发·个人开发·web
Gazer_S1 小时前
【前端状态管理技术解析:Redux 与 Vue 生态对比】
前端·javascript·vue.js
数据知道1 小时前
Go基础:一文掌握Go语言泛型的使用
开发语言·后端·golang·go语言
小光学长1 小时前
基于Vue的图书馆座位预约系统6emrqhc8(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
前端·数据库·vue.js
Y学院1 小时前
vue的组件通信
前端·javascript·vue.js
PairsNightRain1 小时前
React Concurrent Mode 是什么?怎么使用?
前端·react.js·前端框架