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

暴走萝莉·金克丝

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

3.2 布尔类型

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

笔记

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

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

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

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 位的类型,该字符值的高位会被截断:

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”下的在线文档中找到这些方法。

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.i 或 t[i] 的形式来获取第 i 个元素。

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

fn split_at(&self, mid: usize) -> (&str, &str);

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

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");

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

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 成员的结构体,但对如此显而易见的事情来说,这种写法相当烦琐,所以我们只用了一个元组:

/// 把`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 的声明如下所示:

fn swap<T>(x: &mut T, y: &mut T);

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

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 指向的值。它们非常像 C 和 C++ 中的 & 运算符和 * 运算符,并且和 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

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中没有单独的指针相关介绍,关于变量引用值,引用值的特点里提到了指针

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

原文链接:https://juejin.cn/post/7321779252052000818 作者:草帽lufei

(0)
上一篇 2024年1月9日 下午4:15
下一篇 2024年1月9日 下午4:26

相关推荐

发表回复

登录后才能评论