From b2198ebc4fe2cd55313cf4f92682402c4009703e Mon Sep 17 00:00:00 2001 From: yang yue Date: Sat, 18 Feb 2017 18:41:16 +0800 Subject: [PATCH] wip --- docs/ch01-00-introduction.html | 2 +- docs/ch01-01-installation.html | 2 +- docs/ch01-02-hello-world.html | 2 +- docs/ch02-00-guessing-game-tutorial.html | 2 +- docs/ch03-00-common-programming-concepts.html | 2 +- docs/ch03-01-variables-and-mutability.html | 2 +- docs/ch03-02-data-types.html | 2 +- docs/ch03-03-how-functions-work.html | 2 +- docs/ch03-04-comments.html | 2 +- docs/ch03-05-control-flow.html | 2 +- docs/ch04-00-understanding-ownership.html | 2 +- docs/ch04-01-what-is-ownership.html | 2 +- docs/ch04-02-references-and-borrowing.html | 2 +- docs/ch04-03-slices.html | 10 +- docs/ch05-00-structs.html | 356 ++++++++++++++ docs/ch05-01-method-syntax.html | 260 ++++++++++ docs/ch06-00-enums.html | 138 ++++++ docs/ch06-01-defining-an-enum.html | 214 +++++++++ docs/ch06-02-match.html | 131 +++++ docs/ch06-03-if-let.html | 123 +++++ docs/index.html | 2 +- docs/print.html | 452 +++++++++++++++++- src/SUMMARY.md | 10 +- src/ch05-00-structs.md | 329 +++++++++++++ src/ch05-01-method-syntax.md | 178 +++++++ src/ch06-00-enums.md | 9 + src/ch06-01-defining-an-enum.md | 112 +++++ src/ch06-02-match.md | 1 + src/ch06-03-if-let.md | 1 + 29 files changed, 2335 insertions(+), 17 deletions(-) create mode 100644 docs/ch05-00-structs.html create mode 100644 docs/ch05-01-method-syntax.html create mode 100644 docs/ch06-00-enums.html create mode 100644 docs/ch06-01-defining-an-enum.html create mode 100644 docs/ch06-02-match.html create mode 100644 docs/ch06-03-if-let.html create mode 100644 src/ch05-00-structs.md create mode 100644 src/ch05-01-method-syntax.md create mode 100644 src/ch06-00-enums.md create mode 100644 src/ch06-01-defining-an-enum.md create mode 100644 src/ch06-02-match.md create mode 100644 src/ch06-03-if-let.md diff --git a/docs/ch01-00-introduction.html b/docs/ch01-00-introduction.html index 73c97b9..bb21ba6 100644 --- a/docs/ch01-00-introduction.html +++ b/docs/ch01-00-introduction.html @@ -47,7 +47,7 @@
diff --git a/docs/ch01-01-installation.html b/docs/ch01-01-installation.html index 7991fdb..9d90814 100644 --- a/docs/ch01-01-installation.html +++ b/docs/ch01-01-installation.html @@ -47,7 +47,7 @@
diff --git a/docs/ch01-02-hello-world.html b/docs/ch01-02-hello-world.html index 8e79f5e..fd14965 100644 --- a/docs/ch01-02-hello-world.html +++ b/docs/ch01-02-hello-world.html @@ -47,7 +47,7 @@
diff --git a/docs/ch02-00-guessing-game-tutorial.html b/docs/ch02-00-guessing-game-tutorial.html index 814bf5f..551bf29 100644 --- a/docs/ch02-00-guessing-game-tutorial.html +++ b/docs/ch02-00-guessing-game-tutorial.html @@ -47,7 +47,7 @@
diff --git a/docs/ch03-00-common-programming-concepts.html b/docs/ch03-00-common-programming-concepts.html index 93564a6..ecf7e8f 100644 --- a/docs/ch03-00-common-programming-concepts.html +++ b/docs/ch03-00-common-programming-concepts.html @@ -47,7 +47,7 @@
diff --git a/docs/ch03-01-variables-and-mutability.html b/docs/ch03-01-variables-and-mutability.html index 731c884..25651e4 100644 --- a/docs/ch03-01-variables-and-mutability.html +++ b/docs/ch03-01-variables-and-mutability.html @@ -47,7 +47,7 @@
diff --git a/docs/ch03-02-data-types.html b/docs/ch03-02-data-types.html index ecbb835..bd72965 100644 --- a/docs/ch03-02-data-types.html +++ b/docs/ch03-02-data-types.html @@ -47,7 +47,7 @@
diff --git a/docs/ch03-03-how-functions-work.html b/docs/ch03-03-how-functions-work.html index 6e0f9e1..24cebe3 100644 --- a/docs/ch03-03-how-functions-work.html +++ b/docs/ch03-03-how-functions-work.html @@ -47,7 +47,7 @@
diff --git a/docs/ch03-04-comments.html b/docs/ch03-04-comments.html index 11665a9..1d7aa4c 100644 --- a/docs/ch03-04-comments.html +++ b/docs/ch03-04-comments.html @@ -47,7 +47,7 @@
diff --git a/docs/ch03-05-control-flow.html b/docs/ch03-05-control-flow.html index 23368a5..b8390f4 100644 --- a/docs/ch03-05-control-flow.html +++ b/docs/ch03-05-control-flow.html @@ -47,7 +47,7 @@
diff --git a/docs/ch04-00-understanding-ownership.html b/docs/ch04-00-understanding-ownership.html index d836896..4bb45f1 100644 --- a/docs/ch04-00-understanding-ownership.html +++ b/docs/ch04-00-understanding-ownership.html @@ -47,7 +47,7 @@
diff --git a/docs/ch04-01-what-is-ownership.html b/docs/ch04-01-what-is-ownership.html index 4a5c37b..6964d3b 100644 --- a/docs/ch04-01-what-is-ownership.html +++ b/docs/ch04-01-what-is-ownership.html @@ -47,7 +47,7 @@
diff --git a/docs/ch04-02-references-and-borrowing.html b/docs/ch04-02-references-and-borrowing.html index 5e9eaba..d3ebc26 100644 --- a/docs/ch04-02-references-and-borrowing.html +++ b/docs/ch04-02-references-and-borrowing.html @@ -47,7 +47,7 @@ diff --git a/docs/ch05-00-structs.html b/docs/ch05-00-structs.html new file mode 100644 index 0000000..a758e2a --- /dev/null +++ b/docs/ch05-00-structs.html @@ -0,0 +1,356 @@ + + + + + Rust 程序设计语言 中文版 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+

结构体

+
+

ch05-00-structs.md +
+commit 255b44b409585e472e14c396ebc75d28f540a1ac

+
+

struct,是 structure 的缩写,是一个允许我们命名并将多个相关值包装进一个有意义的组合的自定义类型。如果你来自一个面向对象编程语言背景,struct就像对象中的数据属性(字段)。在这一章的下一部分会讲到如何在结构体上定义方法;方法是如何为结构体数据指定行为的函数。structenum(将在第六章讲到)是为了充分利用 Rust 的编译时类型检查,来在程序范围创建新类型的基本组件。

+

对结构体的一种看法是他们与元组类似,这个我们在第三章讲过了。就像元组,结构体的每一部分可以是不同类型。可以命令各部分数据所以能更清楚的知道其值是什么意思。由于有了这些名字使得结构体更灵活:不需要依赖顺序来指定或访问实例中的值。

+

为了定义结构体,通过struct关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字,他们被称作字段fields),并定义字段类型。例如,列表 5-1 展示了一个储存用户账号信息的结构体:

+
+
struct User {
+    username: String,
+    email: String,
+    sign_in_count: u64,
+    active: bool,
+}
+
+
+

Listing 5-1: A User struct definition

+
+
+

一旦定义后为了使用它,通过为每个字段指定具体值来创建这个结构体的实例。创建一个实例需要以结构体的名字开头,接着在大括号中使用key: value对的形式提供字段,其中 key 是字段的名字而 value 是需要储存在字段中的数据值。这时字段的顺序并不必要与在结构体中声明他们的顺序一致。换句话说,结构体的定义就像一个这个类型的通用模板。例如,我们可以像这样来声明一个特定的用户:

+
# struct User {
+#     username: String,
+#     email: String,
+#     sign_in_count: u64,
+#     active: bool,
+# }
+#
+let user1 = User {
+    email: String::from("someone@example.com"),
+    username: String::from("someusername123"),
+    active: true,
+    sign_in_count: 1,
+};
+
+

为了从结构体中获取某个值,可以使用点号。如果我们只想要用户的邮箱地址,可以用user1.email

+

结构体数据的所有权

+

在列表 5-1 中的User结构体的定义中,我们使用了自身拥有所有权的String类型而不是&str字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也应该是有效的。

+

可以使结构体储存被其他对象拥有的数据的引用,不过这么做的话需要用上生命周期lifetimes),这是第十章会讨论的一个 Rust 的功能。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中储存一个引用而不指定生命周期,比如这样:

+

Filename: src/main.rs

+
struct User {
+    username: &str,
+    email: &str,
+    sign_in_count: u64,
+    active: bool,
+}
+
+fn main() {
+    let user1 = User {
+        email: "someone@example.com",
+        username: "someusername123",
+        active: true,
+        sign_in_count: 1,
+    };
+}
+
+

编译器会抱怨它需要生命周期说明符:

+
error[E0106]: missing lifetime specifier
+ -->
+  |
+2 |     username: &str,
+  |               ^ expected lifetime parameter
+
+error[E0106]: missing lifetime specifier
+ -->
+  |
+3 |     email: &str,
+  |            ^ expected lifetime parameter
+
+

第十章会讲到如何修复这个问题以便在结构体中储存引用,不过现在,通过通过从像&str这样的引用切换到像String这类拥有所有权的类型来修改修改这个错误。

+

一个示例程序

+

为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。

+

使用 Cargo 来创建一个叫做 rectangles 的新二进制程序,它会获取一个长方形以像素为单位的长度和宽度并计算它的面积。列表 5-2 中是项目的 src/main.rs 文件中为此实现的一个小程序:

+
+Filename: src/main.rs +
fn main() {
+    let length1 = 50;
+    let width1 = 30;
+
+    println!(
+        "The area of the rectangle is {} square pixels.",
+        area(length1, width1)
+    );
+}
+
+fn area(length: u32, width: u32) -> u32 {
+    length * width
+}
+
+
+

Listing 5-2: Calculating the area of a rectangle specified by its length and +width in separate variables

+
+
+

尝试使用cargo run运行程序:

+
The area of the rectangle is 1500 square pixels.
+
+

使用元组重构

+

我们的小程序能正常运行;它调用area函数用长方形的每个维度来计算出面积。不过我们可以做的更好。长度和宽度是相关联的,因为他们一起才能定义一个长方形。

+

这个做法的问题突显在area的签名上:

+
fn area(length: u32, width: u32) -> u32 {
+
+

函数area本应该计算一个长方形的面积,不过函数却有两个参数。这两个参数是相关联的,不过程序自身却哪里也没有表现出这一点。将长度和宽度组合在一起将更易懂也更易处理。

+

第三章已经讨论过了一种可行的方法:元组。列表 5-3 是一个使用元组的版本:

+
+Filename: src/main.rs +
fn main() {
+    let rect1 = (50, 30);
+
+    println!(
+        "The area of the rectangle is {} square pixels.",
+        area(rect1)
+    );
+}
+
+fn area(dimensions: (u32, u32)) -> u32 {
+    dimensions.0 * dimensions.1
+}
+
+
+

Listing 5-3: Specifying the length and width of the rectangle with a tuple

+
+
+ +

在某种程度上说这样好一点了。元组帮助我们增加了一些结构,现在在调用area的时候只用传递一个参数。不过另一方面这个方法却更不明确了:元组并没有给出它元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分:

+ +
dimensions.0 * dimensions.1
+
+

在面积计算时混淆长宽并没有什么问题,不过当在屏幕上绘制长方形时就有问题了!我们将不得不记住元组索引0length1width。如果其他人要使用这些代码,他们也不得不搞清楚后再记住。容易忘记或者混淆这些值而造成错误,因为我们没有表达我们代码中数据的意义。

+

使用结构体重构:增加更多意义

+

现在引入结构体。我们可以将元组转换为一个有整体名称而且每个部分也有对应名字的数据类型,如列表 5-4 所示:

+
+Filename: src/main.rs +
struct Rectangle {
+    length: u32,
+    width: u32,
+}
+
+fn main() {
+    let rect1 = Rectangle { length: 50, width: 30 };
+
+    println!(
+        "The area of the rectangle is {} square pixels.",
+        area(&rect1)
+    );
+}
+
+fn area(rectangle: &Rectangle) -> u32 {
+    rectangle.length * rectangle.width
+}
+
+
+

Listing 5-4: Defining a Rectangle struct

+
+
+ +

这里我们定义了一个结构体并称其为Rectangle。在{}中定义了字段lengthwidth,都是u32类型的。接着在main中,我们创建了一个长度为 50 和宽度为 30 的Rectangle的具体实例。

+

函数area现在被定义为接收一个名叫rectangle的参数,它的类型是一个结构体Rectangle实例的不可变借用。第四章讲到过,我们希望借用结构体而不是获取它的所有权这样main函数就可以保持rect1的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有&

+

area函数访问Rectanglelengthwidth字段。area的签名现在明确的表明了我们的意图:计算一个Rectangle的面积,通过其lengthwidth字段。这表明了长度和宽度是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值01。这是明确性的胜利。

+

通过衍生 trait 增加实用功能

+

如果能够在调试程序时打印出Rectangle实例来查看其所有字段的值就更好了。列表 5-5 尝试像往常一样使用println!宏:

+
+Filename: src/main.rs +
struct Rectangle {
+    length: u32,
+    width: u32,
+}
+
+fn main() {
+    let rect1 = Rectangle { length: 50, width: 30 };
+
+    println!("rect1 is {}", rect1);
+}
+
+
+

Listing 5-5: Attempting to print a Rectangle instance

+
+
+

如果运行代码,会出现带有如下核心信息的错误:

+
error[E0277]: the trait bound `Rectangle: std::fmt::Display` is not satisfied
+
+

println!宏能处理很多类型的格式,不过,{},默认告诉println!使用称为Display的格式:直接提供给终端用户查看的输出。目前为止见过的基本类型都默认实现了Display,所以它就是向用户展示1或其他任何基本类型的唯一方式。不过对于结构体,println!应该用来输出的格式是不明确的,因为这有更多显示的可能性:是否需要逗号?需要打印出结构体的{}吗?所有字段都应该显示吗?因为这种不确定性,Rust 不尝试猜测我们的意图所以结构体并没有提供一个Display的实现。

+

但是如果我们继续阅读错误,将会发现这个有帮助的信息:

+
note: `Rectangle` cannot be formatted with the default formatter; try using
+`:?` instead if you are using a format string
+
+

让我们来试试!现在println!看起来像println!("rect1 is {:?}", rect1);这样。在{}中加入:?指示符告诉println!我们想要使用叫做Debug的输出格式。Debug是一个 trait,它允许我们在调试代码时以一种对开发者有帮助的方式打印出结构体。

+

让我们试试运行这个变化...见鬼了。仍然能看到一个错误:

+
error: the trait bound `Rectangle: std::fmt::Debug` is not satisfied
+
+

虽然编译器又一次给出了一个有帮助的信息!

+
note: `Rectangle` cannot be formatted using `:?`; if it is defined in your
+crate, add `#[derive(Debug)]` or manually implement it
+
+

Rust 确实包含了打印出调试信息的功能,不过我们必须为结构体显式选择这个功能。为此,在结构体定义之前加上#[derive(Debug)]注解,如列表 5-6 所示:

+
+
#[derive(Debug)]
+struct Rectangle {
+    length: u32,
+    width: u32,
+}
+
+fn main() {
+    let rect1 = Rectangle { length: 50, width: 30 };
+
+    println!("rect1 is {:?}", rect1);
+}
+
+
+

Listing 5-6: Adding the annotation to derive the Debug trait and printing the +Rectangle instance using debug formatting

+
+
+

此时此刻运行程序,运行这个程序,不会有任何错误并会出现如下输出:

+
rect1 is Rectangle { length: 50, width: 30 }
+
+

好极了!这不是最漂亮的输出,不过它显示这个实例的所有字段,毫无疑问这对调试有帮助。如果想要输出再好看和易读一点,这对更大的结构体会有帮助,可以将println!的字符串中的{:?}替换为{:#?}。如果在这个例子中使用了美化的调试风格的话,输出会看起来像这样:

+
rect1 is Rectangle {
+    length: 50,
+    width: 30
+}
+
+

Rust 为我们提供了很多可以通过derive注解来使用的 trait,他们可以为我们的自定义类型增加有益的行为。这些 trait 和行为在附录 C 中列出。第十章会涉及到如何通过自定义行为来实现这些 trait,同时还有如何创建你自己的 trait。

+

我们的area函数是非常明确的————它只是计算了长方形的面积。如果这个行为与Rectangle结构体再结合得更紧密一些就更好了,因为这明显就是Rectangle类型的行为。现在让我们看看如何继续重构这些代码,来将area函数协调进Rectangle类型定义的area方法中。

+ +
+ + + + + + + + + + +
+ + + + + + + + + +
+ + + + + + + + + + + + + + diff --git a/docs/ch05-01-method-syntax.html b/docs/ch05-01-method-syntax.html new file mode 100644 index 0000000..e4694cb --- /dev/null +++ b/docs/ch05-01-method-syntax.html @@ -0,0 +1,260 @@ + + + + + Rust 程序设计语言 中文版 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+

方法语法

+
+

ch05-01-method-syntax.md +
+commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

+
+

方法与函数类似:他们使用fn关键和名字声明,他们可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与方法是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十三章讲解)的上下文中被定义,并且他们第一个参数总是self,它代表方法被调用的结构体的实例。

+

定义方法

+

让我们将获取一个Rectangle实例作为参数的area函数改写成一个定义于Rectangle结构体上的area方法,如列表 5-7 所示:

+
+Filename: src/main.rs +
#[derive(Debug)]
+struct Rectangle {
+    length: u32,
+    width: u32,
+}
+
+impl Rectangle {
+    fn area(&self) -> u32 {
+        self.length * self.width
+    }
+}
+
+fn main() {
+    let rect1 = Rectangle { length: 50, width: 30 };
+
+    println!(
+        "The area of the rectangle is {} square pixels.",
+        rect1.area()
+    );
+}
+
+
+

Listing 5-7: Defining an area method on the Rectangle struct

+
+
+ +

为了使函数定义于Rectangle的上下文中,我们开始了一个impl块(implimplementation 的缩写)。接着将函数移动到impl大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成self。然后在main中将我们调用area方法并传递rect1作为参数的地方,改成使用方法语法Rectangle实例上调用area方法。方法语法获取一个实例并加上一个点号后跟方法名、括号以及任何参数。

+

area的签名中,开始使用&self来替代rectangle: &Rectangle,因为该方法位于impl Rectangle 上下文中所以 Rust 知道self的类型是Rectangle。注意仍然需要在self前面加上&,就像&Rectangle一样。方法可以选择获取self的所有权,像我们这里一样不可变的借用self,或者可变的借用self,就跟其他别的参数一样。

+

这里选择&self跟在函数版本中使用&Rectangle出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要能够在方法中改变调用方法的实例的话,需要将抵押给参数改为&mut self。通过仅仅使用self作为第一个参数来使方法获取实例的所有权,不过这是很少见的;这通常用在当方法将self转换成别的实例的时候,同时我们想要防止调用者在转换之后使用原始的实例。

+

使用方法而不是函数,除了使用了方法语法和不需要在每个函数签名中重复self类型外,其主要好处在于组织性。我将某个类型实例能做的所有事情都一起放入impl块中,而不是让将来的用户在我们的代码中到处寻找`Rectangle的功能。

+ +
+

->运算符到哪去了?

+

像在 C++ 这样的语言中,又两个不同的运算符来调用方法:.直接在对象上调用方法,而->在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果object是一个指针,那么object->something()就像(*object).something()一样。

+

Rust 并没有一个与->等效的运算符;相反,Rust 有一个叫自动引用和解引用automatic referencing and dereferencing)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。

+

这是它如何工作的:当使用object.something()调用方法时,Rust 会自动增加&&mut*以便使object符合方法的签名。也就是说,这些代码是等同的:

+
# #[derive(Debug,Copy,Clone)]
+# struct Point {
+#     x: f64,
+#     y: f64,
+# }
+#
+# impl Point {
+#    fn distance(&self, other: &Point) -> f64 {
+#        let x_squared = f64::powi(other.x - self.x, 2);
+#        let y_squared = f64::powi(other.y - self.y, 2);
+#
+#        f64::sqrt(x_squared + y_squared)
+#    }
+# }
+# let p1 = Point { x: 0.0, y: 0.0 };
+# let p2 = Point { x: 5.0, y: 6.5 };
+p1.distance(&p2);
+(&p1).distance(&p2);
+
+

第一行看起来简洁的多。这种自动引用的行为之所以能行得通是因为方法有一个明确的接收者————self的类型。在给出接收者和方法名的前提下,Rust 可以明确的计算出方法是仅仅读取(所以需要&self),做出修改(所以是&mut self)或者是获取所有权(所以是self)。Rust 这种使得借用对方法接收者来说是隐式的做法是其所有权系统人体工程学实践的一大部分。

+
+ +

带有更多参数的方法

+

让我们更多的实践一下方法,通过为Rectangle结构体实现第二个方法。这回,我们让一个Rectangle的实例获取另一个Rectangle实例并返回self能否完全包含第二个长方形,如果能返回true若不能则返回false。当我们定义了can_hold方法,就可以运行列表 5-8 中的代码了:

+
+Filename: src/main.rs +
fn main() {
+    let rect1 = Rectangle { length: 50, width: 30 };
+    let rect2 = Rectangle { length: 40, width: 10 };
+    let rect3 = Rectangle { length: 45, width: 60 };
+
+    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
+    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
+}
+
+
+

Listing 5-8: Demonstration of using the as-yet-unwritten can_hold method

+
+
+

我们希望看到如下输出,因为rect2的长宽都小于rect1,而rect3rect1要宽:

+
Can rect1 hold rect2? true
+Can rect1 hold rect3? false
+
+

因为我们想定义一个方法,所以它应该位于impl Rectangle块中。方法名是can_hold,并且它会获取另一个Rectangle的不可变借用作为参数。通过观察调用点可以看出参数是什么类型的:rect1.can_hold(&rect2)传入了&rect2,它是一个Rectangle的实例rect2的不可变借用。这是可以理解的,因为我们只需要读取rect2(而不是写入,这意味着我们需要一个可变借用)而且希望main保持rect2的所有权这样就可以在调用这个方法后继续使用它。can_hold的返回值是一个布尔值,其实现会分别检查self的长宽是够都大于另一个Rectangle。让我们在列表 5-7 的impl块中增加这个新方法:

+

Filename: src/main.rs

+
# #[derive(Debug)]
+# struct Rectangle {
+#     length: u32,
+#     width: u32,
+# }
+#
+impl Rectangle {
+    fn area(&self) -> u32 {
+        self.length * self.width
+    }
+
+    fn can_hold(&self, other: &Rectangle) -> bool {
+        self.length > other.length && self.width > other.width
+    }
+}
+
+ +

如果结合列表 5-8 的main函数来运行,就会看到想要得到的输出!方法可以在self后增加多个参数,而且这些参数就像函数中的参数一样工作。

+

关联函数

+

impl块的另一个好用的功能是:允许在impl块中定义self作为参数的函数。这被称为关联函数associated functions),因为他们与结构体相关联。即便如此他们也是函数而不是方法,因为他们并不作用于一个结构体的实例。你已经使用过一个关联函数了:String::from

+

关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以一个关联函数,它获取一个维度参数并且用来作为长宽,这样可以更轻松的创建一个正方形Rectangle而不必指定两次同样的值:

+

Filename: src/main.rs

+
# #[derive(Debug)]
+# struct Rectangle {
+#     length: u32,
+#     width: u32,
+# }
+#
+impl Rectangle {
+    fn square(size: u32) -> Rectangle {
+        Rectangle { length: size, width: size }
+    }
+}
+
+

使用结构体名和::语法来调用这个关联函数:比如let sq = Rectangle::square(3);。这个方法位于结构体的命名空间中:::语法用于关联函数和模块创建的命名空间,第七章会讲到后者。

+

总结

+

结构体让我们可以在自己的范围内创建有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名他们来使得代码更清晰。方法允许为结构体实例指定行为,而关联函数将特定功能置于结构体的命名空间中并且无需一个实例。

+

结构体并不是创建自定义类型的唯一方法;让我们转向 Rust 的enum功能并为自己的工具箱再填一个工具。

+ +
+ + + + + + + + + + +
+ + + + + + + + + +
+ + + + + + + + + + + + + + diff --git a/docs/ch06-00-enums.html b/docs/ch06-00-enums.html new file mode 100644 index 0000000..edbf4e2 --- /dev/null +++ b/docs/ch06-00-enums.html @@ -0,0 +1,138 @@ + + + + + Rust 程序设计语言 中文版 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+

枚举和模式匹配

+
+

ch06-00-enums.md +
+commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d

+
+

本章介绍枚举,也被称作 enums。枚举允许你通过列举可能的值来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做Option,它代表一个值要么是一些值要么什么都不是。然后会讲到match表达式中的模式匹配如何使对枚举不同的值运行不同的代码变得容易。最后会涉及到if let,另一个简洁方便处理代码中枚举的结构。

+

枚举是一个很多语言都有的功能,不过不同语言中的功能各不相同。Rust 的枚举与像F#、OCaml 和 Haskell这样的函数式编程语言中的代数数据类型algebraic data types)最为相似。

+ +
+ + + + + + + + + + +
+ + + + + + + + + +
+ + + + + + + + + + + + + + diff --git a/docs/ch06-01-defining-an-enum.html b/docs/ch06-01-defining-an-enum.html new file mode 100644 index 0000000..543f26d --- /dev/null +++ b/docs/ch06-01-defining-an-enum.html @@ -0,0 +1,214 @@ + + + + + Rust 程序设计语言 中文版 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+

定义枚举

+
+

ch06-01-defining-an-enum.md +
+commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d

+
+

让我们通过一用代码来表现的场景,来看看为什么这里枚举是有用的而且比结构体更合适。比如我们要处理 IP 地。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序只可能会遇到两种 IP 地址:我们可以枚举出所有可能的值,这也正是它名字的由来。

+

任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的而不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值尽可能是其一个成员。IPv4 和 IPv6 从根本上讲都是 IP 地址,所以当代码在处理申请任何类型的 IP 地址的场景时应该把他们当作相同的类型。

+

可以通过在代码中定义一个IpAddrKind枚举来表现这个概念并列出可能的 IP 地址类型,V4V6。这被称为枚举的成员variants):

+
enum IpAddrKind {
+    V4,
+    V6,
+}
+
+

现在IpAddrKind就是一个可以在代码中使用的自定义类型了。

+

枚举值

+

可以像这样创建IpAddrKind两个不同成员的实例:

+
# enum IpAddrKind {
+#     V4,
+#     V6,
+# }
+#
+let four = IpAddrKind::V4;
+let six = IpAddrKind::V6;
+
+

注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在IpAddrKind::V4IpAddrKind::V6是相同类型的:IpAddrKind。例如,接着我们可以顶一个函数来获取IpAddrKind

+
# enum IpAddrKind {
+#     V4,
+#     V6,
+# }
+#
+fn route(ip_type: IpAddrKind) { }
+
+

现在可以使用任意成员来调用这个函数:

+
# enum IpAddrKind {
+#     V4,
+#     V6,
+# }
+#
+# fn route(ip_type: IpAddrKind) { }
+#
+route(IpAddrKind::V4);
+route(IpAddrKind::V6);
+
+

使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个储存实际 IP 地址数据的方法;只知道它是什么类型的。考虑到已经在第五章学习过结构体了,你可以想如列表 6-1 那样修改这个问题:

+
+
enum IpAddrKind {
+    V4,
+    V6,
+}
+
+struct IpAddr {
+    kind: IpAddrKind,
+    address: String,
+}
+
+let home = IpAddr {
+    kind: IpAddrKind::V4,
+    address: String::from("127.0.0.1"),
+};
+
+let loopback = IpAddr {
+    kind: IpAddrKind::V6,
+    address: String::from("::1"),
+};
+
+
+

Listing 6-1: Storing the data and IpAddrKind variant of an IP address using a +struct

+
+
+

这里我们定义了一个有两个字段的结构体IpAddrkind字段是IpAddrKind(之前定义的枚举)类型的而address字段是String类型的。这里有两个结构体的实例。第一个,home,它的kind的值是IpAddrKind::V4与之相关联的地址数据是127.0.0.1。第二个实例,loopbackkind的值是IpAddrKind的另一个成员,V6,关联的地址是::1。我们使用了要给结构体来将kindaddress打包在一起,现在枚举成员就与值相关联了。

+

我们可以使用一种更简洁的方式来表达相同的概念,仅仅使用枚举并将数据直接放进每一个枚举成员而不是将枚举作为结构体的一部分。IpAddr枚举的新定义表明了V4V6成员都关联了String值:

+
enum IpAddr {
+    V4(String),
+    V6(String),
+}
+
+let home = IpAddr::V4(String::from("127.0.0.1"));
+
+let loopback = IpAddr::V6(String::from("::1"));
+
+

我们直接将数据附加到枚举的每个成员上,这样就不需要一个额外的结构体了。

+

使用枚举而不是结构体还有另外一个优势:每个成员可以处理不同类型和数量的数据。

+ +
+ + + + + + + + + + +
+ + + + + + + + + +
+ + + + + + + + + + + + + + diff --git a/docs/ch06-02-match.html b/docs/ch06-02-match.html new file mode 100644 index 0000000..cefe6a2 --- /dev/null +++ b/docs/ch06-02-match.html @@ -0,0 +1,131 @@ + + + + + Rust 程序设计语言 中文版 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+

The match Control Flow Operator

+ +
+ + + + + + + + + + +
+ + + + + + + + + +
+ + + + + + + + + + + + + + diff --git a/docs/ch06-03-if-let.html b/docs/ch06-03-if-let.html new file mode 100644 index 0000000..0b683ae --- /dev/null +++ b/docs/ch06-03-if-let.html @@ -0,0 +1,123 @@ + + + + + Rust 程序设计语言 中文版 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+

Concise Control Flow with if let

+ +
+ + + + + + + + +
+ + + + + + + +
+ + + + + + + + + + + + + + diff --git a/docs/index.html b/docs/index.html index a872f59..29ca5d8 100644 --- a/docs/index.html +++ b/docs/index.html @@ -46,7 +46,7 @@
diff --git a/docs/print.html b/docs/print.html index 4a29c86..b0b4b2f 100644 --- a/docs/print.html +++ b/docs/print.html @@ -47,7 +47,7 @@
@@ -2294,6 +2294,456 @@ let slice = &a[1..3];

总结

所有权、借用和 slice 这些概念是 Rust 何以在编译时保障内存安全的关键所在。Rust 像其他系统编程语言那样给予你对内存使用的控制,但拥有数据所有者在离开作用域后自动清除其数据的功能意味着你无须额外编写和调试相关的控制代码。

所有权系统影响了 Rust 中其他很多部分如何工作,所以我们会继续讲到这些概念,贯穿本书的余下内容。让我们开始下一个章节,来看看如何将多份数据组合进一个struct中。

+

结构体

+
+

ch05-00-structs.md +
+commit 255b44b409585e472e14c396ebc75d28f540a1ac

+
+

struct,是 structure 的缩写,是一个允许我们命名并将多个相关值包装进一个有意义的组合的自定义类型。如果你来自一个面向对象编程语言背景,struct就像对象中的数据属性(字段)。在这一章的下一部分会讲到如何在结构体上定义方法;方法是如何为结构体数据指定行为的函数。structenum(将在第六章讲到)是为了充分利用 Rust 的编译时类型检查,来在程序范围创建新类型的基本组件。

+

对结构体的一种看法是他们与元组类似,这个我们在第三章讲过了。就像元组,结构体的每一部分可以是不同类型。可以命令各部分数据所以能更清楚的知道其值是什么意思。由于有了这些名字使得结构体更灵活:不需要依赖顺序来指定或访问实例中的值。

+

为了定义结构体,通过struct关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字,他们被称作字段fields),并定义字段类型。例如,列表 5-1 展示了一个储存用户账号信息的结构体:

+
+
struct User {
+    username: String,
+    email: String,
+    sign_in_count: u64,
+    active: bool,
+}
+
+
+

Listing 5-1: A User struct definition

+
+
+

一旦定义后为了使用它,通过为每个字段指定具体值来创建这个结构体的实例。创建一个实例需要以结构体的名字开头,接着在大括号中使用key: value对的形式提供字段,其中 key 是字段的名字而 value 是需要储存在字段中的数据值。这时字段的顺序并不必要与在结构体中声明他们的顺序一致。换句话说,结构体的定义就像一个这个类型的通用模板。例如,我们可以像这样来声明一个特定的用户:

+
# struct User {
+#     username: String,
+#     email: String,
+#     sign_in_count: u64,
+#     active: bool,
+# }
+#
+let user1 = User {
+    email: String::from("someone@example.com"),
+    username: String::from("someusername123"),
+    active: true,
+    sign_in_count: 1,
+};
+
+

为了从结构体中获取某个值,可以使用点号。如果我们只想要用户的邮箱地址,可以用user1.email

+

结构体数据的所有权

+

在列表 5-1 中的User结构体的定义中,我们使用了自身拥有所有权的String类型而不是&str字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也应该是有效的。

+

可以使结构体储存被其他对象拥有的数据的引用,不过这么做的话需要用上生命周期lifetimes),这是第十章会讨论的一个 Rust 的功能。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中储存一个引用而不指定生命周期,比如这样:

+

Filename: src/main.rs

+
struct User {
+    username: &str,
+    email: &str,
+    sign_in_count: u64,
+    active: bool,
+}
+
+fn main() {
+    let user1 = User {
+        email: "someone@example.com",
+        username: "someusername123",
+        active: true,
+        sign_in_count: 1,
+    };
+}
+
+

编译器会抱怨它需要生命周期说明符:

+
error[E0106]: missing lifetime specifier
+ -->
+  |
+2 |     username: &str,
+  |               ^ expected lifetime parameter
+
+error[E0106]: missing lifetime specifier
+ -->
+  |
+3 |     email: &str,
+  |            ^ expected lifetime parameter
+
+

第十章会讲到如何修复这个问题以便在结构体中储存引用,不过现在,通过通过从像&str这样的引用切换到像String这类拥有所有权的类型来修改修改这个错误。

+

一个示例程序

+

为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。

+

使用 Cargo 来创建一个叫做 rectangles 的新二进制程序,它会获取一个长方形以像素为单位的长度和宽度并计算它的面积。列表 5-2 中是项目的 src/main.rs 文件中为此实现的一个小程序:

+
+Filename: src/main.rs +
fn main() {
+    let length1 = 50;
+    let width1 = 30;
+
+    println!(
+        "The area of the rectangle is {} square pixels.",
+        area(length1, width1)
+    );
+}
+
+fn area(length: u32, width: u32) -> u32 {
+    length * width
+}
+
+
+

Listing 5-2: Calculating the area of a rectangle specified by its length and +width in separate variables

+
+
+

尝试使用cargo run运行程序:

+
The area of the rectangle is 1500 square pixels.
+
+

使用元组重构

+

我们的小程序能正常运行;它调用area函数用长方形的每个维度来计算出面积。不过我们可以做的更好。长度和宽度是相关联的,因为他们一起才能定义一个长方形。

+

这个做法的问题突显在area的签名上:

+
fn area(length: u32, width: u32) -> u32 {
+
+

函数area本应该计算一个长方形的面积,不过函数却有两个参数。这两个参数是相关联的,不过程序自身却哪里也没有表现出这一点。将长度和宽度组合在一起将更易懂也更易处理。

+

第三章已经讨论过了一种可行的方法:元组。列表 5-3 是一个使用元组的版本:

+
+Filename: src/main.rs +
fn main() {
+    let rect1 = (50, 30);
+
+    println!(
+        "The area of the rectangle is {} square pixels.",
+        area(rect1)
+    );
+}
+
+fn area(dimensions: (u32, u32)) -> u32 {
+    dimensions.0 * dimensions.1
+}
+
+
+

Listing 5-3: Specifying the length and width of the rectangle with a tuple

+
+
+ +

在某种程度上说这样好一点了。元组帮助我们增加了一些结构,现在在调用area的时候只用传递一个参数。不过另一方面这个方法却更不明确了:元组并没有给出它元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分:

+ +
dimensions.0 * dimensions.1
+
+

在面积计算时混淆长宽并没有什么问题,不过当在屏幕上绘制长方形时就有问题了!我们将不得不记住元组索引0length1width。如果其他人要使用这些代码,他们也不得不搞清楚后再记住。容易忘记或者混淆这些值而造成错误,因为我们没有表达我们代码中数据的意义。

+

使用结构体重构:增加更多意义

+

现在引入结构体。我们可以将元组转换为一个有整体名称而且每个部分也有对应名字的数据类型,如列表 5-4 所示:

+
+Filename: src/main.rs +
struct Rectangle {
+    length: u32,
+    width: u32,
+}
+
+fn main() {
+    let rect1 = Rectangle { length: 50, width: 30 };
+
+    println!(
+        "The area of the rectangle is {} square pixels.",
+        area(&rect1)
+    );
+}
+
+fn area(rectangle: &Rectangle) -> u32 {
+    rectangle.length * rectangle.width
+}
+
+
+

Listing 5-4: Defining a Rectangle struct

+
+
+ +

这里我们定义了一个结构体并称其为Rectangle。在{}中定义了字段lengthwidth,都是u32类型的。接着在main中,我们创建了一个长度为 50 和宽度为 30 的Rectangle的具体实例。

+

函数area现在被定义为接收一个名叫rectangle的参数,它的类型是一个结构体Rectangle实例的不可变借用。第四章讲到过,我们希望借用结构体而不是获取它的所有权这样main函数就可以保持rect1的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有&

+

area函数访问Rectanglelengthwidth字段。area的签名现在明确的表明了我们的意图:计算一个Rectangle的面积,通过其lengthwidth字段。这表明了长度和宽度是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值01。这是明确性的胜利。

+

通过衍生 trait 增加实用功能

+

如果能够在调试程序时打印出Rectangle实例来查看其所有字段的值就更好了。列表 5-5 尝试像往常一样使用println!宏:

+
+Filename: src/main.rs +
struct Rectangle {
+    length: u32,
+    width: u32,
+}
+
+fn main() {
+    let rect1 = Rectangle { length: 50, width: 30 };
+
+    println!("rect1 is {}", rect1);
+}
+
+
+

Listing 5-5: Attempting to print a Rectangle instance

+
+
+

如果运行代码,会出现带有如下核心信息的错误:

+
error[E0277]: the trait bound `Rectangle: std::fmt::Display` is not satisfied
+
+

println!宏能处理很多类型的格式,不过,{},默认告诉println!使用称为Display的格式:直接提供给终端用户查看的输出。目前为止见过的基本类型都默认实现了Display,所以它就是向用户展示1或其他任何基本类型的唯一方式。不过对于结构体,println!应该用来输出的格式是不明确的,因为这有更多显示的可能性:是否需要逗号?需要打印出结构体的{}吗?所有字段都应该显示吗?因为这种不确定性,Rust 不尝试猜测我们的意图所以结构体并没有提供一个Display的实现。

+

但是如果我们继续阅读错误,将会发现这个有帮助的信息:

+
note: `Rectangle` cannot be formatted with the default formatter; try using
+`:?` instead if you are using a format string
+
+

让我们来试试!现在println!看起来像println!("rect1 is {:?}", rect1);这样。在{}中加入:?指示符告诉println!我们想要使用叫做Debug的输出格式。Debug是一个 trait,它允许我们在调试代码时以一种对开发者有帮助的方式打印出结构体。

+

让我们试试运行这个变化...见鬼了。仍然能看到一个错误:

+
error: the trait bound `Rectangle: std::fmt::Debug` is not satisfied
+
+

虽然编译器又一次给出了一个有帮助的信息!

+
note: `Rectangle` cannot be formatted using `:?`; if it is defined in your
+crate, add `#[derive(Debug)]` or manually implement it
+
+

Rust 确实包含了打印出调试信息的功能,不过我们必须为结构体显式选择这个功能。为此,在结构体定义之前加上#[derive(Debug)]注解,如列表 5-6 所示:

+
+
#[derive(Debug)]
+struct Rectangle {
+    length: u32,
+    width: u32,
+}
+
+fn main() {
+    let rect1 = Rectangle { length: 50, width: 30 };
+
+    println!("rect1 is {:?}", rect1);
+}
+
+
+

Listing 5-6: Adding the annotation to derive the Debug trait and printing the +Rectangle instance using debug formatting

+
+
+

此时此刻运行程序,运行这个程序,不会有任何错误并会出现如下输出:

+
rect1 is Rectangle { length: 50, width: 30 }
+
+

好极了!这不是最漂亮的输出,不过它显示这个实例的所有字段,毫无疑问这对调试有帮助。如果想要输出再好看和易读一点,这对更大的结构体会有帮助,可以将println!的字符串中的{:?}替换为{:#?}。如果在这个例子中使用了美化的调试风格的话,输出会看起来像这样:

+
rect1 is Rectangle {
+    length: 50,
+    width: 30
+}
+
+

Rust 为我们提供了很多可以通过derive注解来使用的 trait,他们可以为我们的自定义类型增加有益的行为。这些 trait 和行为在附录 C 中列出。第十章会涉及到如何通过自定义行为来实现这些 trait,同时还有如何创建你自己的 trait。

+

我们的area函数是非常明确的————它只是计算了长方形的面积。如果这个行为与Rectangle结构体再结合得更紧密一些就更好了,因为这明显就是Rectangle类型的行为。现在让我们看看如何继续重构这些代码,来将area函数协调进Rectangle类型定义的area方法中。

+

方法语法

+
+

ch05-01-method-syntax.md +
+commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c

+
+

方法与函数类似:他们使用fn关键和名字声明,他们可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与方法是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十三章讲解)的上下文中被定义,并且他们第一个参数总是self,它代表方法被调用的结构体的实例。

+

定义方法

+

让我们将获取一个Rectangle实例作为参数的area函数改写成一个定义于Rectangle结构体上的area方法,如列表 5-7 所示:

+
+Filename: src/main.rs +
#[derive(Debug)]
+struct Rectangle {
+    length: u32,
+    width: u32,
+}
+
+impl Rectangle {
+    fn area(&self) -> u32 {
+        self.length * self.width
+    }
+}
+
+fn main() {
+    let rect1 = Rectangle { length: 50, width: 30 };
+
+    println!(
+        "The area of the rectangle is {} square pixels.",
+        rect1.area()
+    );
+}
+
+
+

Listing 5-7: Defining an area method on the Rectangle struct

+
+
+ +

为了使函数定义于Rectangle的上下文中,我们开始了一个impl块(implimplementation 的缩写)。接着将函数移动到impl大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成self。然后在main中将我们调用area方法并传递rect1作为参数的地方,改成使用方法语法Rectangle实例上调用area方法。方法语法获取一个实例并加上一个点号后跟方法名、括号以及任何参数。

+

area的签名中,开始使用&self来替代rectangle: &Rectangle,因为该方法位于impl Rectangle 上下文中所以 Rust 知道self的类型是Rectangle。注意仍然需要在self前面加上&,就像&Rectangle一样。方法可以选择获取self的所有权,像我们这里一样不可变的借用self,或者可变的借用self,就跟其他别的参数一样。

+

这里选择&self跟在函数版本中使用&Rectangle出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要能够在方法中改变调用方法的实例的话,需要将抵押给参数改为&mut self。通过仅仅使用self作为第一个参数来使方法获取实例的所有权,不过这是很少见的;这通常用在当方法将self转换成别的实例的时候,同时我们想要防止调用者在转换之后使用原始的实例。

+

使用方法而不是函数,除了使用了方法语法和不需要在每个函数签名中重复self类型外,其主要好处在于组织性。我将某个类型实例能做的所有事情都一起放入impl块中,而不是让将来的用户在我们的代码中到处寻找`Rectangle的功能。

+ +
+

->运算符到哪去了?

+

像在 C++ 这样的语言中,又两个不同的运算符来调用方法:.直接在对象上调用方法,而->在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果object是一个指针,那么object->something()就像(*object).something()一样。

+

Rust 并没有一个与->等效的运算符;相反,Rust 有一个叫自动引用和解引用automatic referencing and dereferencing)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。

+

这是它如何工作的:当使用object.something()调用方法时,Rust 会自动增加&&mut*以便使object符合方法的签名。也就是说,这些代码是等同的:

+
# #[derive(Debug,Copy,Clone)]
+# struct Point {
+#     x: f64,
+#     y: f64,
+# }
+#
+# impl Point {
+#    fn distance(&self, other: &Point) -> f64 {
+#        let x_squared = f64::powi(other.x - self.x, 2);
+#        let y_squared = f64::powi(other.y - self.y, 2);
+#
+#        f64::sqrt(x_squared + y_squared)
+#    }
+# }
+# let p1 = Point { x: 0.0, y: 0.0 };
+# let p2 = Point { x: 5.0, y: 6.5 };
+p1.distance(&p2);
+(&p1).distance(&p2);
+
+

第一行看起来简洁的多。这种自动引用的行为之所以能行得通是因为方法有一个明确的接收者————self的类型。在给出接收者和方法名的前提下,Rust 可以明确的计算出方法是仅仅读取(所以需要&self),做出修改(所以是&mut self)或者是获取所有权(所以是self)。Rust 这种使得借用对方法接收者来说是隐式的做法是其所有权系统人体工程学实践的一大部分。

+
+ +

带有更多参数的方法

+

让我们更多的实践一下方法,通过为Rectangle结构体实现第二个方法。这回,我们让一个Rectangle的实例获取另一个Rectangle实例并返回self能否完全包含第二个长方形,如果能返回true若不能则返回false。当我们定义了can_hold方法,就可以运行列表 5-8 中的代码了:

+
+Filename: src/main.rs +
fn main() {
+    let rect1 = Rectangle { length: 50, width: 30 };
+    let rect2 = Rectangle { length: 40, width: 10 };
+    let rect3 = Rectangle { length: 45, width: 60 };
+
+    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
+    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
+}
+
+
+

Listing 5-8: Demonstration of using the as-yet-unwritten can_hold method

+
+
+

我们希望看到如下输出,因为rect2的长宽都小于rect1,而rect3rect1要宽:

+
Can rect1 hold rect2? true
+Can rect1 hold rect3? false
+
+

因为我们想定义一个方法,所以它应该位于impl Rectangle块中。方法名是can_hold,并且它会获取另一个Rectangle的不可变借用作为参数。通过观察调用点可以看出参数是什么类型的:rect1.can_hold(&rect2)传入了&rect2,它是一个Rectangle的实例rect2的不可变借用。这是可以理解的,因为我们只需要读取rect2(而不是写入,这意味着我们需要一个可变借用)而且希望main保持rect2的所有权这样就可以在调用这个方法后继续使用它。can_hold的返回值是一个布尔值,其实现会分别检查self的长宽是够都大于另一个Rectangle。让我们在列表 5-7 的impl块中增加这个新方法:

+

Filename: src/main.rs

+
# #[derive(Debug)]
+# struct Rectangle {
+#     length: u32,
+#     width: u32,
+# }
+#
+impl Rectangle {
+    fn area(&self) -> u32 {
+        self.length * self.width
+    }
+
+    fn can_hold(&self, other: &Rectangle) -> bool {
+        self.length > other.length && self.width > other.width
+    }
+}
+
+ +

如果结合列表 5-8 的main函数来运行,就会看到想要得到的输出!方法可以在self后增加多个参数,而且这些参数就像函数中的参数一样工作。

+

关联函数

+

impl块的另一个好用的功能是:允许在impl块中定义self作为参数的函数。这被称为关联函数associated functions),因为他们与结构体相关联。即便如此他们也是函数而不是方法,因为他们并不作用于一个结构体的实例。你已经使用过一个关联函数了:String::from

+

关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以一个关联函数,它获取一个维度参数并且用来作为长宽,这样可以更轻松的创建一个正方形Rectangle而不必指定两次同样的值:

+

Filename: src/main.rs

+
# #[derive(Debug)]
+# struct Rectangle {
+#     length: u32,
+#     width: u32,
+# }
+#
+impl Rectangle {
+    fn square(size: u32) -> Rectangle {
+        Rectangle { length: size, width: size }
+    }
+}
+
+

使用结构体名和::语法来调用这个关联函数:比如let sq = Rectangle::square(3);。这个方法位于结构体的命名空间中:::语法用于关联函数和模块创建的命名空间,第七章会讲到后者。

+

总结

+

结构体让我们可以在自己的范围内创建有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名他们来使得代码更清晰。方法允许为结构体实例指定行为,而关联函数将特定功能置于结构体的命名空间中并且无需一个实例。

+

结构体并不是创建自定义类型的唯一方法;让我们转向 Rust 的enum功能并为自己的工具箱再填一个工具。

+

枚举和模式匹配

+
+

ch06-00-enums.md +
+commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d

+
+

本章介绍枚举,也被称作 enums。枚举允许你通过列举可能的值来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做Option,它代表一个值要么是一些值要么什么都不是。然后会讲到match表达式中的模式匹配如何使对枚举不同的值运行不同的代码变得容易。最后会涉及到if let,另一个简洁方便处理代码中枚举的结构。

+

枚举是一个很多语言都有的功能,不过不同语言中的功能各不相同。Rust 的枚举与像F#、OCaml 和 Haskell这样的函数式编程语言中的代数数据类型algebraic data types)最为相似。

+

定义枚举

+
+

ch06-01-defining-an-enum.md +
+commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d

+
+

让我们通过一用代码来表现的场景,来看看为什么这里枚举是有用的而且比结构体更合适。比如我们要处理 IP 地。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序只可能会遇到两种 IP 地址:我们可以枚举出所有可能的值,这也正是它名字的由来。

+

任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的而不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值尽可能是其一个成员。IPv4 和 IPv6 从根本上讲都是 IP 地址,所以当代码在处理申请任何类型的 IP 地址的场景时应该把他们当作相同的类型。

+

可以通过在代码中定义一个IpAddrKind枚举来表现这个概念并列出可能的 IP 地址类型,V4V6。这被称为枚举的成员variants):

+
enum IpAddrKind {
+    V4,
+    V6,
+}
+
+

现在IpAddrKind就是一个可以在代码中使用的自定义类型了。

+

枚举值

+

可以像这样创建IpAddrKind两个不同成员的实例:

+
# enum IpAddrKind {
+#     V4,
+#     V6,
+# }
+#
+let four = IpAddrKind::V4;
+let six = IpAddrKind::V6;
+
+

注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在IpAddrKind::V4IpAddrKind::V6是相同类型的:IpAddrKind。例如,接着我们可以顶一个函数来获取IpAddrKind

+
# enum IpAddrKind {
+#     V4,
+#     V6,
+# }
+#
+fn route(ip_type: IpAddrKind) { }
+
+

现在可以使用任意成员来调用这个函数:

+
# enum IpAddrKind {
+#     V4,
+#     V6,
+# }
+#
+# fn route(ip_type: IpAddrKind) { }
+#
+route(IpAddrKind::V4);
+route(IpAddrKind::V6);
+
+

使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个储存实际 IP 地址数据的方法;只知道它是什么类型的。考虑到已经在第五章学习过结构体了,你可以想如列表 6-1 那样修改这个问题:

+
+
enum IpAddrKind {
+    V4,
+    V6,
+}
+
+struct IpAddr {
+    kind: IpAddrKind,
+    address: String,
+}
+
+let home = IpAddr {
+    kind: IpAddrKind::V4,
+    address: String::from("127.0.0.1"),
+};
+
+let loopback = IpAddr {
+    kind: IpAddrKind::V6,
+    address: String::from("::1"),
+};
+
+
+

Listing 6-1: Storing the data and IpAddrKind variant of an IP address using a +struct

+
+
+

这里我们定义了一个有两个字段的结构体IpAddrkind字段是IpAddrKind(之前定义的枚举)类型的而address字段是String类型的。这里有两个结构体的实例。第一个,home,它的kind的值是IpAddrKind::V4与之相关联的地址数据是127.0.0.1。第二个实例,loopbackkind的值是IpAddrKind的另一个成员,V6,关联的地址是::1。我们使用了要给结构体来将kindaddress打包在一起,现在枚举成员就与值相关联了。

+

我们可以使用一种更简洁的方式来表达相同的概念,仅仅使用枚举并将数据直接放进每一个枚举成员而不是将枚举作为结构体的一部分。IpAddr枚举的新定义表明了V4V6成员都关联了String值:

+
enum IpAddr {
+    V4(String),
+    V6(String),
+}
+
+let home = IpAddr::V4(String::from("127.0.0.1"));
+
+let loopback = IpAddr::V6(String::from("::1"));
+
+

我们直接将数据附加到枚举的每个成员上,这样就不需要一个额外的结构体了。

+

使用枚举而不是结构体还有另外一个优势:每个成员可以处理不同类型和数量的数据。

+

The match Control Flow Operator

+

Concise Control Flow with if let

diff --git a/src/SUMMARY.md b/src/SUMMARY.md index ff115d6..e33c2cd 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -18,4 +18,12 @@ - [认识所有权](ch04-00-understanding-ownership.md) - [什么是所有权](ch04-01-what-is-ownership.md) - [引用 & 借用](ch04-02-references-and-borrowing.md) - - [Slices](ch04-03-slices.md) \ No newline at end of file + - [Slices](ch04-03-slices.md) + +- [结构体](ch05-00-structs.md) + - [方法语法](ch05-01-method-syntax.md) + +- [枚举和模式匹配](ch06-00-enums.md) + - [定义枚举](ch06-01-defining-an-enum.md) + - [`match`控制流运算符](ch06-02-match.md) + - [使用`if let`的具体控制流](ch06-03-if-let.md) \ No newline at end of file diff --git a/src/ch05-00-structs.md b/src/ch05-00-structs.md new file mode 100644 index 0000000..102b031 --- /dev/null +++ b/src/ch05-00-structs.md @@ -0,0 +1,329 @@ +# 结构体 + +> [ch05-00-structs.md](https://github.com/rust-lang/book/blob/master/src/ch05-00-structs.md) +>
+> commit 255b44b409585e472e14c396ebc75d28f540a1ac + +`struct`,是 *structure* 的缩写,是一个允许我们命名并将多个相关值包装进一个有意义的组合的自定义类型。如果你来自一个面向对象编程语言背景,`struct`就像对象中的数据属性(字段)。在这一章的下一部分会讲到如何在结构体上定义方法;方法是如何为结构体数据指定**行为**的函数。`struct`和`enum`(将在第六章讲到)是为了充分利用 Rust 的编译时类型检查,来在程序范围创建新类型的基本组件。 + +对结构体的一种看法是他们与元组类似,这个我们在第三章讲过了。就像元组,结构体的每一部分可以是不同类型。可以命令各部分数据所以能更清楚的知道其值是什么意思。由于有了这些名字使得结构体更灵活:不需要依赖顺序来指定或访问实例中的值。 + +为了定义结构体,通过`struct`关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字,他们被称作**字段**(*fields*),并定义字段类型。例如,列表 5-1 展示了一个储存用户账号信息的结构体: + +
+ +```rust +struct User { + username: String, + email: String, + sign_in_count: u64, + active: bool, +} +``` + +
+ +Listing 5-1: A `User` struct definition + +
+
+ +一旦定义后为了使用它,通过为每个字段指定具体值来创建这个结构体的**实例**。创建一个实例需要以结构体的名字开头,接着在大括号中使用`key: value`对的形式提供字段,其中 key 是字段的名字而 value 是需要储存在字段中的数据值。这时字段的顺序并不必要与在结构体中声明他们的顺序一致。换句话说,结构体的定义就像一个这个类型的通用模板。例如,我们可以像这样来声明一个特定的用户: + +```rust +# struct User { +# username: String, +# email: String, +# sign_in_count: u64, +# active: bool, +# } +# +let user1 = User { + email: String::from("someone@example.com"), + username: String::from("someusername123"), + active: true, + sign_in_count: 1, +}; +``` + +为了从结构体中获取某个值,可以使用点号。如果我们只想要用户的邮箱地址,可以用`user1.email`。 + +## 结构体数据的所有权 + +在列表 5-1 中的`User`结构体的定义中,我们使用了自身拥有所有权的`String`类型而不是`&str`字符串 slice 类型。这是一个有意而为之的选择,因为我们想要这个结构体拥有它所有的数据,为此只要整个结构体是有效的话其数据也应该是有效的。 + +可以使结构体储存被其他对象拥有的数据的引用,不过这么做的话需要用上**生命周期**(*lifetimes*),这是第十章会讨论的一个 Rust 的功能。生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中储存一个引用而不指定生命周期,比如这样: + +Filename: src/main.rs + +```rust,ignore +struct User { + username: &str, + email: &str, + sign_in_count: u64, + active: bool, +} + +fn main() { + let user1 = User { + email: "someone@example.com", + username: "someusername123", + active: true, + sign_in_count: 1, + }; +} +``` + +编译器会抱怨它需要生命周期说明符: + +``` +error[E0106]: missing lifetime specifier + --> + | +2 | username: &str, + | ^ expected lifetime parameter + +error[E0106]: missing lifetime specifier + --> + | +3 | email: &str, + | ^ expected lifetime parameter +``` + +第十章会讲到如何修复这个问题以便在结构体中储存引用,不过现在,通过通过从像`&str`这样的引用切换到像`String`这类拥有所有权的类型来修改修改这个错误。 + +## 一个示例程序 + +为了理解何时会需要使用结构体,让我们编写一个计算长方形面积的程序。我们会从单独的变量开始,接着重构程序直到使用结构体替代他们为止。 + +使用 Cargo 来创建一个叫做 *rectangles* 的新二进制程序,它会获取一个长方形以像素为单位的长度和宽度并计算它的面积。列表 5-2 中是项目的 *src/main.rs* 文件中为此实现的一个小程序: + +
+Filename: src/main.rs + +```rust +fn main() { + let length1 = 50; + let width1 = 30; + + println!( + "The area of the rectangle is {} square pixels.", + area(length1, width1) + ); +} + +fn area(length: u32, width: u32) -> u32 { + length * width +} +``` + +
+ +Listing 5-2: Calculating the area of a rectangle specified by its length and +width in separate variables + +
+
+ +尝试使用`cargo run`运行程序: + +``` +The area of the rectangle is 1500 square pixels. +``` + +### 使用元组重构 + +我们的小程序能正常运行;它调用`area`函数用长方形的每个维度来计算出面积。不过我们可以做的更好。长度和宽度是相关联的,因为他们一起才能定义一个长方形。 + +这个做法的问题突显在`area`的签名上: + +```rust,ignore +fn area(length: u32, width: u32) -> u32 { +``` + +函数`area`本应该计算一个长方形的面积,不过函数却有两个参数。这两个参数是相关联的,不过程序自身却哪里也没有表现出这一点。将长度和宽度组合在一起将更易懂也更易处理。 + +第三章已经讨论过了一种可行的方法:元组。列表 5-3 是一个使用元组的版本: + +
+Filename: src/main.rs + +```rust +fn main() { + let rect1 = (50, 30); + + println!( + "The area of the rectangle is {} square pixels.", + area(rect1) + ); +} + +fn area(dimensions: (u32, u32)) -> u32 { + dimensions.0 * dimensions.1 +} +``` + +
+ +Listing 5-3: Specifying the length and width of the rectangle with a tuple + +
+
+ + + +在某种程度上说这样好一点了。元组帮助我们增加了一些结构,现在在调用`area`的时候只用传递一个参数。不过另一方面这个方法却更不明确了:元组并没有给出它元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分: + + + +```rust,ignore +dimensions.0 * dimensions.1 +``` + +在面积计算时混淆长宽并没有什么问题,不过当在屏幕上绘制长方形时就有问题了!我们将不得不记住元组索引`0`是`length`而`1`是`width`。如果其他人要使用这些代码,他们也不得不搞清楚后再记住。容易忘记或者混淆这些值而造成错误,因为我们没有表达我们代码中数据的意义。 + +### 使用结构体重构:增加更多意义 + +现在引入结构体。我们可以将元组转换为一个有整体名称而且每个部分也有对应名字的数据类型,如列表 5-4 所示: + + +
+Filename: src/main.rs + +```rust +struct Rectangle { + length: u32, + width: u32, +} + +fn main() { + let rect1 = Rectangle { length: 50, width: 30 }; + + println!( + "The area of the rectangle is {} square pixels.", + area(&rect1) + ); +} + +fn area(rectangle: &Rectangle) -> u32 { + rectangle.length * rectangle.width +} +``` + +
+ +Listing 5-4: Defining a `Rectangle` struct + +
+
+ + + +这里我们定义了一个结构体并称其为`Rectangle`。在`{}`中定义了字段`length`和`width`,都是`u32`类型的。接着在`main`中,我们创建了一个长度为 50 和宽度为 30 的`Rectangle`的具体实例。 + +函数`area`现在被定义为接收一个名叫`rectangle`的参数,它的类型是一个结构体`Rectangle`实例的不可变借用。第四章讲到过,我们希望借用结构体而不是获取它的所有权这样`main`函数就可以保持`rect1`的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有`&`。 + +`area`函数访问`Rectangle`的`length`和`width`字段。`area`的签名现在明确的表明了我们的意图:计算一个`Rectangle`的面积,通过其`length`和`width`字段。这表明了长度和宽度是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值`0`和`1`。这是明确性的胜利。 + +### 通过衍生 trait 增加实用功能 + +如果能够在调试程序时打印出`Rectangle`实例来查看其所有字段的值就更好了。列表 5-5 尝试像往常一样使用`println!`宏: + +
+Filename: src/main.rs + +```rust,ignore +struct Rectangle { + length: u32, + width: u32, +} + +fn main() { + let rect1 = Rectangle { length: 50, width: 30 }; + + println!("rect1 is {}", rect1); +} +``` + +
+ +Listing 5-5: Attempting to print a `Rectangle` instance + +
+
+ +如果运行代码,会出现带有如下核心信息的错误: + +``` +error[E0277]: the trait bound `Rectangle: std::fmt::Display` is not satisfied +``` + +`println!`宏能处理很多类型的格式,不过,`{}`,默认告诉`println!`使用称为`Display`的格式:直接提供给终端用户查看的输出。目前为止见过的基本类型都默认实现了`Display`,所以它就是向用户展示`1`或其他任何基本类型的唯一方式。不过对于结构体,`println!`应该用来输出的格式是不明确的,因为这有更多显示的可能性:是否需要逗号?需要打印出结构体的`{}`吗?所有字段都应该显示吗?因为这种不确定性,Rust 不尝试猜测我们的意图所以结构体并没有提供一个`Display`的实现。 + +但是如果我们继续阅读错误,将会发现这个有帮助的信息: + +``` +note: `Rectangle` cannot be formatted with the default formatter; try using +`:?` instead if you are using a format string +``` + +让我们来试试!现在`println!`看起来像`println!("rect1 is {:?}", rect1);`这样。在`{}`中加入`:?`指示符告诉`println!`我们想要使用叫做`Debug`的输出格式。`Debug`是一个 trait,它允许我们在调试代码时以一种对开发者有帮助的方式打印出结构体。 + +让我们试试运行这个变化...见鬼了。仍然能看到一个错误: + +``` +error: the trait bound `Rectangle: std::fmt::Debug` is not satisfied +``` + +虽然编译器又一次给出了一个有帮助的信息! + +``` +note: `Rectangle` cannot be formatted using `:?`; if it is defined in your +crate, add `#[derive(Debug)]` or manually implement it +``` + +Rust **确实**包含了打印出调试信息的功能,不过我们必须为结构体显式选择这个功能。为此,在结构体定义之前加上`#[derive(Debug)]`注解,如列表 5-6 所示: + +
+ +```rust +#[derive(Debug)] +struct Rectangle { + length: u32, + width: u32, +} + +fn main() { + let rect1 = Rectangle { length: 50, width: 30 }; + + println!("rect1 is {:?}", rect1); +} +``` + +
+ +Listing 5-6: Adding the annotation to derive the `Debug` trait and printing the +`Rectangle` instance using debug formatting + +
+
+ +此时此刻运行程序,运行这个程序,不会有任何错误并会出现如下输出: + +``` +rect1 is Rectangle { length: 50, width: 30 } +``` + +好极了!这不是最漂亮的输出,不过它显示这个实例的所有字段,毫无疑问这对调试有帮助。如果想要输出再好看和易读一点,这对更大的结构体会有帮助,可以将`println!`的字符串中的`{:?}`替换为`{:#?}`。如果在这个例子中使用了美化的调试风格的话,输出会看起来像这样: + +``` +rect1 is Rectangle { + length: 50, + width: 30 +} +``` + +Rust 为我们提供了很多可以通过`derive`注解来使用的 trait,他们可以为我们的自定义类型增加有益的行为。这些 trait 和行为在附录 C 中列出。第十章会涉及到如何通过自定义行为来实现这些 trait,同时还有如何创建你自己的 trait。 + +我们的`area`函数是非常明确的————它只是计算了长方形的面积。如果这个行为与`Rectangle`结构体再结合得更紧密一些就更好了,因为这明显就是`Rectangle`类型的行为。现在让我们看看如何继续重构这些代码,来将`area`函数协调进`Rectangle`类型定义的`area`**方法**中。 \ No newline at end of file diff --git a/src/ch05-01-method-syntax.md b/src/ch05-01-method-syntax.md new file mode 100644 index 0000000..0fe0ee7 --- /dev/null +++ b/src/ch05-01-method-syntax.md @@ -0,0 +1,178 @@ +## 方法语法 + +> [ch05-01-method-syntax.md](https://github.com/rust-lang/book/blob/master/src/ch05-01-method-syntax.md) +>
+> commit c9fd8eb1da7a79deee97020e8ad49af8ded78f9c + +**方法**与函数类似:他们使用`fn`关键和名字声明,他们可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与方法是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十三章讲解)的上下文中被定义,并且他们第一个参数总是`self`,它代表方法被调用的结构体的实例。 + +### 定义方法 + +让我们将获取一个`Rectangle`实例作为参数的`area`函数改写成一个定义于`Rectangle`结构体上的`area`方法,如列表 5-7 所示: + +
+Filename: src/main.rs + +```rust +#[derive(Debug)] +struct Rectangle { + length: u32, + width: u32, +} + +impl Rectangle { + fn area(&self) -> u32 { + self.length * self.width + } +} + +fn main() { + let rect1 = Rectangle { length: 50, width: 30 }; + + println!( + "The area of the rectangle is {} square pixels.", + rect1.area() + ); +} +``` + +
+ +Listing 5-7: Defining an `area` method on the `Rectangle` struct + +
+
+ + + +为了使函数定义于`Rectangle`的上下文中,我们开始了一个`impl`块(`impl`是 *implementation* 的缩写)。接着将函数移动到`impl`大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成`self`。然后在`main`中将我们调用`area`方法并传递`rect1`作为参数的地方,改成使用**方法语法**在`Rectangle`实例上调用`area`方法。方法语法获取一个实例并加上一个点号后跟方法名、括号以及任何参数。 + +在`area`的签名中,开始使用`&self`来替代`rectangle: &Rectangle`,因为该方法位于`impl Rectangle` 上下文中所以 Rust 知道`self`的类型是`Rectangle`。注意仍然需要在`self`前面加上`&`,就像`&Rectangle`一样。方法可以选择获取`self`的所有权,像我们这里一样不可变的借用`self`,或者可变的借用`self`,就跟其他别的参数一样。 + +这里选择`&self`跟在函数版本中使用`&Rectangle`出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要能够在方法中改变调用方法的实例的话,需要将抵押给参数改为`&mut self`。通过仅仅使用`self`作为第一个参数来使方法获取实例的所有权,不过这是很少见的;这通常用在当方法将`self`转换成别的实例的时候,同时我们想要防止调用者在转换之后使用原始的实例。 + +使用方法而不是函数,除了使用了方法语法和不需要在每个函数签名中重复`self`类型外,其主要好处在于组织性。我将某个类型实例能做的所有事情都一起放入`impl`块中,而不是让将来的用户在我们的代码中到处寻找`Rectangle的功能。 + + + +> ### `->`运算符到哪去了? +> +> 像在 C++ 这样的语言中,又两个不同的运算符来调用方法:`.`直接在对象上调用方法,而`->`在一个对象的指针上调用方法,这时需要先解引用(dereference)指针。换句话说,如果`object`是一个指针,那么`object->something()`就像`(*object).something()`一样。 +> +> Rust 并没有一个与`->`等效的运算符;相反,Rust 有一个叫**自动引用和解引用**(*automatic referencing and dereferencing*)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。 +> +> 这是它如何工作的:当使用`object.something()`调用方法时,Rust 会自动增加`&`、`&mut`或`*`以便使`object`符合方法的签名。也就是说,这些代码是等同的: +> +> ```rust +> # #[derive(Debug,Copy,Clone)] +> # struct Point { +> # x: f64, +> # y: f64, +> # } +> # +> # impl Point { +> # fn distance(&self, other: &Point) -> f64 { +> # let x_squared = f64::powi(other.x - self.x, 2); +> # let y_squared = f64::powi(other.y - self.y, 2); +> # +> # f64::sqrt(x_squared + y_squared) +> # } +> # } +> # let p1 = Point { x: 0.0, y: 0.0 }; +> # let p2 = Point { x: 5.0, y: 6.5 }; +> p1.distance(&p2); +> (&p1).distance(&p2); +> ``` +> +> 第一行看起来简洁的多。这种自动引用的行为之所以能行得通是因为方法有一个明确的接收者————`self`的类型。在给出接收者和方法名的前提下,Rust 可以明确的计算出方法是仅仅读取(所以需要`&self`),做出修改(所以是`&mut self`)或者是获取所有权(所以是`self`)。Rust 这种使得借用对方法接收者来说是隐式的做法是其所有权系统人体工程学实践的一大部分。 + + + +### 带有更多参数的方法 + +让我们更多的实践一下方法,通过为`Rectangle`结构体实现第二个方法。这回,我们让一个`Rectangle`的实例获取另一个`Rectangle`实例并返回`self`能否完全包含第二个长方形,如果能返回`true`若不能则返回`false`。当我们定义了`can_hold`方法,就可以运行列表 5-8 中的代码了: + +
+Filename: src/main.rs + +```rust,ignore +fn main() { + let rect1 = Rectangle { length: 50, width: 30 }; + let rect2 = Rectangle { length: 40, width: 10 }; + let rect3 = Rectangle { length: 45, width: 60 }; + + println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); + println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3)); +} +``` + +
+ +Listing 5-8: Demonstration of using the as-yet-unwritten `can_hold` method + +
+
+ +我们希望看到如下输出,因为`rect2`的长宽都小于`rect1`,而`rect3`比`rect1`要宽: + +``` +Can rect1 hold rect2? true +Can rect1 hold rect3? false +``` + +因为我们想定义一个方法,所以它应该位于`impl Rectangle`块中。方法名是`can_hold`,并且它会获取另一个`Rectangle`的不可变借用作为参数。通过观察调用点可以看出参数是什么类型的:`rect1.can_hold(&rect2)`传入了`&rect2`,它是一个`Rectangle`的实例`rect2`的不可变借用。这是可以理解的,因为我们只需要读取`rect2`(而不是写入,这意味着我们需要一个可变借用)而且希望`main`保持`rect2`的所有权这样就可以在调用这个方法后继续使用它。`can_hold`的返回值是一个布尔值,其实现会分别检查`self`的长宽是够都大于另一个`Rectangle`。让我们在列表 5-7 的`impl`块中增加这个新方法: + + +Filename: src/main.rs + +```rust +# #[derive(Debug)] +# struct Rectangle { +# length: u32, +# width: u32, +# } +# +impl Rectangle { + fn area(&self) -> u32 { + self.length * self.width + } + + fn can_hold(&self, other: &Rectangle) -> bool { + self.length > other.length && self.width > other.width + } +} +``` + + + +如果结合列表 5-8 的`main`函数来运行,就会看到想要得到的输出!方法可以在`self`后增加多个参数,而且这些参数就像函数中的参数一样工作。 + +### 关联函数 + +`impl`块的另一个好用的功能是:允许在`impl`块中定义**不**以`self`作为参数的函数。这被称为**关联函数**(*associated functions*),因为他们与结构体相关联。即便如此他们也是函数而不是方法,因为他们并不作用于一个结构体的实例。你已经使用过一个关联函数了:`String::from`。 + +关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以一个关联函数,它获取一个维度参数并且用来作为长宽,这样可以更轻松的创建一个正方形`Rectangle`而不必指定两次同样的值: + +Filename: src/main.rs + +```rust +# #[derive(Debug)] +# struct Rectangle { +# length: u32, +# width: u32, +# } +# +impl Rectangle { + fn square(size: u32) -> Rectangle { + Rectangle { length: size, width: size } + } +} +``` + +使用结构体名和`::`语法来调用这个关联函数:比如`let sq = Rectangle::square(3);`。这个方法位于结构体的命名空间中:`::`语法用于关联函数和模块创建的命名空间,第七章会讲到后者。 + +## 总结 + +结构体让我们可以在自己的范围内创建有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名他们来使得代码更清晰。方法允许为结构体实例指定行为,而关联函数将特定功能置于结构体的命名空间中并且无需一个实例。 + +结构体并不是创建自定义类型的唯一方法;让我们转向 Rust 的`enum`功能并为自己的工具箱再填一个工具。 \ No newline at end of file diff --git a/src/ch06-00-enums.md b/src/ch06-00-enums.md new file mode 100644 index 0000000..efc6c0e --- /dev/null +++ b/src/ch06-00-enums.md @@ -0,0 +1,9 @@ +# 枚举和模式匹配 + +> [ch06-00-enums.md](https://github.com/rust-lang/book/blob/master/src/ch06-00-enums.md) +>
+> commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d + +本章介绍**枚举**,也被称作 *enums*。枚举允许你通过列举可能的值来定义一个类型。首先,我们会定义并使用一个枚举来展示它是如何连同数据一起编码信息的。接下来,我们会探索一个特别有用的枚举,叫做`Option`,它代表一个值要么是一些值要么什么都不是。然后会讲到`match`表达式中的模式匹配如何使对枚举不同的值运行不同的代码变得容易。最后会涉及到`if let`,另一个简洁方便处理代码中枚举的结构。 + +枚举是一个很多语言都有的功能,不过不同语言中的功能各不相同。Rust 的枚举与像F#、OCaml 和 Haskell这样的函数式编程语言中的**代数数据类型**(*algebraic data types*)最为相似。 \ No newline at end of file diff --git a/src/ch06-01-defining-an-enum.md b/src/ch06-01-defining-an-enum.md new file mode 100644 index 0000000..18cecb5 --- /dev/null +++ b/src/ch06-01-defining-an-enum.md @@ -0,0 +1,112 @@ +# 定义枚举 + +> [ch06-01-defining-an-enum.md](https://github.com/rust-lang/book/blob/master/src/ch06-01-defining-an-enum.md) +>
+> commit 396e2db4f7de2e5e7869b1f8bc905c45c631ad7d + +让我们通过一用代码来表现的场景,来看看为什么这里枚举是有用的而且比结构体更合适。比如我们要处理 IP 地。目前被广泛使用的两个主要 IP 标准:IPv4(version four)和 IPv6(version six)。这是我们的程序只可能会遇到两种 IP 地址:我们可以**枚举**出所有可能的值,这也正是它名字的由来。 + +任何一个 IP 地址要么是 IPv4 的要么是 IPv6 的而不能两者都是。IP 地址的这个特性使得枚举数据结构非常适合这个场景,因为枚举值尽可能是其一个成员。IPv4 和 IPv6 从根本上讲都是 IP 地址,所以当代码在处理申请任何类型的 IP 地址的场景时应该把他们当作相同的类型。 + +可以通过在代码中定义一个`IpAddrKind`枚举来表现这个概念并列出可能的 IP 地址类型,`V4`和`V6`。这被称为枚举的**成员**(*variants*): + +```rust +enum IpAddrKind { + V4, + V6, +} +``` + +现在`IpAddrKind`就是一个可以在代码中使用的自定义类型了。 + +### 枚举值 + +可以像这样创建`IpAddrKind`两个不同成员的实例: + +```rust +# enum IpAddrKind { +# V4, +# V6, +# } +# +let four = IpAddrKind::V4; +let six = IpAddrKind::V6; +``` + +注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在`IpAddrKind::V4`和`IpAddrKind::V6`是相同类型的:`IpAddrKind`。例如,接着我们可以顶一个函数来获取`IpAddrKind`: + +```rust +# enum IpAddrKind { +# V4, +# V6, +# } +# +fn route(ip_type: IpAddrKind) { } +``` + +现在可以使用任意成员来调用这个函数: + +```rust +# enum IpAddrKind { +# V4, +# V6, +# } +# +# fn route(ip_type: IpAddrKind) { } +# +route(IpAddrKind::V4); +route(IpAddrKind::V6); +``` + +使用枚举甚至还有更多优势。进一步考虑一下我们的 IP 地址类型,目前没有一个储存实际 IP 地址**数据**的方法;只知道它是什么**类型**的。考虑到已经在第五章学习过结构体了,你可以想如列表 6-1 那样修改这个问题: + +
+ +```rust +enum IpAddrKind { + V4, + V6, +} + +struct IpAddr { + kind: IpAddrKind, + address: String, +} + +let home = IpAddr { + kind: IpAddrKind::V4, + address: String::from("127.0.0.1"), +}; + +let loopback = IpAddr { + kind: IpAddrKind::V6, + address: String::from("::1"), +}; +``` + +
+ +Listing 6-1: Storing the data and `IpAddrKind` variant of an IP address using a +`struct` + +
+
+ +这里我们定义了一个有两个字段的结构体`IpAddr`:`kind`字段是`IpAddrKind`(之前定义的枚举)类型的而`address`字段是`String`类型的。这里有两个结构体的实例。第一个,`home`,它的`kind`的值是`IpAddrKind::V4`与之相关联的地址数据是`127.0.0.1`。第二个实例,`loopback`,`kind`的值是`IpAddrKind`的另一个成员,`V6`,关联的地址是`::1`。我们使用了要给结构体来将`kind`和`address`打包在一起,现在枚举成员就与值相关联了。 + +我们可以使用一种更简洁的方式来表达相同的概念,仅仅使用枚举并将数据直接放进每一个枚举成员而不是将枚举作为结构体的一部分。`IpAddr`枚举的新定义表明了`V4`和`V6`成员都关联了`String`值: + +```rust +enum IpAddr { + V4(String), + V6(String), +} + +let home = IpAddr::V4(String::from("127.0.0.1")); + +let loopback = IpAddr::V6(String::from("::1")); +``` + +我们直接将数据附加到枚举的每个成员上,这样就不需要一个额外的结构体了。 + +使用枚举而不是结构体还有另外一个优势:每个成员可以处理不同类型和数量的数据。 \ No newline at end of file diff --git a/src/ch06-02-match.md b/src/ch06-02-match.md new file mode 100644 index 0000000..b493e56 --- /dev/null +++ b/src/ch06-02-match.md @@ -0,0 +1 @@ +# The `match` Control Flow Operator diff --git a/src/ch06-03-if-let.md b/src/ch06-03-if-let.md new file mode 100644 index 0000000..1984155 --- /dev/null +++ b/src/ch06-03-if-let.md @@ -0,0 +1 @@ +# Concise Control Flow with `if let`