所有权:
变量具有唯一所有权。如果一个类型拥有 Copy
trait,一个旧的变量在将其赋值给其他变量后仍然可用。除此之外,赋值意味着转移所有权。Rust 不允许自身或其任何部分实现了 Drop
trait 的类型使用 Copy
trait。
如下是一些 Copy
的类型:
- 所有整数类型,比如
u32
。 - 布尔类型,
bool
,它的值是true
和false
。 - 所有浮点数类型,比如
f64
。 - 字符类型,
char
。 - 元组,当且仅当其包含的类型也都是
Copy
的时候。比如,(i32, i32)
是Copy
的,但(i32, String)
就不是。
引用:
引用指的是获取对象或变量的内容而不获取所有权。也意味着不能修改被引用的值。可以存在多个引用。
let s1 = Stri与使用&
引用相反的操作是 解引用(dereferencing),它使用解引用运算符,*
ng::from("hello"); let len = calculate_length(&s1); //引用
与使用 &
引用相反的操作是 解引用(dereferencing),它使用解引用运算符 *
可变引用:
为了对引用的对象修改,可以在特定作用域中对特定数据仅存在唯一可变引用。
let mut s = String::from("hello"); let r1 = &mut s; let r2 = &mut s; //Error
但,需要注意的是,不能在拥有不可变引用的同时拥有可变引用。即,要么引用,要么可变引用。
let mut s = String::from("hello"); let r1 = &s; // no problem let r2 = &s; // no problem let r3 = &mut s; // Error, BIG PROBLEM
Slice:
数组是同一类型的对象的集合 T
, 存储在连续内存中。 用方括号 []
创建数组, 以及它们的大小在编译的时候判定,是它们的类型签名的一部分 [T; size]
切片和数组相似,但它们的大小在编译时是不知道的. 相反,切片是一个双字对象,第一个字是一个指针中的数据,第二个字是切片的长度。切片可借用数组的截面,并具有式签名 &[T]
结构体(struct):
struct User { username: String, email: String, sign_in_count: u64, active: bool, }
字段初始化简写语法(field init shorthand):
fn build_user(email: String, username: String) -> User { User { email, username, active: true, sign_in_count: 1, } }
结构体更新语法(struct update syntax)实现从其他结构体创建实例:
let user2 = User { email: String::from("another@example.com"), username: String::from("anotherusername567"), ..user1 };
使用结构体更新语法为一个 User
实例设置新的 email
和 username
值,不过其余值来自 user1
变量中实例的字段
元组结构体(tuple structs)
元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。当你想给整个元组取一个名字,并使元组成为与其他元组不同的类型时,元组结构体是很有用的,这时像常规结构体那样为每个字段命名就显得多余和形式化了。
struct Color(i32, i32, i32); struct Point(i32, i32, i32); let black = Color(0, 0, 0); let origin = Point(0, 0, 0);
因为它们是不同的元组结构体的实例。你定义的每一个结构体有其自己的类型,即使结构体中的字段有着相同的类型。例如,一个获取 Color
类型参数的函数不能接受 Point
作为参数,即便这两个类型都由三个 i32
值组成。在其他方面,元组结构体实例类似于元组:可以将其解构为单独的部分,也可以使用 .
后跟索引来访问单独的值,
类单元结构体(unit-like structs)
没有任何字段,类似于 ()
,即 unit 类型。类单元结构体常常在你想要在某个类型上实现 trait 但不需要在类型中存储数据的时候发挥作用。
结构体数据的所有权
可以使结构体存储被其他对象拥有的数据的引用,不过这么做的话需要用上 生命周期(lifetimes)。
struct 类型友好打印:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50 }; println!("rect1 is {:#?}", rect1); }
方法 与函数类似:它们使用 fn
关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。不过方法与函数是不同的,因为它们在结构体的上下文中被定义(或者是枚举或 trait 对象的上下文),并且它们第一个参数总是 self
,它代表调用该方法的结构体实例。
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } //方法 impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50 }; println!( "The area of the rectangle is {} square pixels.", rect1.area() ); }
在 area
的签名中,使用 &self
来替代 rectangle: &Rectangle
,因为该方法位于 impl Rectangle
上下文中所以 Rust 知道 self
的类型是 Rectangle
。注意仍然需要在 self
前面加上 &
,就像 &Rectangle
一样。方法可以选择获取 self
的所有权,或者像我们这里一样不可变地借用 self
,或者可变地借用 self
,就跟其他参数一样。
这里选择 &self
的理由跟在函数版本中使用 &Rectangle
是相同的:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要在方法中改变调用方法的实例,需要将第一个参数改为 &mut self
。通过仅仅使用 self
作为第一个参数来使方法获取实例的所有权是很少见的;这种技术通常用在当方法将 self
转换成别的实例的时候,这时我们想要防止调用者在转换之后使用原始的实例。
关联函数
impl
块的另一个有用的功能是:允许在 impl
块中定义 不 以 self
作为参数的函数。这被称为 关联函数(associated functions),因为它们与结构体相关联。它们仍是函数而不是方法,因为它们并不作用于一个结构体的实例。你已经使用过 String::from
关联函数了。
关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以提供一个关联函数,它接受一个维度参数并且同时作为宽和高,这样可以更轻松的创建一个正方形 Rectangle
而不必指定两次同样的值:
impl Rectangle { fn square(size: u32) -> Rectangle { Rectangle { width: size, height: size } } }
使用结构体名和 ::
语法来调用这个关联函数:比如 let sq = Rectangle::square(3);
。这个方法位于结构体的命名空间中:::
语法用于关联函数和模块创建的命名空间。
每个结构体都允许拥有多个 impl
块。
枚举:
举例:
enum IpAddrKind { V4, V6, }
可以像这样创建 IpAddrKind
两个不同成员的实例:
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
Reference :enum
下边的IpAddr
枚举的新定义表明了 V4
和 V6
成员都关联了 String
值:
enum IpAddr { V4(String), V6(String), } let home = IpAddr::V4(String::from("127.0.0.1")); let loopback = IpAddr::V6(String::from("::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"));
另一个示例:
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), }
这个枚举有四个含有不同类型的成员:
Quit
没有关联任何数据。Move
包含一个匿名结构体。Write
包含单独一个String
。ChangeColor
包含三个i32
结构体和枚举还有另一个相似点:就像可以使用 impl
来为结构体定义方法那样,也可以在枚举上定义方法。这是一个定义于我们 Message
枚举上的叫做 call
的方法:
impl Message { fn call(&self) { // 在这里定义方法体 } } let m = Message::Write(String::from("hello")); m.call();
方法体使用了 self
来获取调用方法的值。这个例子中,创建了一个值为 Message::Write(String::from("hello"))
的变量 m
,而且这就是当 m.call()
运行时 call
方法中的 self
的值。
Option
Option是标准库定义的另一个枚举。Rust 并没有空值,不过它确实拥有一个可以编码存在或不存在概念的枚举。这个枚举是 Option<T>
,而且它定义于标准库中,如下:
enum Option<T> { Some(T), None, }
Option<T>
枚举是如此有用以至于它甚至被包含在了 prelude 之中,你不需要将其显式引入作用域。另外,它的成员也是如此,可以不需要 Option::
前缀来直接使用 Some
和 None
。即便如此 Option<T>
也仍是常规的枚举,Some(T)
和 None
仍是 Option<T>
的成员。
<T>
语法是一个泛型类型参数。<T>
意味着 Option
枚举的 Some
成员可以包含任意类型的数据。这里是一些包含数字类型和字符串类型 Option
值的例子:
let some_number = Some(5); let some_string = Some("a string"); let absent_number: Option<i32> = None;
如果使用 None
而不是 Some
,需要告诉 Rust Option<T>
是什么类型的,因为编译器只通过 None
值无法推断出 Some
成员保存的值的类型。
当有一个 Some
值时,我们就知道存在一个值,而这个值保存在 Some
中。当有个 None
值时,在某种意义上,它跟空值具有相同的意义:并没有一个有效的值。
>Option<T>
比空值要好的原因是因为 Option<T>
和 T
(这里 T
可以是任何类型)是不同的类型,编译器不允许像一个肯定有效的值那样使用 Option<T>
。(也就是不能混用,要使用空类型,必须使用Option<T>,而非此类型的变量,永远非空,不需要做空值检查。)