Skip to content

Latest commit

 

History

History
891 lines (610 loc) · 47.5 KB

ch03-基本类型.md

File metadata and controls

891 lines (610 loc) · 47.5 KB

基本类型(Basic Types)

世界上有许多许多类型的书籍,这很有道理,因为有很多很多类型的人,每个人都想读不同的东西. --Lemony Snicket

原文

There are many, many types of books in the world, which makes good sense, because there are many, many types of people, and everybody wants to read something different. --Lemony Snicket

Rust的类型有几个目标:

安全

通过检查程序的类型,Rust编译器会排除所有类的常见错误.通过使用类型安全的替代方案替换空指针和未经检查的联合,Rust甚至能够消除其他语言中常见的导致崩溃的错误.

高效

程序员可以对Rust程序如何在内存中表示值进行细粒度控制,并且可以选择他们知道处理器将高效处理的类型. 程序不需要为他们不使用的一般性或灵活性付费.

简洁

Rust管理所有的这些,而不需要程序员在代码中写出类型的形式提供太多的指导.与类似的C++程序相比,Rust程序在类型上通常不会那么混乱.

Rust不使用解释器或即时编译器,而是使用提前编译:整个程序到机器代码的转换在开始执行之前就已完成.Rust的类型可以帮助提前编译器为你的程序所操作的值选择良好的机器级表示:可以预测其性能的表示,并允许你完全使用机器的功能.

Rust是一种静态类型的语言:在没有实际运行程序的情况下,编译器会检查每个可能的执行路径是否仅以与其类型一致的方式使用值.这使Rust能够及早发现许多编程错误,这对Rust的安全保证至关重要.

和动态类型的语言如JavaScript或Python相比,Rust要求你预先做更多的规划:你必须说明函数的参数和返回值的类型,结构类型的成员以及其它一些构造.然而,Rust的两个特性使这个麻烦比你预期的要少:

  • 根据你写出的类型,Rust会为你推断其余的大部分.在实践中,通常只有一种类型适用于给定的变量或表达式;在这种情况下,Rust允许你省略类型.例如,你可以写出函数中的每个类型,如下所示:
