Rust程序设计语言
所有权
变量与数据交互的方式
移动
let x = 5; let y = x; // 将变量 x 的整数值赋给 y let s1 = String::from("hello"); let s2 = s1;
String
由三部分组成:一个指向存放字符串内容的指针,一个长度,和一个容量。这一组数据存储在栈上。右侧的内容则放在堆上.
当我们将s1
赋值给s2
,这意味着我们从栈上拷贝了它的指针、长度和容量。我们并没有赋值指针指向的堆上数据。内存中的数据表现如下
当s2
和s1
离开作用域,它们都会尝试释放相同的内存,这是一个叫做二次释放(double free)的错误。赋值s2
时,尝试拷贝被分配的内存,Rust 则认为s1
不再有效,因此 Rust 不需要在s1
离开作用域后清理任何东西。
克隆
当我们需要深度赋值String
中堆上的数据,而不仅仅是栈上的数据,可以使用clone
函数。
let s1 = String::from("hello"); let s2 = s1.clone();
- 只在栈上的数据:拷贝
let x = 5; let y = x;
这段代码没有调用clone
,不过x
依然没有移动到y
中。原因时像整形这样的在编译时已知大小的类型被整个存储在栈上,拷贝其实际值是快速的。
Rust有一个叫做
Copy
trait 的特殊注解,可以用在类似整型这样的存储在栈上的类型上,如果一个类型拥有Copy
trait,一个旧的变量在将其赋值给其他变量后仍然可用
所有权与函数
将值传递给函数在语义上与给变量赋值相似。向函数传递值可能会移动或者复制,就像赋值语句一样。
fn main() { let s = String::from("hello"); // s 进入作用域 takes_ownership(s); // s 的值移动到函数里 ... // ... 所以到这里不再有效 let x = 5; // x 进入作用域 makes_copy(x); // x 应该移动函数里, // 但 i32 是 Copy 的,所以在后面可继续使用 x } // 这里, x 先移出了作用域,然后是 s。但因为 s 的值已被移走, // 所以不会有特殊操作 fn takes_ownership(some_string: String) { // some_string 进入作用域 println!("{}", some_string); } // 这里,some_string 移出作用域并调用 `drop` 方法。占用的内存被释放 fn makes_copy(some_integer: i32) { // some_integer 进入作用域 println!("{}", some_integer); } // 这里,some_integer 移出作用域。不会有特殊操作
引用与借用
为了在调用takes_ownership
后仍能使用s
,它以一个对象的引用作为参数而不是获取值的所有权:
fn main() { let s = String::from("hello"); // s 进入作用域 takes_ownership(&s); println!("s: {}", s); } fn takes_ownership(some_string: &String) { // some_string 进入作用域 println!("{}", some_string); } // 这里,s 离开了作用域。但因为它并不拥有引用值的所有权, // 所以什么也不会发生
&s
语法让我们创建一个指向值s
的引用,但是并不拥有它。因为并不拥有这个值,当引用离开作用域时其指向的值也不会被丢弃。
正如变量默认是不可变的,引用也一样。(默认)不允许修改引用的值。
- 可变引用
fn main() { let mut s = String::from("hello"); change(&mut s); } fn change(some_string: &mut String) { some_string.push_str(", world"); }
首先,必须将s
改为mut
。然后必须创建一个可变引用&mut s
和接受一个可变引用some_string: &mut String
。不过可变引用有一个很大的限制:在特定作用域中的特定数据有且只有一个可变引用。这些代码会失败:
let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; println!("{}, {}", r1, r2);
这个限制的好处是 Rust 可以在编译时就避免数据竞争。数据竞争(data race)类似于竞态条件,它可由这三个行为造成:
- 两个或更多指针同时访问同一数据。
- 至少有一个指针被用来写入数据。
- 没有同步数据访问的机制。
我们可以使用大括号来创建一个新的作用域,以允许拥有多个可变引用,只是不能同时拥有。类似的规则也存在于同时使用可变与不可变引用中。这些代码会导致一个错误:
let mut s = String::from("hello"); let r1 = &s; // 没问题 let r2 = &s; // 没问题 let r3 = &mut s; // 大问题 println!("{}, {}, and {}", r1, r2, r3);
我们不能在拥有不可变引用的同时拥有可变引用。不可变引用的用户可不希望在他们的眼皮底下值就被意外的改变了!然而,多个不可变引用是可以的,因为没有哪个只能读取数据的人有能力影响其他人读取到的数据。
注意一个引用的作用域从声明的地方开始一直持续到最后一次使用为止。例如,因为最后一次使用不可变引用在声明可变引用之前,所以是可以编译的。
总结引用的规则:
- 在任意给定时间,要么 只能有一个可变引用,要么 只能有多个不可变引用。
- 引用必须总是有效。
Slice 类型
另一个没有所有权的数据类型是slice
。slice
允许你引用集合中一段连续的元素序列,而不用引用整个集合。
字符串slice
字符串slice(string slice)是String
中一部分值的引用。
let s = String::from("hello world"); let len = s.len(); // 11 let hello = &s[0..5]; // hello let hello = &s[..5]; // hello let world = &s[6..11]; // world let world = &s[6..=10]; // world let world = &s[6..]; // world
fn main() { let s = String::from("hello world"); println!("first word: {}", first_world(&s[..])); } fn first_world(s: &str) -> &str { let bytes = s.as_bytes(); for (i, &item) in bytes.iter().enumerate() { if item == b' ' { return &s[..i]; } } &s[..] }
结构体
struct,或者 structure,是一个自定义数据类型,允许你命名和包装多个相关的值,从而形成一个有意义的组合。。在本章中,我们会对比元组与结构体的异同,演示结构体的用法,并讨论如何在结构体上定义方法和关联函数来指定与结构体数据相关的行为。
定义结构体
struct User { username: String, email: String, sign_in_count: u64, active: bool, } fn main() { let mut user1 = User{ // Rust并不允许某个字段标记为可变 email: String::from("[email protected]"), username: String::from("someusername123"), active: true, sign_in_count: 1, }; user1.email = String::from("[email protected]"); // 获取结构体中某个字段的值可以使用点号 let user2 = User{ email: String::from("[email protected]"), username: String::from("anotherusername567"), ..user1 // active: user1.active, // sign_in_count: user1.sign_in_count, }; } fn build_user(email: String, username: String) -> User { User{ email, // email: email username, // 变量与字段同名时的字段初始化简写语法 active: true, sign_in_count: 1, } }
使用没有命名字段的元组结构体来创建不同的类型
元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了。
struct Color(i32, i32, i32); struct Point(i32, i32, i32); let black = Color(0, 0, 0); let origin = Point(0, 0, 0);
没有任何字段的类单元结构体
我们也可以定义一个没有任何字段的结构体!它们被称为类单元结构体(unit-like structs)因为它们类似于 (),即unit
类型。类单元结构体常常在你想要在某个类型上实现trait
但不需要在类型中存储数据的时候发挥作用。
结构体数据的所有权
在定义结构体User
时我们使用了自身拥有所有权的String
类型而不是&str
字符串slice
类型。因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也是有效的。
可以使结构体存储被其他对象拥有的数据的引用,不过这么做的话需要用上生命周期(lifetimes)。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中存储一个引用而不指定生命周期将是无效的。
一个使用结构体的示例程序
为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50 }; println!("rect1 is {:#?}", rect1); println!( "The area of the rectangle is {} square pixels.", area(&rect1) ); } fn area(rectangle: &Rectangle) -> u32 { rectangle.width * rectangle.height }
方法语法
方法与函数类似:他们使用fn
关键字声明,可以拥有参数和返回值。不过方法与函数不同的是它们在结构体的上下文中被定义,并且它们第一个参数总是self
,它代表调用该方法的结构体实例。
定义方法
我们改写函数area
,使它成为定义于Rectangle
结构体上的方法:
impl Rectangle { fn area(&self) -> u32 { self.width * self.height } }
使用结构体名和::
语法来调用这个关联函数:比如let sq = Rectangle::square(3);
。这个方法位于结构体的命名空间中:::
语法用于关联函数和模块创建的命名空间。
关联函数
impl
块的另一个有用的功能是:允许在impl
块中定义不以self
作为参数的函数。这被称为关联函数(associated functions),因为它们与结构体相关联。它们仍是函数而不是方法,因为它们并不作用于一个结构体的实例。你已经使用过String::from
关联函数了。
关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以提供一个关联函数,它接受一个维度参数并且同时作为宽和高,这样可以更轻松的创建一个正方形Rectangle
而不必指定两次同样的值:
枚举和模式匹配
本章介绍枚举(enumerations),也被称作enums
。枚举允许你通过列举可能的值来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做Option
,它代表一个值要么是某个值要么什么都不是。然后会讲到在match
表达式中用模式匹配,针对不同的枚举值编写相应要执行的代码。最后会介绍if let
s,另一个简洁方便处理代码中枚举的结构。
定义枚举
假设我们要处理IP地址,任何一个IP地址要么时IPv4的要么时IPv6的,枚举数据结构非常适合这个场景。可以通过定义一个IpAddrKind
枚举来表示这个概念:
enum IpAddrKind { V4, V6, } struct IpAddr { // 将 kind 和 address 打包在一起,现在枚举成员就与值相关联了 kind: IpAddrKind, address: String, } let home = IpAddr { kind: IpAddrKind::V4, address: String::from("127.0.0.1"), }
我们可以使用一种更简洁的方式来表达相同的概念:
enum IpAddr { V4(String), // 仅使用枚举并将数据直接放进每一个枚举成员而不是将枚举作为结构体的一部分 V6(String), } let home = IpAddr::V4(String::from("127.0.0.1"));
用枚举代替结构体还有另一个优势:每个成员可以处理不同类型和数量的数据。IPv4版本的IP地址总是含有四个值在 0 和 255 之间的数字部分。如果我们想要将V4地址存储为四个u8
值而 V6 地址仍然表现为一个String
,这就不能使用结构体了。枚举则可以轻易处理的这个情况:
enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1"));
Option 枚举和其相对于空值的优势
Option
是标准库定义的另一个枚举,它的应用场景十分普遍,即一个值要么有值要么没值。
Rust 并没有很多其他语言中有的空值功能。空值(Null )是一个值,它代表没有值。在有空值的语言中,变量总是这两种状态之一:空值和非空值。
空值的问题在于当你尝试像一个非空值那样使用一个空值,会出现某种形式的错误。因为空和非空的属性无处不在,非常容易出现这类错误。然而,空值尝试表达的概念仍然是有意义的:空值是一个因为某种原因目前无效或缺失的值。
为此,Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option
enum Option<T> { Some(T), None, }
Option<T>
枚举是如此有用以至于它甚至被包含在了 prelude 之中,你不需要将其显式引入作用域。另外,它的成员也是如此,可以不需要Option::
前缀来直接使用Some
和None
。即便如此Option<T>
也仍是常规的枚举,Some(T)
和None
仍是Option<T>
的成员。
let some_number = Some(5); let some_string = Some("a string"); let absent_number: Option<i32> = None; // 使用 None 无法推断 Option<T> 中 T 的类型,需要指定 T 的类型
Option<T>
相对与空值:Rust编译器不允许像一个有效值那样使用Option<T>
。例如:
let x: i8 = 5; let y: Option<i8> = Some(5); let sum = x + y; // 尝试将 Option<i8> 与 i8 相加,无法通过编译
Rust 不知道该如何将Option<i8>
与i8
相加,因为它们的类型不同。在对Option<T>
进行T
的运算之前必须将其转换为T
。通常这能帮助我们捕获到空值最常见的问题之一:假设某值不为空但实际上为空的情况。
不再担心会错误的假设一个非空值,会让你对代码更加有信心。为了拥有一个可能为空的值,你必须要显式的将其放入对应类型的Option<T>
中。接着,当使用这个值时,必须明确的处理值为空的情况。只要一个值不是Option<T>
类型,你就 可以 安全的认定它的值不为空。
为了使用Option<T>
值,需要编写处理每个成员的代码。你想要一些代码只当拥有ome(T)
值时运行,允许这些代码使用其中的T
。也希望一些代码在值为None
时运行,这些代码并没有一个可用的T
值。match
表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。
match 控制流运算符
Rust 有一个叫做match
的极为强大的控制流运算符,它允许我们将一个值与一系列的模式相比较并根据相匹配的模式执行相应代码,模式可由字面值、变量、通配符和许多其他内容构成。
我们可以编写一个函数来获取一个未知的(美帝)硬币,并以一种类似验钞机的方式,确定它是何种硬币并返回它的美分值: