Rust的类型系统借助其结构化类型和特征从功能世界中借鉴了很多。 类型系统非常强大:类型不会在引擎盖下发生变化,当某些东西需要类型X时,你需要给它类型X.此外,类型系统是静态的; 几乎所有的类型检查都是在运行时完成的。 这些功能为您提供了非常强大的程序,在运行时很少做错事,编写程序的成本变得更加受限制。
Rust的整个类型系统不小,我们将尝试深入研究它。 本章期待很多重物,勇敢!
在本章中,我们将介绍以下主题:
- 字符串类型
- 数组和切片
- 通用类型
- 特征和实施
- 常数和静力学
字符串类型
Rust有两种类型的字符串:字符串切片(str)和字符串。 运行时检查保证它们都是有效的Unicode字符串,并且它们都在内部编码为UTF-8。 没有单独的非Unicode字符或字符串类型; 原始类型u8用于可能是也可能不是Unicode的字节流。
为什么这两种? 它们的存在主要是因为Rust的内存管理及其运行时成本为零的理念。 在程序周围传递字符串切片几乎是免费的:它几乎不会产生分配成本,也不会复制内存。 不幸的是,没有任何东西实际上是免费的,在这种情况下,这意味着你,程序员,将需要支付一些价格。 字符串切片的概念对你来说可能是新的,并且有点棘手,但理解它会给你带来很大的好处。
我们潜入吧。
字符串切片
str类型具有固定大小,其内容不能更改。 此类型的值通常以以下三种方式之一用作借用类型(&str):
- 指向静态分配的字符串(&'static str)
- 作为函数的参数
- 作为另一个数据结构内的字符串的视图
让我们看看每个代码如何看待代码。 首先,这里有两种静态分配的字符串,一种在全局范围内,另一种在函数范围内:
// string-slices.rs
const CONSTANT_STRING: &'static str = "This is a constant string";
fn main() {
let another_string = "This string is local to the main function";
println!("Constant string says: {}", CONSTANT_STRING);
println!("Another string says: {}", another_string);
}
您可能还记得,Rust有本地类型推断,这意味着我们可以在适合我们时省略函数体内的类型。 在这种情况下,它确实适合我们,因为两个字符串的类型都不漂亮:&'static str。 让我们一块一块地阅读类型语法:
- &表示这是一个引用
- 'static意味着引用的生命周期是静态的,也就是说,它在程序的整个持续时间内都存在
- str表示这是一个字符串切片
在第6章,记忆,生命周期和借阅中更全面地介绍了参考文献和生命周期。 不过,您已经可以理解,CONSTANT_STRING和another_string都不是字符串切片本身。 相反,它们指向现有的字符串,以及这些字符串在程序执行期间的生存方式由生命周期“静态”明确指定。
让我们看看第二点:使用字符串切片作为函数的参数。 除非你知道自己在做什么,并且特别需要特殊的东西,否则字符串切片是将字符串传递给函数的方法。
这是一个很容易错过的重点,因此可以重复强调:如果要将字符串传递给函数,请使用&str类型。 这是一个例子:
// passing-string-slices.rs
fn say_hello(to_whom: &str) {
println!("Hey {}!", to_whom)
} f
n main() {
let string_slice: &'static str = "you";let string: String = string_slice.into();
say_hello(string_slice);
say_hello(&string);
}
在这里,您可以看到为什么我之前强调了这一点。 字符串切片是一个可接受的输入参数,不仅适用于实际的字符串切片引用,还适用于String引用! 所以,再一次:如果你需要将一个字符串传递给你的函数,使用字符串slice,&str。
这将我们带到第三点:使用字符串切片作为其他数据结构的视图。 在前面的代码中,我们就是这样做的。 变量字符串是String类型,但我们可以通过添加引用运算符&而将其内容作为字符串片段借用。 这种操作非常便宜,因为不需要进行数据复制。
字符串类型
好的,我们来看看更高级别的String类型。 与字符串切片一样,其内容保证为Unicode。 与字符串切片不同,String是可变的和可增长的,它可以在运行时创建,它实际上将数据保存在其中。 不幸的是,这些伟大的功能有一个缺点。 String类型的成本不是零:它需要在堆中分配,并且可能在它增长时重新分配。 堆分配是一项相对昂贵的操作,但幸运的是,对于大多数应用来说,这个成本可以忽略不计。 我们将在第6章,内存,生命周期和借阅中更全面地介绍内存分配。
String类型可以相当透明地转换为&str(如我们刚刚看到的示例中),但反之亦然。 如果需要,则必须显式请求从字符串切片创建新的String类型。 这就是前一个例子的这一行:
let string: String = string_slice.into();
对任何事物调用into()是将类型从一个值转换为另一个值的通用方法。 Rust指出你想要一个String类型,因为类型是(并且必须)明确指定。 当然,并非所有转换都已定义,如果您尝试进行此类转换,则会出现编译器错误。
让我们看一下使用标准库中的方法构建和操作String类型的不同方法。 这是一些重要的列表:
- String :: new()分配一个空的String类型。
- String :: from(&str)分配一个新的String类型并从字符串切片填充它。
- String :: with_capacity(capacity:usize)分配一个具有预分配大小的空String。
- String :: from_utf8(vec:Vec )尝试从bytestring中分配一个新的String。参数的内容必须是UTF-8,否则将失败。
- len()方法为您提供String的长度,将Unicode考虑在内。例如,包含单词yö的String的长度为2,即使它在内存中占用3个字节。
- push(ch:char)和push_str(string:&str)方法将字符或字符串切片添加到String。
当然,这是一个非排他性的清单。 有关Strings所有操作的完整列表,请访问
https://doc.rust-lang.org/std/string/struct.String.html.
这是一个使用所有上述方法的示例:
// mutable-string.rs
fn main() {
let mut empty_string = String::new();
let empty_string_with_capacity = String::with_capacity(50);
let string_from_bytestring: String = String::from_utf8(vec![82, 85, 83,
84]).expect("Creating String from bytestring failed");
println!("Length of the empty string is {}", empty_string.len());
println!("Length of the empty string with capacity is {}",
empty_string_with_capacity.len());println!("Length of the string from a bytestring is {}",
string_from_bytestring.len());
println!("Bytestring says {}", string_from_bytestring);
empty_string.push('1');
println!("1) Empty string now contains {}", empty_string);
empty_string.push_str("2345");
println!("2) Empty string now contains {}", empty_string);
println!("Length of the previously empty string is now {}",
empty_string.len());
}
字节串
第三种形式的字符串实际上不是字符串,而是字节流。 在Rust代码中,这是无符号的8位类型,封装在向量(Vec )或数组([u8])中。 与引用通常使用字符串切片的方式相同,数组也是如此。 因此,后一种类型通常用作&[u8]。
这就是我们在与外界交谈时必须使用字符串的方法。 您的所有文件都只是字节,就像我们收到的数据和发送到互联网一样。 这可能是一个问题,因为并非每个字节数组都是有效的UTF-8,这就是我们需要处理转换可能产生的任何错误的原因。 回想一下前面的转换:
let string_from_bytestring: String = String::from_utf8(vec![82,
85, 83, 84]).expect("Creating String from bytestring failed");
在这种情况下,转换是正常的,但如果没有,程序的执行就会在那里停止,因为重复一遍,Rust中的字符串保证是Unicode。
总结和任务(Takeaways and tasks)
让我们结束字符串。 这是要记住的:
- 有两种字符串类型:String和&str
- Rust中的字符串保证是Unicode
- 将字符串传递给函数时,请使用&str类型
- 从函数返回字符串时,请使用String类型
- 原始字节字符串是8位无符号整数的数组或向量(u8)
- 字符串是堆分配和动态增长的,这使它们变得灵活但成本更高
以下是一些任务:
1.创建一些字符串切片和字符串,然后打印它们。 使用push和push_str来填充带有数据的String。
2.编写一个带有字符串切片并打印出来的函数。 传递几个静态字符串切片和几个字符串。
3.使用UTF-8字符串和非UTF-8字符串定义字节字符串。 尝试将Strings从它们中删除,看看会发生什么。
4.创建一个包含短语You is a Rust编码器的字符串,Harry。 将字符串拆分为单词并打印第二个单词。 请参阅
https://doc.rust-lang.org/std/string/struct.String.html; 你需要使用collect()方法。
数组和切片
我们曾多次触及阵列。 让我们看一下。
数组包含任意单个类型的固定数量的元素。 他们的类型是[T; n],其中T是包含值的类型,n是数组的大小。 请注意,矢量类型(稍后介绍)为您提供动态大小的数组。 每次您希望自己创建阵列时,必须明确写出。 就像任何其他类型一样,数组可以是可变的也可以是不可变的。
可以使用数组名称后面的[n]语法通过索引访问数组,非常类似于其他语言。 如果您尝试索引超出数组的长度,此操作将在运行时导致混乱。
我们来看看下面的例子:
// fixed-array-example.rs
fn main() {
let mut integer_array_1 = [1, 2, 3];
let integer_array_2: [u64; 3] = [2, 3, 4];
let integer_array_3: [u64; 32] = [0; 32];
let integer_array_4: [i32; 16438] = [-5; 16438];
integer_array_1[1] = 255;
println!("integer_array_1: {:?}", integer_array_1);
println!("integer_array_2: {:?}", integer_array_2);
println!("integer_array_3: {:?}", integer_array_3);
// println!("integer_array_4: {:?}", integer_array_4);
println!("integer_array_1[0]: {}", integer_array_1[0]);
println!("integer_array_1[5]: {}", integer_array_1[5]);
}
最后一行需要被注释掉,因为只有等于或小于32的数组才能获得Debug特性,这意味着我们不能只是出去打印更大的特性。 以下是运行此程序的内容:
之前已经提到过字符串切片,但是可以对任何数组进行切片,而不仅仅是字符串。 切片简单而便宜:它们指向现有的数据结构并包含长度。 切片的类型接近于数组的类型:&[T]。 如您所见,与数组不同,此类型没有附加大小信息。
切片的语法是[n…m],其中n是切片的包含起始点,m是非包含端点。 换句话说,n处的元素包含在切片中,但m处的元素不包含在切片中。 第一个元素的索引是0。
以下是切片用法的示例:
// array-slicing.rs
use std::fmt::Debug;
fn print_slice<T: Debug>(slice: &[T]) {
println!("{:?}", slice);
} f
n main() {
let array: [u8; 5] = [1, 2, 3, 4, 5];
print!("Whole array just borrowed: ");
print_slice(&array);
print!("Whole array sliced: ");
print_slice(&array[..]);
print!("Without the first element: ");
print_slice(&array[1..]);
print!("One element from the middle: ");
print_slice(&array[3..4]);
print!("First three elements: ");
print_slice(&array[..3]);
//print!("Oops, going too far!: ");
//print_slice(&array[..900]);
}
有一个print_slice函数,它接受任何实现Debug特性的值。 所有这些值都可以输入println! 作为参数,大多数内部类型实现Debug特性。 以下是运行此程序的内容:
因此,切片时需要非常小心。 溢出会导致恐慌,导致程序崩溃。 通过实现一个名为Index的特定特征,也可以使自己的类型可索引或可切片,但稍后我们会这样做。
总结及任务
以下是关于数组和切片要记住的内容:数组具有固定大小,并且在编译时需要知道大小。 类型是[T; n],其中T是数组中值的类型,n是数组的大小:
- 切片是现有东西的视图,它们的大小更加动态。 类型是&[T]
- 要将序列传递给函数,请使用切片
- 索引特征可用于使您自己的类型可索引或可切片
以下是您应该自己尝试的一些任务:
1.制作10个元素的固定数字数组。
2.获取包含除第一个和最后一个之外的前一个数组的所有元素的切片。
3.在xs中使用x(在第1章“让你的脚变湿”中简要说明),将数组和切片中的所有数字相加。 打印数字。
通用类型
想象一下,您需要将某些值封装在其他内容中。 向量,HashMaps,Ropes,各种树和图形…可能有用的数据结构的数量是无穷无尽的,您可能希望在其中放入的可能类型的值的数量也是如此。 此外,一种有用的编程技术是将您的类型封装在其他类型中以增强其语义值,这最多可以提高代码的清晰度和安全性。
现在,假设您需要为这种类型实现一个方法,例如从HashMap中获取特定的键。 HashMaps具有指向值的键。 天真地,您需要为程序中需要的每个键值类型对编写特定方法,即使所有这些方法可能都相同。 这就是泛型类型更方便:它们允许您参数化您正在封装的类型,从而导致代码重复和源代码维护的显着减少。
制作自己的泛型类型有两种方法:枚举和结构。 用法类似于我们之前的用法,但现在,我们将声明包含在泛型类型中。 类型可以是括在尖括号中的任何大写字母。 默认情况下,在没有理由另行指定时使用字母T. 以下是标准库中Option和Result枚举的示例,它们是通用的:
enum Option<T> {
Some(T),
None
} e
num Result<T, E> {
Ok(T),
Err(E)
}
当您需要可选的空值,或者您有可能成功或可能不成功的操作时,这些是您将使用的类型。 他们有几个围绕这些概念的操作,例如Option类型方法:
- 如果Option类型具有值,则is_some返回true,否则返回false
- is_none与is_some的工作方式相同,但反之亦然
- expect会从Option类型中提取值,如果没有,则会发生恐慌
请参阅https://doc.rust lang.org/std/result/ 上的完整列表。 关键是这些方法都不依赖于Option中包含的实际类型; 他们只是在包装上运作。 因此,它们非常适合作为通用类型。
这是一个包含泛型参数的结构和使用它的示例:
// generic-struct.rs
#[derive(Debug)]
struct Money<T> {
amount: T,currency: String
} f
n main() {
let whole_euros: Money<u8> = Money { amount: 42, currency: "EUR".to_string() };
let floating_euros: Money<f32> = Money { amount: 24.312, currency: "EUR".to_string() };
println!("Whole euros: {:?}", whole_euros);
println!("Floating euros: {:?}", floating_euros);
}
最后,我们可以为函数定义泛型类型。 但是,完全通用的函数参数受到相当的约束,但它们可以通过特征边界得到增强,稍后我们将介绍它们。
这是一个返回两个参数中第一个的示例:
// generic-function.rs
fn select_first<T>(p1: T, _: T) -> T {
p1
} f
n main() {
let x = 1;
let y = 2;
let a = "meep";
let b = "moop";
println!("Selected first: {}", select_first(x, y));
println!("Selected first: {}", select_first(a, b));
}
由于函数仅为函数定义了单个类型T,因此需要在调用站点匹配类型。
换句话说,以下调用不会是正常的:
select_first(a, y);
这是因为T必须能够形成具体类型,并且类型不能同时是字符串切片和数字。
总结和任务
以下是本节的要点:
- 泛型类型的语法是,其中T可以是任何有效的Rust类型。
- 在使用它的每个块中,需要在使用之前声明它。 例如,在声明函数时,在参数列表之前声明。
- 标准库中的泛型类型Option用于表示可能无效的任何值。
- 泛型类型Result用于表示可能成功或可能不成功的操作。
以下是您的几项任务:
1.查看https://doc.rust lang.org/stable/std/collections/ 中记录的集合类型。
2.对您选择的任何键值类型对使用HashMap。
3.对任何键值类型对使用BTreeMap。
4.看看各种系列的新方法。 注意泛型类型的区别。
想想他们的迁移
Traits和实现
常量和静
特征和实现类似于在面向对象语言中实现这些接口的接口和类。 即使面向对象是一个非常自由的术语,可能意味着许多不同的东西,这里是典型的OO语言和Rust之间的一些关键差异:
- 尽管traits在Rust中具有一种继承形式,但实现却没有。 因此,使用组合而不是继承。
- 您可以在任何地方编写实现块,而无需访问实际类型。
特征块的语法定义了一组类型和方法。 一个非常简单的特征声明如下:
trait TraitName {
fn method(&self);
}
这个特性的实现需要为所有这些事情指定一些东西。 如果我们有一个名为MyType的类型并希望为它实现前面的特征,那么它的外观如下:
impl TraitName for MyType {
fn method(&self) {
// implementation
}
}
让我们通过查看标准库中的一些并为Money 类型实现它们来处理特征:
std :: ops :: Add trait让我们重载+运算符
std :: convert :: Into trait允许我们指定从和到任意类型的转换方法
Display trait让我们指定我们的类型如何格式化为字符串
特别是,“进入”和“显示”是您可能希望为自己的ypes实现的特征。
让我们从Add trait的实现开始。 首先,我们必须查看Add的文档,这样我们就可以了解对我们的期望。 该特征的定义在https://doc.rust-lang.org/std/ops/trait.Add.html上提供:
pub trait Add<RHS = Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
让我们逐行看一下:
pub trait Add <RHS = Self>表示该特征的泛型类型RHS需要等于Self类型。
输出表示任何实现
需要声明一个输出类型。
fn add(self,rhs:RHS) - > Self :: Output表示任何实现都需要实现一个add方法,该方法将在第一行声明的右侧参数与Self类型相同。 换句话说,+运算符周围的左侧和右侧需要是相同类型的。 最后,它说这个add方法必须返回我们在第二行声明的类型。
好吧,我们来试试吧。 这是代码:
// std-trait-impls.rs
use std::ops::Add;
#[derive(Debug)]
struct Money<T> {
amount: T,
currency: String
} i
mpl<T: Add<T, Output=T>> Add for Money<T> {
type Output = Money<T>;
fn add(self, rhs: Money<T>) -> Self::Output {
assert!(self.currency == rhs.currency);
Money { currency: rhs.currency, amount: self.amount + rhs.amount }
}
} f
n main() {
let whole_euros_1: Money<u8> = Money { amount: 42, currency: "EUR".to_string() };
let whole_euros_2: Money<u8> = Money { amount: 42, currency: "EUR".to_string() };
let summed_euros = whole_euros_1 + whole_euros_2;
println!("Summed euros: {:?}", summed_euros);
}
那太吓人了! 但不要担心; 让我们在impl区块攻击野兽:
impl<T: Add<T, Output=T> Add for Money<T>
这里有一个新概念叫做特质约束。 特质界限是为泛型赋予边界。 这让我们告诉编译器我们没有定义所有类型,只定义它们的某个子集。 这不仅仅是一个可选阶段,而是需要通过类型检查。 让我们分成几块。
impl <T:Add <T,Output = T>代码行表示我们的实现具有泛型类型T,但是我们给出了类型的附加边界:
Add for Money :这表示我们正在为类型Money 实现add特征,其中T代表的是先前在该行上声明的
T:Add:此类型必须实现add特征。 如果没有,我们就不能在其上使用+运算符<T,Output = T>:此外,Add trait的实现必须使其输入和输出类型相同
很明显,这正是我们所需要的。 不幸的是,有点复杂的是编译器无法为我们猜测这些东西,仍然保持它需要的强大和静态保证。 我们需要拼出来。
然后进入特征; 你之前已经看过这个特性的用法:trait声明了一个into方法,为我们提供了一种在类型之间进行转换的通用方法。 特征文档位于https://doc.rust lang.org/std/convert/trait.Into.html。 这是特征:
pub trait Into<T> {
fn into(self) -> T;
}
这比前一个简单一点。 当我们实现它时,我们只需要给出输出类型,然后我们可以在我们的类型上使用该方法。 这是一个将Money 转换为新的(有点傻)类型CurrencylessMoney 的实现:
// into-impl.rs
use std::convert::Into;
struct Money<T> {
amount: T,
currency: String
} #
[derive(Debug)]
struct CurrencylessMoney<T> {
amount: T
} i
mpl<T> Into<CurrencylessMoney<T>> for Money<T> {
fn into(self) -> CurrencylessMoney<T> {
CurrencylessMoney { amount: self.amount }
}
} f
n main() {
let money = Money { amount: 42, currency: "EUR".to_string() };
let currencyless_money: CurrencylessMoney<u32> = money.into();
println!("Money without currency: {:?}", currencyless_money);
}
再次,让我们看看impl线。 这类似于Add trait,除了我们不必通过任何特殊输出类型绑定泛型,因为Into没有:
impl<T> Into<CurrencylessMoney<T>> for Money<T>
第一个是泛型类型T的声明,第二个和第三个是它的用法。 如果你通过了添加特征的飞行颜色,这应该是相当容易的。
最后,我们来谈谈显示特征。 它记录在https://doc.rust lang.org/std/fmt/trait.Display.html,这是特征:
pub trait Display {
fn fmt(&self, &mut Formatter) -> Result<(), Error>;
}
没有什么花哨的,但是,再次,当我们正在为泛型类型实现时,我们将不得不再次拼出一些东西。 这是Money 类型的示例实现:
// display-trait.rs
use std::fmt::{Form
struct Money<T> {
amount: T,
currency: String
} i
mpl<T: Display> Display for Money<T> {
fn fmt(&self, f: &mut Formatter) -> Result {
write!(f, "{} {}", self.amount, self.currency)
}
} f
n main() {
let money = Money { amount: 42, currency: "EUR".to_string() };
println!("Displaying money: {}", money);
}
我们需要实现以实现显示特征的fmt方法采用Formatter,我们使用write写入!宏。 和之前一样,因为我们的Money 类型对amount字段使用泛型类型,我们需要指定它也必须满足Display特征。
让我们看看如果我们不指定绑定会发生什么,也就是说,如果我们的impl行看起来像这样:
impl<T> Display for Money<T>
这类似于说我们正在尝试为任何类型T实现此显示。但是,这并不行,因为并非所有类型都实现了我们在fmt方法中使用的内容。 编译器会告诉我们如下:
最后,我们将介绍一些关于特征的东西:一个叫做特质对象的概念。 可以为值赋予一个特征类型,这意味着该类型可以包含实现该特征的任何对象。 这是动态分派的一种形式,因为关于真实事物类型的任何决定只能在运行时进行。 以下是在单个Debug trait对象中存储两种不同类型的示例:
// trait-object.rs
use std::fmt::Debug;
#[derive(Debug)]
struct Point {
x: i8,
y: i8
} #
[derive(Debug)]
struct ThreeDimPoint {
x: i8,
y: i8,
z: i8
} f
n main() {
let point = Point { x: 1, y: 3};
let three_d_point = ThreeDimPoint { x: 3, y: 5, z: 9 };
let mut x: &Debug = &point as &Debug;
println!("1: {:?}", x);
x = &three_d_point;
println!("2: {:?}", x);
}
作业和任务
好的,现在是时候把特征包起来了,然后前往总结和练习。 以下是您需要记住的特征:
特征就像OO语言中的接口:它们允许以受控方式提供类型的附加功能我们可以通过提供impl块使类型满足特征
我们也可以为具有泛型的类型定义impl块,尽管我们需要注意类型边界
类型边界允许我们通过声明需要为类型实现什么特征来缩小我们的泛型类型
这里有一些额外的工作,你可以而且应该做的是推动特征:
1.制作自己的类型,没有泛型。 也许只是从Money 中删除泛型类型。 为它实现部分或全部运算符。 有关更多信息,请参阅https://doc.rust-lang.org/std/
OPS/ index.html的。
2.与上一个练习相同,但使您的类型具有泛型(或使用本节中的Money 类型)。
3.实现描述2D空间中的点的Point结构。
4.实现一个Square结构,该结构使用前一练习中为坐标定义的Point结构。
5.同样实现Rectangle结构。
6.制作一个特征卷,它有一个获取某个东西大小的方法。 实现方形和矩形结构的体积。
恭喜击败本章最棘手的部分! 现在,我们可以用一些较轻的主题放松一下。