fn build_vector() -> Vec<i16> {
    let mut v: Vec<i16> = Vec::<i16>::new();
    v.push(10i16);
    v.push(20i16);
    v

但这是混乱和重复的. 根据函数的返回类型,很明显v必须是Vec<i16>,一个16位有符号整数的向量;其它类型都不行.由此可以得出,向量的每个元素都必须是i16.这正是Rust的类型推断适用的推理,允许你改写:

fn build_vector() -> Vec<i16> {
    let mut v = Vec::new();
    v.push(10);
    v.push(20);
    v

这两个定义完全相同;两种方式,Rust会生成相同的机器代码.类型推断提供了动态类型语言的大部分易读性,同时仍然在编译时捕获类型错误.

  • 函数可以是 通用(generic,也是泛型) 的:当函数的目的和实现足够通用时,你可以将其定义为适用于满足必要条件的任何类型的集合.单个定义可以涵盖一组开放式用例.

在Python和JavaScript中,所有函数都自然地以这种方式工作:函数可以对任何具有函数所需属性和方法的值进行操作.(这种特性通常称为鸭子类型:如果它像鸭子一样嘎嘎叫,那它就是一只鸭子.) 但正是这种灵活性使得这些语言很难及早发现类型错误;测试通常是捕捉此类错误的唯一方法.

Rust的泛型函数为语言提供了相同的灵活性,同时仍能在编译时捕获所有类型的错误.

尽管泛型函数具有灵活性,但它们与非泛型函数一样高效.我们将在第11章详细讨论泛型函数.

本章的其余部分将自下而上介绍Rust的类型,从简单的机器类型(如整数和浮点值)开始,然后展示如何将它们组合成更复杂的结构.在适当的情况下,我们将描述Rust如何在内存中表示这些类型的值以及它们的性能特征. 以下是你在Rust中看到的各种类型的摘要.此表显示了Rust的基本类型,标准库中的一些非常常见的类型,以及一些用户定义类型的示例:

类型 描述
i8,i16,i32,i64,
u8,u16,u32,u64
有符号和无符号整数,指定位宽 42,
isize,usize 有符号和无符号整数,和机器地址同大小(32或64为) -5i8,0x400u16,0o100i16,
20_922_789_888_000u64,
b'*' (u8字节字面量)
f32,f64 IEEE浮点数,单精度和双精度 1.61803,3.14f32,6.0221e23f64
bool 布尔值 true,false
char Unicode字符,32位宽 '*','\n','字','\x7f','\u{CA0}'
(char, u8, i32) 元组:允许混合类型 ('*', 0x7f, -1)
() "单元"(空)元组 ()
struct S { x: f32, y: f32 } 命名字段结构 S { x: 120.0, y: 209.0 }
struct T(i32, char) 元组风格结构 T(120, 'X')
struct E 单元风格结构;没有字段 E
enum Attend { OnTime, Late(u32) } 枚举,代数数据类型 Attend::Late(5),Attend::OnTime
Box<Attend> Box:拥有指向堆中值的指针 Box::new(Late(15))
&i32,&mut i32 共享和可变引用:不拥有指针,不能比它们的指示物活得久 &s.y,&mut v
String UTF-8字符串,动态大小 "ラーメン: ramen".to_string()
&str str的引用:不拥有指向UTF-8文本的指针 "そば: soba",&s[0..12]
[f64; 4],[u8; 256] 数组,固定大小;所有元素类型相同 [1.0, 0.0, 0.0, 1.0],
[b' '; 256]
Vec<f64> 向量,可变长度;所有元素类型相同 &v[10..20],&mut a[..]
&Any,&mut Read Trait对象:对实现给定方法集的任何值的引用 value as &Any,&mut file as &mut Read
fn(&str, usize) -> isize 函数指针 i32::saturating_add
(闭包类型没有书面形式) 闭包 `

本章将介绍其中的大部分类型,以下内容除外:

  • struct类型自己一章,第9章.
  • 枚举类型自己一章,第10章.
  • trait对象在第11章描述.
  • 这里描述String和&str的要点,但在第17章中提供了更多的细节.
  • 第14章涵盖了函数和闭包类型

机器类型(Machine Types)

Rust类型系统的基础是一组固定宽度的数字类型,选择它们以匹配几乎所有现代处理器直接在硬件中实现的类型,以及布尔类型和字符类型.

Rust的数字类型的名称遵循一个有规律的模式,以位为单位拼写宽度,以及它们使用的表示形式:

大小(位) 无符号整数 有符号整数 浮点数
8 u8 i8
16 u16 i16 f32
32 u32 i32 f64
64 u64 i64
机器字长 usize isize

这里,机器字长 是代码运行的机器上的地址大小的值,通常是32或64位.

整型(Integer Types)

Rust的无符号整数类型使用它们的全范围来表示正值和零:

类型 范围
u8 0到2^8^-1(0到255)
u16 0到2^16^-1(0)到65,535
u32 0到2^32^-1(0到4,294,967,295))
u64 0到2^64^−1(0到18,446,744,073,709,551,615,或18*10的18次幂
usize 0到2^32^-1 或到2^64^-1

Rust的有符号整数类型使用二进制补码表示,使用与相应无符号类型相同的位模式来覆盖正值和负值范围:

类型 范围
i8 -2^7^到2^7^-1(-128到127)
i16 -2^15^到2^15^-1(-32,768到32,767)
i32 -2^31^到2^31^-1(-2,147,483,648到2,147,483,647)
i64 -2^63^到2^63^-1(−9,223,372,036,854,775,808到9,223,372,036,854,775,807)
isize −2^31^到2^31^−1,或−2^63^到2^63^−1

Rust通常将u8类型用于字节值.例如,从文件或套接字读取数据会产生u8值的流.

与C和C++不同,Rust将字符视为与数字类型不同;char既不是u8也不是i8.我们在第52页的"字符(Characters)"中描述了Rust的char类型.

usizeisize类型类似于C和C++中的size_tptrdiff_t.usize类型是无符号的,isize是有符号的.它们的精度取决于目标机器上地址空间的大小:它们在32位架构上为32位长,在64位架构上为64位长.Rust要求使用数组索引为usize值.表示数组或向量的大小的值或某些数据结构中的元素数的计数通常也具有usize类型.

在调试版本中,Rust检查算术中的整数溢出:

let big_val = std::i32::MAX;
let x = big_val + 1;  // panic: arithmetic operation overflowed

在发布版本中,此加法将被包装为一个负数(与C++不同,其中有符号整数溢出是未定义的行为).但除非你想永远放弃调试版本,否则依靠它是一个坏主意.如果要包装算术,请使用以下方法:

let x = big_val.wrapping_add(1);  // ok

Rust中的整数字面量可以采用后缀表示其类型:42u8u8值,1729isizeisize.您可以省略整数字面量的后缀,在这种情况下,Rust会尝试从上下文中推断出它的类型.该推断通常标识一种唯一类型,但有时几种类型中的任何一种都可以使用.在这种情况下,Rust默认为i32,如果这是可能性之一.否则,Rust会将歧义报告为错误.

前缀0x,0o0b表示十六进制,八进制和二进制字面量.

要使长数字更清晰,可以在数字中插入下划线.例如,您可以将最大的u32值写为4_294_967_295.下划线的确切位置并不重要,因此您可以将十六进制或二进制数分成四位数一组而不是三位数一组,如0xffff_ffff或给数字中设置类型后缀,如127_u8.

一些整数字面量的例子:

字面量 类型 十进制值
116i8 i8 116
0xcafeu32 u32 51966
0b0010_1010 依推断 42
0o106 依推断 70

尽管数字类型和char类型是不同的,但Rust确实提供 字节字面量(byte literals) 它是类似字符字面量的u8值:b'X'表示字符X的ASCII码,作为u8值.例如,由于A的ASCII码是65,因此字面量b'A'65u8完全相等.字节字面量中只能出现ASCII字符.

有一些字符不能简单地放在单引号后面,因为这可能是语法模糊或难以阅读.以下字符需要在它们前面放置反斜杠:

字符 字节字面量 数值等效
单引号' b'\'' 39u8
反斜杠\ b'\\' 92u8
换行 b'\n' 10u8
回车 b'\r' 13u8
制表符 b'\t' 9u8

对于难以写入或读取的字符,您可以用十六进制编写代码.一个形式为b'\xHH'的字节字面量,其中HH是任何两位十六进制数,表示其值为HH的字节.例如,你可以将ASCII "escape"控制字符写为b'\x1b'字节字面量.因为"escape"的ASCII代码是27或十六进制的1B.由于字节字面量只是u8值的另一种表示法,考虑一个简单的数字字面量是否更加清晰:只有当你想强调该值代表一个ASCII码时,使用b'\x1b'而不是简单的27可能是有意义.

你可以使用as运算符将一种整数类型转换为另一种整数类型.我们将在第139页的"类型转换(Type Casts)"中解释转换的工作方式,但以下是一些示例:

assert_eq!(   10_i8 as u16,    10_u16); // in range
assert_eq!( 2525_u16 as i16,  2525_i16); // in range

assert_eq!(   -1_i16 as i32,    -1_i32); // sign-extended
assert_eq!(65535_u16 as i32, 65535_i32); // zero-extended

// Conversions that are out of range for the destination
// produce values that are equivalent to the original modulo 2^N,
// where N is the width of the destination in bits. This
// is sometimes called "truncation".
assert_eq!( 1000_i16 as u8,   232_u8);
assert_eq!(65535_u32 as i16,    -1_i16);

assert_eq!(   -1_i8 as u8,    255_u8);
assert_eq!(  255_u8 as i8,     -1_i8);

像任何其他类型的值一样,整数也有方法.标准库提供了一些基本操作,你可以在在线文档中查找.请注意,文档包含类型本身的单独页面(搜索"i32(基本类型)",比如说)以及专用于该类型的模块(搜索"std::i32").例如:

assert_eq!(2u16.pow(4), 16);            // exponentiation
assert_eq!((-4i32).abs(), 4);           // absolute value
assert_eq!(0b101101u8.count_ones(), 4); // population count

这里需要字面量的类型后缀:Rust在知道其类型之前无法查找值的方法.但是,在实际代码中,通常需要额外的上下文来消除类型的歧义,因此不需要后缀.

浮点型(Floating-Point Types)

Rust提供IEEE单精度和双精度浮点类型.遵循IEEE 754-2008规范,这些类型包括正负无穷大,不同的正负零值和 非数(not-a-number) 值:

类型 精度 范围
f32 IEEE单精度(至少至少6位小数) 大概–3.4×10^38^到+3.4×10^38^
f64 IEEE单精度(至少至少15位小数) 大概–1.8×10^308^到+1.8×10^308^

Rust的f32f64对应于支持IEEE浮点数的C和C++实现中的float和double类型,而Java中始终使用IEEE浮点数.

浮点数字面量的一般形式如图3-1所示.

图3-1. 一个浮点数字面量.

浮点数的整数部分之后的每个部分都是可选的,但必须至少存在小数部分,指数或类型后缀中的一个,以将其与整数字面量区分开.小数部分可能包含一个小数点,因此5.是一个有效的浮点常数.

如果浮点字面量缺少类型后缀,Rust会从上下文中推断它是f32还是f64,如果两者都可能则默认为f64.(类似地,C,C++和Java都将没有后缀的浮点字面量视为double值.)出于类型推断的目的,Rust将整数字面量和浮点字面量视为不同的类别:它永远不会将整数字面量推断为浮点类型,反之亦然.

一些浮点字面量的例子:

字面量 类型 数值
-1.5625 依推断 $-(1\frac{9}{16})$
2. 依推断 2
0,25 依推断 $\frac{1}{4}$
1e4 依推断 10,000
40f32 f32 40
9.109_383_56e-31f64 f64 大概9..10938356×10^-31^

标准库的std::f32std::f64模块定义了IEEE所需特殊值的常量,如INFINITY,NEG_INFINITY(负无穷大),NAN(非数值),MINMAX(最大值和最大值有限值).std::f32::constsstd::f64::consts模块提供各种常用的数学常数,如E,PI和2平方根.

f32f64类型提供了完整的数学计算方法;例如,2f64.sqrt()是2的双精度平方根.标准库文档以"f32(基本类型)"和"f64(基本类型)"的名称描述这些它们.一些例子:

assert_eq!(5f32.sqrt() * 5f32.sqrt(), 5.); // exactly 5.0, per IEEE
assert_eq!(-1.01f64.floor(), -1.0);
assert!((-1. / std::f32::INFINITY).is_sign_negative());

和以前一样,你通常不需要在实际代码中写出后缀,因为上下文将确定类型.但是,如果没有,则错误消息可能会令人惊讶.例如,以下内容无法编译:

println!("{}", (2.0).sqrt());

Rust抱怨说:

error: no method named `sqrt` found for type `{float}` in the current scope

这可能有点令人困惑;除了浮点类型,还能在哪里找到sqrt方法呢?解决方法是用这样或那样的方式说明你想要的类型:

println!("{}", (2.0_f64).sqrt());
println!("{}", f64::sqrt(2.0));

与C和C ++不同,Rust几乎不会隐式地执行数字转换.如果函数需要f64参数,则将i32值作为参数传递是错误的.实际上,Rust甚至不会将i16值隐式转换为i32值,即使每个i16值也是i32值.但这里的关键词是 隐式地(implicitly) :你总是可以使用as运算符写出 显式地(explicit) 转换:i as f64,或x as i32.缺少隐式转换有时会使Rust表达式比类似的C或C++代码更冗长.但是,隐式整数转换具有导致错误和安全漏洞的完善记录;根据我们的经验,在Rust中写出数字转换的行为已经提醒我们我们可能会错过的问题.我们将在"类型转换"(第139页)中的详细解释转换行为.

布尔类型(The bool Type)

Rust的布尔类型bool通常有两个值,truefalse.比较运算符如==<产生bool结果:2 < 5的值为true.

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

Rust的as操作符可以将bool值转为整型:

assert_eq!(false as i32, 0);
assert_eq!(true as i32, 1);

但是,由于不会在另一个方向转换,从数字类型转换为bool.相反,你必须写出一个像x!= 0这样的显式比较.尽管bool只需要一个位来表示它,但是Rust在内存中使用整个字节作为bool值,因此你可以创建一个指向它的指针.

字符(Characters)

Rust的字符类型char表示单个Unicode字符,为32位值.

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

字符字面量是用单引号括起来的字符,如'8''!'.你可以使用你喜欢的任何Unicode字符:'锖'是代表日本汉字 sabi(rust)的char字面量.

与字节字面量一样,几个字符需要反斜杠转义:

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

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

  • 如果字符的代码点在U+0000到U+007F的范围内(即,如果它是从ASCII字符集中绘制的),则可以将字符写为'\xHH',其中HH是两位数十六进制数.例如,字符文字'*''\x2A'是等价的,因为字符*的代码点是42,或者是十六进制的2A.

  • 您可以将任何Unicode字符写为'\u{HHHHHH}',其中HHHHHH是长度为1到6位的十六进制数字.例如,字符字面量'\u{CA0}'表示字符"ಠ",一个坎纳达语(Kannada)字符,用在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 truncated to eight bits, signed

从另一个方向来看,u8as运算符能够转换为char的唯一类型:Rust意图as运算符只执行简单,可靠的转换,但除了u8之外的每个整数类型都包含非法的Unicode代码点的值,所以那些转换需要运行时检查.相反,标准库函数std::char::from_u32接受任何u32值并返回Option <char>:如果u32不是合法的Unicode代码点,则from_u32返回None;否则,它返回Some(c),其中cchar结果.

标准库提供了一些有用的字符方法,您可以通过搜索"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'));

当然,孤立的单个字符不像字符串和文本流那样有趣.我们将在第64页的"字符串类型"中描述Rust的标准String类型和文本处理.

元组(Tuples)

元组 是一对,或三个,或四个,...各种类型的值.你可以将元组写为用逗号分隔并用括号括起来的元素序列.例如,("Brazil", 1985)是一个元组,其第一个元素是静态分配的字符串,第二个元素是整数;它的类型是(&str, i32)(或任何Rust由1985推断出的整数类型). 给定一个元组值t,你可以用t.0,t.1,等访问其元素.

元组与数组不太相似:一方面,元组的每个元素都可以有不同的类型,而数组的元素必须是相同的类型.此外,元组只允许常量作为索引,如t.4.你不能写t.it[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章的Mandelbrot程序中,我们需要将图像的宽度和高度传递给绘制它的函数,并将其写入磁盘.我们可以声明一个具有宽度和高度成员的结构,但是对于一些显而易见的东西,这是相当沉重的符号,所以我们只使用一个元组:

/// Write the buffer `pixels`, whose dimensions are given by `bounds`, to the
/// file named `filename`.
fn write_image(filename: &str, pixels: &[u8], bounds: (usize, usize))
    -> Result<(), std::io:Error>
{ ... }

bounds参数的类型是(usize,usize),一个由两个usize值组成的元组.不可否认的是,我们也可以单独写出宽度和高度的参数,并且机器代码的方式大致相同. 这是一个清晰度的问题.我们认为大小是一个值,而不是两个,使用元组让我们写出我们的意思.

另一种常用的元组类型,也许令人惊讶,是零元组(). 这通常称为单元类型(unit type) ,因为它只有一个值,也是写出的().Rust使用单元类型,其中没有有意义的值可以携带,但上下文需要某种类型.

例如,不返回值的函数的返回类型为().标准库的std::mem::swap函数没有有意义的返回值;它只是交换两个参数的值.std::mem::swap的声明如下:

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

<T>意味着swap泛型的(generic) :你可以在引用任何类型T的值时使用它.但签名完全省略了swap的返回类型,这是返回单元类型的简写:

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

类似地,我们之前提到的write_bitmap示例的返回类型为Result<(), std::io::Error>,这意味着如果出现错误,函数将返回std::io::Error值.但成功时不返回任何值.

如果你愿意,你可以在元组的最后一个元素后面加一个逗号:类型(&str, i32,)(&str, i32)是等价的,表达式("Brazil", 1985,)("Brazil", 1985)也一样.Rust始终允许在使用逗号的任何地方使用额外的尾随逗号:函数参数,数组,结构和枚举定义等.这对于人类读者来说可能看起来很奇怪,但是当在列表的末尾添加和删除条目时,它可以使差异更容易阅读.

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

指针类型(Pointer Type)

Rust有几种表示内存地址的类型.

这是Rust和大部分具有垃圾回收的语言之间最大的不同.在Java中,如果class Tree包含一个字段Tree left;,则left是对另一个单独创建的Tree对象的引用.对象从不在物理上包含Java中的其他对象.

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

这对于内存效率非常好,但因此,当Rust程序需要指向其它值的值时,必须显式地使用指针类型.好消息是安全Rust中使用的指针类型会受到约束,以消除未定义的行为,因此指针在Rust中比在C++中更容易正确使用.

我们将在这里讨论三种指针类型:引用,boxes和不安全指针.

引用(References)

类型&String(发音为"ref String")的值是对String值的引用,&i32是对i32的引用,依此类推.

通过将引用视为Rust的基本指针类型来开始是最容易的.引用可以指向任何位置(堆栈或堆)的任何值.表达式&x产生对x的引用;在Rust术语中,我们说它 借用了对 x的引用(borrows a reference to x) .给定引用r,表达式*r指的是r指向的值.这些非常类似于C和C++中的&*运算符.和C指针一样,当引用超出作用域时,引用不会自动释放任何资源.

但是,与C指针不同,Rust引用永远不会为空(null):根本无法在安全Rust中生成空引用.默认情况下,Rust引用是不可变的:

&T

不可变引用,如C中的const T*.

&mut T

可变引用,如C中的T*.

另一个主要区别是Rust跟踪值的所有权和生命周期,因此在编译时排除了诸如悬空指针(dangling pointers),双重释放(double frees)和指针失效(pointer invalidation)之类的错误.第5章解释了Rust的安全引用使用规则.

Boxes

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

let t = (12, "eggs");
let b = Box::new(t);  // allocate a tuple in the heap

t的类型是(i32, &str),所以b的类型是Box<(i32, &str)>.Box::new()在堆上分配足够的内存来包含元组.当b超出作用域时,内存会立即释放,除非b被 移动(moved) --例如返回它.

移动对于Rust处理堆分配值的方式至关重要;我们将在第4章详细解释这一切.

裸指针(Raw Pointers)

Rust还有原始指针类型*mut T*const T.原始指针真的就像C++中的指针一样.使用原始指针是不安全的,因为Rust不会努力跟踪它指向的内容.例如,原始指针可以为空,或者它们可以指向已释放的内存或现在包含不同类型的值的内存.C++的所有经典指针错误都提供给你欣赏.

但是,你只能在不安全块中解引用原始指针.不安全块是Rust的高级语言特性的选择机制,其安全性取决于你. 如果你的代码没有不安全块(或者它所包含的块编写正确),那么我们在本书中强调的安全保证仍然存在.有关详细信息,请参阅第21章.

数组,向量和切片(Arrays,Vectors,and Slices)

Rust有三种类型用于表示内存中的值序列:

  • 类型[T; N]表示N个值的数组,每个类型都是T.数组的大小是在编译时确定的常量,并且是类型的一部分;你不能追加新的元素,或缩小数组.

  • Vec<T>类型,称为 T的向量(vector of Ts),是一个动态分配的,可增长的T类型的值的序列.向量的元素存在于堆上,因此您可以随意调整向量的大小:将新元素推到它们上,其他向量附加到它们上,删除元素,等等.

  • 类型$[T]$mut [T],称为 T的共享切片(shared slice of Ts)T的可变切片(mutable slice of Ts) ,是对一系列元素的引用,这些元素是某个其他值的一部分,如数组或向量.你可以将切片视为指向其第一个元素的指针,以及从该点开始可以访问的元素数量的计数.可变切片&mut [T]允许你读取和修改元素,但不能共享;共享切片&[T]允许你在多个读者之间共享访问权限,但不允许你修改元素.

给定这三种类型中的任意一种的v值,表达式v.len()给出v中元素的数量,v[i]v的第i个元素. 第一个元素是v[0],最后一个元素是v[v.len()- 1]. Rust检查i总是在这个范围内;如果没有,表达式会panics(恐慌).v的长度可以为零,在这种情况下,任何索引它的尝试都会引起恐慌.i必须是一个usize值;你不能使用任何其它整数类型作为索引.

数组(Arrays)

有几种方法可以编写数组值.最简单的是在方括号内写一系列值:

let lazy_caterer: [u32; 6] = [1, 2, 4, 7, 11, 16];
let taxonomy = ["Animalia", "Arthropoda", "Insecta"];
assert_eq!(lazy_caterer[3], 7);
assert_eq!(taxonomy.len(), 3);

对于填充了某个值的长数组的常见情况,可以写[V; N],其中V是每个元素应该具有的值,N是长度.例如,[true; 10000]是一个包含10,000个bool元素的数组,全部元素设置为true:

let mut sieve = [true; 10000];
for i in 2..100 {
    if sieve[i] {
        let mut j = i * i;
        while j < 10000 {
            sieve[j] = false;
            j += i;
        }
    }
}

assert!(sieve[211]);
assert!(!sieve[9876]);

你会看到这个语法用于固定大小的缓冲区:[0u8; 1024]可以是一个千字节的缓冲区,填充零字节.Rust没有未初始化数组的表示法.(通常,Rust确保代码永远不会访问任何未初始化的值.)

数组的长度是其类型的一部分,在编译时固定.如果n是变量,则不能写[true; n]获取n个元素的数组. 如果需要一个长度在运行时变化的数组(通常也是如此),请使用向量.

你希望在数组上看到的有用方法--迭代元素,搜索,排序,填充,过滤等等--都显示为切片的方法,而不是数组.但是Rust在搜索方法时隐式地将对数组的引用转换为切片,因此您可以直接在数组上调用任何切片方法:

let mut chaos = [3, 5, 4, 1, 2];
chaos.sort();
assert_eq!(chaos, [1, 2, 3, 4, 5]);

这里,sort方法实际上是在切片上定义的,但由于sort通过引用获取其操作数,我们可以直接在chaos上使用它:该调用隐式地产生一个引用整个数组的&mut [i32]切片. 实际上,我们前面提到的len方法也是一种切片方法.我们在第62页的"切片(Slices)"中更详细地介绍了切片.

向量(Vectors)

向量Vec<T>一个可调整大小的T类型元素的数组,在堆上分配.

有几种方法可以创建向量,最简单的是使用vec!宏,它为我们提供了一个看起来非常像数组字面量的向量语法:

let mut v = vec![2, 3, 5, 7];
assert_eq!(v.iter().fold(1, |a, b| a * b), 210);

但是,当然,这是一个向量,而不是一个数组,所以我们可以动态地添加元素:

v.push(11);
v.push(13);
assert_eq!(v.iter().fold(1, |a, b| a * ), 30030);

您还可以通过重复给定值一定次数来构建向量,同样使用模仿数组字面量的语法:

fn new_pixel_buffer(rows: usize, cols: usize) -> Vec<u8> {
    vec![0; rows * cols]
}

vec!宏相当于调用Vec::new创建一个新的空向量,然后将元素推入到它里面,这是另一个惯用语法:

let mut v = Vec::new();
v.push("step");
v.push("on");
v.push("no");
v.push("pets");
assert_eq!(v, vec!["step", "on", "no", "pets"]);

另一种可能性是从迭代器生成的值构建一个向量:

let v: Vec<i32> = (0..5).collect();
assert_eq!(v, [0, 1, 2, 3, 4]);

在使用collect时你经常需要提供类型(正如我们在这里所做的那样),因为它可以构建许多不同类型的集合,而不仅仅是向量.通过为v显式提供类型,我们已经明确地将它们变成了我们想要的那种集合.

和数组一样,你可以在向量上使用切片方法:

// A palindrome!
let mut v = vec!["a man", "a plan", "a canal", "panama"];
v.reverse();
// Reasonable yet disappointing:
assert_eq!(v, vec!["panama", "a canal", "a plan", "a man"]);

这里,reverse方法实际上是在切片上定义的,但是调用隐式地从向量中借用了一个&mut [&str]切片,并在其上调用reverse.

Vec是Rust的一个重要的类型--它被用在几乎任何需要动态大小列表的地方--因此有许多其它方法可以构建新向量或扩展现有向量.我们将在第16章介绍它们.

Vec<T>由三个值组成:一个指向堆分配的缓冲区的指针,该缓冲区用于保存元素; 缓冲区有容量存储的元素数量; 以及它实际包含的元素数量(换句话说,它的长度).当缓冲区达到其容量时,向向量添加另一个元素需要分配更大的缓冲区,将当前内容复制到其中,更新向量的指针和容量以描述新的缓冲区,最后释放旧的缓冲区.

如果你事先知道向量需要的元素数量,从一开始,你可以调用Vec::with_capacity而不是Vec::new来创建一个具有足够大的缓冲区来容纳所有元素的向量;然后,你可以一次一个地向向量添加元素,而不会导致任何重新分配.vec!宏使用了这样的技巧,因为它知道最终向量将有多少个元素.注意,这只建立了向量的初始大小;如果超出估计值,向量会像往常一样扩展它的存储空间.

许多库函数都在寻找机会去使用Vec::with_capacity替代Vec::new.例如,在collect示例中迭代器0..5事先知道它将产生五个值,并且collect函数利用它来预先分配它返回的向量以正确的容量.我们将在第15章中看到它是如何工作的.

就像vectorlen方法返回它现在包含的元素数一样,它的capacity方法返回它可以保存但不重新分配的元素数:

let mut v = Vec::with_capacity(2);
assert_eq!(v.len(), 0);
assert_eq!(v.capacity(), 2);

v.push(1);
v.push(2);
assert_eq!(v.len(), 2);
assert_eq!(v.capacity(), 2);

v.push(3);
assert_eq!(v.len(), 3);
assert_eq!(v.capacity(), 4);

你在你的代码中看到的容量可能与此处显示的容量不同.即使在with_capacity情况下,Vec和系统的堆分配器也可以对请求进行舍入.

你可以在向量中的任何位置插入和删除元素,尽管这些操作会在插入点向前或向后移动所有元素,因此如果向量很长,它们可能会很慢:

let mut v = vec![10, 20, 30, 40, 50];
// Make the element at index 3 be 35.
v.insert(3, 35);
assert_eq!(v, [10, 20, 30, 35, 40, 50]);

// Remove the element at index 1.
v.remove(1);
assert_eq!(v, [10, 30, 35, 40, 50]);

你可以使用pop方法删除最后一个元素并将其返回.更准确地说,从Vec<T>中弹出一个值会返回一个Option<T>:如果向量已经为空,则返回None;如果最后一个元素是v,则返回Some(v):

let mut v = vec!["carmen", "miranda"];
assert_eq!(v.pop(), Some("miranda"));
assert_eq!(v.pop(), Some("carmen"));
assert_eq!(v.pop(), None);

你可以使用for循环迭代向量:

// Get our command-line arguments as a vector of Strings.
let languages: Vec<String> = std::env::args().skip(1).collect();
for l in languages {
    println!("{}: {}", l,
            if l.len() % 2 == 0 {
                "functional"
            } else {
                "imperative"
            });
}

用一系列编程语言运行该程序很有启发性:

$ cargo run Lisp Scheme C C++ Fortran
   Compiling fragments v0.1.0 (file:///home/jimb/rust/book/fragments)
     Running `.../target/debug/fragments Lisp Scheme C C++ Fortran`
Lisp: functional
Scheme: functional
C: imperative
C++: imperative
Fortran: imperative
$

最后,对术语 函数式语言(functional language) 的定义令人满意.

尽管它具有基本的作用,但Vec是Rust中定义的普通类型,不是内置在语言中.我们将在第21章介绍实现此类类型所需的技术.

一个元素一个元素地构建向量(Building Vectors Element by Element)

一次一个元素地构建向量并不像它听起来那么糟糕.每当向量超出其缓冲区的容量时,它会选择一个旧缓冲区两倍大的新缓冲区.假设向量以一个只能容纳一个元素的缓冲区开始:当它增长到最终容量时,它将具有的缓冲区大小为1,2,4,8,依此类推,直到达到最终大小为2^n^,对于某些n来说.如果你考虑2的幂是如何工作的,你会发现所有先前较小的缓冲区加起来的总大小是2^n^-1,非常接近最终的缓冲区大小.由于实际元素的数量至少是缓冲区大小的一半,因此向量总是每个元素执行少于两个副本!

这意味着使用Vec::with_capacity而不是Vec::new是一种在速度上获得常数因子改进的方法,而不是算法改进.对于小向量,避免一些对堆分配器的调用可以在性能上产生明显的差异.

切片(Slices)

切片写作[T],没有指定长度,是一个数组或向量的区域.由于切片可以是任何长度,因此切片不能直接存储在变量中或作为函数参数传递.切片总是通过引用传递.

对切片的引用是一个 胖指针(fat pointer) :双字值,包括一个指向切片的第一个元素的指针,以及切片中的元素数.

假设您运行以下代码:

let v: Vec<f64> = vec![0.0,  0.707,  1.0,  0.707];
let a: [f64; 4] =     [0.0, -0.707, -1.0, -0.707];

let sv: &[f64] = &v;
let sa: &[f64] = &a;

在最后两行,Rust自动转换&Vec<f64>引用和&[f64; 4]引用为引用直接指向数据的指针的切片.

到最后,内存如图3-2所示:

图3-2. 内存中的向量v和数组a,以及分别引用它们的切片sa和sv.

普通引用是指向单个值的非拥有指针,而对切片的引用是指向多个值的非拥有指针.这使切片引用成为编写一个操作任何同类数据序列的函数的很好的选择,不管它是存储在数组,向量,堆栈还是堆中.例如,这是一个打印数字切片的函数,每行一个:

fn print(n: &[f64]) {
    for elt in n {
        println!("{}", elt);
    }
}

print(&v);  
// works on vectors
print(&a);  
// works on arrays

因为此函数将切片引用作为参数,所以可以将其应用于向量或数组,如图所示.实际上,你可能认为属于向量或数组的许多方法事实上是在切片上定义的:例如,sortreverse方法(它们对元素序列进行排序或反转)实际上是切片类型[T]上的方法.

你可以通过使用范围索引来获取对数组或向量的切片,或现有切片的切片的引用:

print(&v[0..2]);
// print the first two elements of v
print(&a[2..]);
// print elements of a starting with a[2]
print(&sv[1..3]);
// print v[1] and v[2]

与普通数组访问一样,Rust检查索引是否有效.试图借用延伸到数据末尾的切片会导致panic.

我们经常使用 切片(slice) 这个词指代像&[T]&str这样的引用类型,但这是一种简写:正确地称呼是切片的引用(references to slices) .由于切片几乎总是出现在引用之后,因此我们使用较短的名称来表示更常见的概念.

字符串类型(String Types)

熟悉C++的程序员会记得,该语言中有两种字符串类型.字符串字面量的指针类型为const char *.标准库还提供了一个类std::string,用于在运行时动态创建字符串. Rust有类似的设计.在本节中,我们将展示编写字符串字面量的所有方法,然后介绍Rust的两种字符串类型.我们在第17章中提供了有关字符串和文本处理的更多细节.

字符串字面量(String Literals)

字符串字面量用双引号括起来.它们使用与char字面量相同的反斜杠转义序列:

let speech = "\"Ouch!\" said the well.\n";

在字符串字面量中,与char字面量不同,单引号不需要反斜杠转义,双引号需要.

字符串可能跨越多行:

println!("In the room the women come and go,
    Singing of Mount Abora");

该字符串文字中的换行符包含在字符串中,因此也包含在输出中.第二行开头的空格也是如此.

如果字符串的一行以反斜杠结尾,则删除换行符和下一行的前导空格:

println!("It was a bright, cold day in April, and \
    there were four of us—\
    more or less.");

这会打印一单行文本.该字符串在"and"和"there"之间包含一个空格,因为在程序中反斜杠之前有一个空格,而破折号(dash)之后没有空格.

在少数情况下,需要将字符串中的每个反斜杠加倍是一件麻烦事.(典型示例是正则表达式和Windows路径.)对于这些情况,Rust提供 原始字符串(raw strings) . 原始字符串用小写字母r标记.原始字符串中的所有反斜杠和空格字符都逐字包含在字符串中.没有识别转义序列.

let default_win_install_path = r"C:\Program Files\Gorillas";
let pattern = Regex::new(r"\d+(\.\d+)*");

你不能在原始字符串中包含一个双引号字符,仅仅在它前面放一个反斜杠--记住,我们说 没有 识别转义序列.但是,也有办法解决这个问题.原始字符串的开头和结尾可以用磅字符(#)标记:

println!(r###"
    This raw string started with 'r###"'.
    Therefore it does not end until we reach a quote mark ('"')
    followed immediately by three pound signs ('###'):
"###);

您可以根据需要添加几个或多个的磅标记,以便清楚原始字符串结束的位置.

字节字符串(Byte Strings)

带有b前缀的字符串字面量是 字节字符串(byte string) .这样的字符串是一个u8值的切片--即字节--而不是Unicode文本:

let method = b"GET";
assert_eq!(method, &[b'G', b'E', b'T']);

这与我们展示的所有其他字符串语法相结合:字节字符串可以跨越多行,使用转义序列,并使用反斜杠来连接行.原始字节字符串以br"开头.

字节字符串不能包含任意Unicode字符.他们必须使用ASCII和\xHH转义序列.

这里显示的method的类型是&[u8; 3]它是对三个字节数组的引用.它没有我们将在一分钟内讨论的任何字符串方法.关于它的最类似于字符串的事情是我们用来编写它的语法.

内存中的字符串(String in Memery)

Rust字符串是Unicode字符序列,但它们不作为char数组存储在内存中.相反,它们使用UTF-8存储,这是一种可变宽度编码.字符串中的每个ASCII字符都存储在一个字节中.其他字符占用多个字节.

图3-3展示了显示代码创建的String&str值:

let noodles = "noodles".to_string();
let oodles = &noodles[1..];
let poodles = "ಠ_ಠ";

图3-3. 字符串,&str,和str.

String有一个可调整大小的缓冲区,用于保存UTF-8文本.缓冲区在堆上分配,因此可以根据需要或请求调整其缓冲区的大小.在示例中,noodles是一个拥有8字节缓冲区的String,其中7个正在使用中.你可以将String视为Vec<u8>,它保证能容纳格式良好的UTF-8;实际上,这就是String的实现方式.

&str(发音为"stir"或"string slice(字符串切片)")是对其他人拥有的UTF-8文本的引用:它"借用"文本. 在示例中,oodles是一个&str,指的是属于noodles的文本的最后六个字节,因此它代表文本"oodles",与其他切片引用一样,&str是一个胖指针,包含实际数据的地址及其长度.你可以认为&str只不过是一个保证能够保持结构良好的UTF-8的&[u8].

字符串字面量是&str,它指的是预先分配的文本,通常与程序的机器码一起存储在只读存储器中.在前面的示例中,poodles是一个字符串字面量,指向程序开始执行时创建的七个字节,直到程序退出为止.

String&str.len()方法返回其长度.此长度以字节为单位,而不是字符:

assert_eq!("ಠ_ಠ".len(), 7);
assert_eq!("ಠ_ಠ".chars().count(), 3);

修改&str是不可能的:

let mut s = "hello";
s[0] = 'c';    // error: the type `str` cannot be mutably indexed
s.push('\n');  // error: no method named `push` found for type `&str`

要在运行时创建新字符串,请使用String.

字符串(String)

&str非常像&[T]:指向某些数据的胖指针.String则类似于Vec<T>:

Vec<T> String
自动释放缓冲区
可增长
::new()::with_capacity()静态方法
.reserve().capacity方法
.push.pop()方法
范围语法v[atast..stop] 是,返回&[T] 是,返回&str
自动转换 &Vec<T>&[T] &String&str
继承方法 &[T] &str

Vec一样,每个String都有自己的堆分配的缓冲区,不与任何其它String共享.当String变量超出作用域时,将自动释放缓冲区,除非移动了String.

有几种创建字符串的方法:

  • .to_string()方法将&str转换为String.这会复制字符串:
    let error_message = "too many pets".to_string();
  • format!()宏的工作方式与println!()类似,不同之处在于它返回一个新的String而不是将文本写入stdout,并且它不会在末尾处自动添加换行符.
    assert_eq!(format!("{}°{:02}′{:02}′′N", 24, 5, 23),
        "24°05′23′′N".to_string());
  • 字符串的数组,切片和向量有两个方法,.concat().join(sep)它们从许多字符串组成一个新的String.
    let bits = vec!["veni", "vidi", "vici"];
    assert_eq!(bits.concat(), "venividivici");
    assert_eq!(bits.join(", "), "veni, vidi, vici");

有时会出现使用哪种类型的选择:&strString. 第5章详细讨论了这个问题.现在,只需要指出的是,&str可以引用任何字符串的任何片切片,无论它是字符串字面量(存储在可执行文件中)还是String(在运行时分配和释放).这意味着当允许调用者传递任何一种字符串时,&str更适合于函数参数.

使用字符串(Using Strings)

字符串支持==!=操作符.如果两个字符串包含相同顺序的相同字符(无论它们是否指向内存中的相同位置),则它们是相等的.

assert!("ONE".to_lowercase() == "one");

字符串还支持比较运算符<,<=,>>=,以及许多有用的方法和函数,你可以通过搜索"str(基本类型(primitive type)"或"std::str"模块(或干脆翻到第17章)在在线文档中找到.这里有一些例子:

assert!("peanut".contains("nut"));
assert_eq!("ಠ_ಠ".replace("ಠ", "■"), "■_■");
assert_eq!("    clean\n".trim(), "clean");

for word in "veni, vidi, vici".split(", ") {
    assert!(word.starts_with("v"));
}

请记住,鉴于Unicode的性质,简单的逐个char(char-by-char)比较并不总是给出预期的答案.例如,Rust字符串"th\u{e9}""\u{301}"都是 thé 的有效Unicode表示, thé 是法语中的茶的单词. Unicode表示,它们应该以相同的方式显示和处理,但Rust将它们视为两个完全不同的字符串.类似地,Rust的排序操作符如<使用基于字符代码点值的简单字典顺序.这种排序有时类似于用户语言和文化中用于文本的排序.我们将在第17章中更详细地讨论这些问题.

其它类似字符串的类型(Other String-Like Types)

Rust保证字符串是有效的UTF-8.有时,程序确实需要能够处理 非(not) 有效的Unicode字符串.这通常发生在Rust程序必须与不强制执行任何此类规则的其他系统进行互操作时.例如,在大多数操作系统中,很容易创建一个文件名不是Unicode的文件.当Rust程序遇到这种文件名时会发生什么?

Rust的解决方案是为这些情况提供一些类似字符串的类型:

  • 坚持使用String&str支持Unicode文本.
  • 处理文件名时,请使用std::path::PathBuf&Path.
  • 处理完全不是字符数据的二进制数据时,请使用Vec<u8>&[u8].
  • 在处理操作系统提供的本机形式的环境变量名称和命令行参数时,请使用OsString&OsStr.
  • 当与使用以null结尾的字符串的C库进行互操作时,请使用std::ffi::CString&CStr.

超越基本类型(Beyong the Basic)

类型是Rust的核心部分.我们将继续讨论类型并在整本书中介绍新类型.特别是,Rust的用户定义类型为语言提供了很多风格,因为这是定义方法的地方.有三种用户定义的类型,我们将在三个连续的章节中介绍它们:第9章中的结构,第10章中的枚举和第11章中的traits.

函数和闭包有各自的类型,在第14章中介绍.整本书涵盖了构成标准库的类型.例如,第16章介绍了标准集合类型.

但所有这些都必须先等等.在我们继续之前,是时候解决Rust的安全规则的核心概念了.