Ice 的博客

用心记录思想

创建公有的结构体和枚举(Making Structs and Enums Public)

我们也可以使用pub来指定结构体和枚举为公有,但是这里有一些额外的使用详情需要注意。如果我们在使用pub定义一个结构体,我们使得这个结构体为公有,但是结构体的字段将仍是私有的。我们可以视情况而定让每一个字段成为公有或者私有。在Listing 7-9中,我们定义了一个公有的back_of_house::Breakfast结构体并且toast为公有字段,而seasonal_fruit为私有字段。该模型以餐厅为例,顾客可以选择随餐面包的类型,但是主厨会根据当季水果和库存来搭配什么水果。可选择的水果变化很快,所以顾客不能选择水果,甚至不能看到他们能得到的水果。

src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
mod back_of_house {
pub struct Breakfast {
pub toast: String,
seasonal_fruit: String,
}

impl Breakfast {
pub fn summer(toast: &str) -> Breakfast {
Breakfast {
toast: String::from(toast),
seasonal_fruit: String::from("peaches"),
}
}
}
}

pub fn eat_at_restaurant() {
// Order a breakfast in the summer with Rye toast
let mut meal = back_of_house::Breakfast::summer("Rye");
// Change our mind about what bread we'd like
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);

// The next line won't compile if we uncomment it; we're not allowed
// to see or modify the seasonal fruit that comes with the meal
// meal.seasonal_fruit = String::from("blueberries");
}

Listing 7-9: A struct with some public fields and some private fields

因为toast字段在back_of_house::Breakfast结构体内是公有的,在eat_at_restaurant中我们可以编写并且读到toast字段使用点(.)号。注意,我们不能在eat_at_restaurant中使用seasonal_fruit字段,因为seasonal_fruit字段是私有的。尝试取消注释修改季节水果字段的值,看看会得到什么错误。

同样,注意因为back_of_house::Breakfast有一个私有字段,这个结构体需要提供一个公有的关联函数来构造一个Breakfast实例(这里是名为summer的函数)。如果Breakfast没有这样一个函数,我们就不能在eat_at_restaurant中创建Breakfast实例,因为我们不能在eat_at_rest_aurant中给seasonal_fruit私有字段设置值。

相反地,如果我们设置枚举为公有,它所有的字段都是公有的。我们只需要在enum关键字前面加上pub即可,如Listing 7-10所示:

src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}

pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}

Listing 7-10: Designating an enum as public makes all its variants public

因为我们设置了Appetizer为公有枚举,我们可以在eat_at_restaurant中使用SoupSalad成员变量。

除非设置枚举的变量为公开的,否则枚举就没什么用了;在每种情况下都必须使用pub来声明所有枚举变量,这将是很恼人的设计,所以默认情况下枚举的所有变量都是公有的。结构体在字段为私有的情况通常是有用的,所以结构体字段默认遵循所有内容为私有的普遍规则,除非使用pub声明公有。

这里还涉及pub另外一个情况我们没有讨论到,那就是我们最后要讲的模块功能:use关键字。我们将单独介绍use,然后展示如何结合pubuse起来使用。

包和Crates

模块系统的第一部分我们将介绍包和crate。

crate是Rust编译时最小的代码单位。如果你用rustc而不是cargo来编译一个文件(第一章我们这么做),编译器还是会将那个文件认作一个crate。crate可以包含模块,模块可以定义在其他文件,然后和crate一起编译,我们会在接下来的章节中遇到。

crate有两种形式:二进制项和库。二进制crate可以被编译为可执行程序,比如一个命令行程序或者一个服务器。它们必须有一个main函数来定义当程序被执行的时候所需要做的事情。到目前为止,我们所创建的crate都是二进制crate。

库(Library crates) 没有main函数,而且它们不会编译成可执行文件。相反,它们定义了旨在与多个项目共享的功能。例如,我们在第二章用过的rand函数,用来生成随机数。大多数的Rust开发者说的crate指的都是库,这与其他变成语言中的library概念一致。

crate root是一个资源文件,Rust编译器以它为起始点,并组成crate的根模块(我们将在”Defining Modules to Control Scope and Privacy” 部分深度解析)。

包(package)是提供一系列功能的一个或者多个crate。一个包会包含Cargo.toml文件,阐述如何去创建这些crates。Cargo就是一个包含构建你代码的二进制项的包。Cargo也包含这些二进制项所依赖的库。其他项目也能用Cargo库来实现与Cargo命令行程序一样的逻辑。

包可以包含很多二进制(binary) crate,但是最多只能有一个库(library) crate。包无论是一个库(library crate)还是一个二进制库(binary crate), 至少要包含一个library crate。

让我们来看看创建包的时候会发生什么。首先,我们输入命令cargo new

1
2
3
4
5
6
7
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs

运行cargo new之后,我们使用ls查看Cargo创建了什么,在这个项目目录里,Cargo会给我们创建一个Cargo.toml文件,这就是一个包(package)。这里也有一个src目录包含main.rs文件。用编辑器打开Cargo.toml,并且文件内容里面没有提及src/main.rs。因为Cargo遵循的一个约定:src/main.rs就是一个与包同名的二进制crate的crate根。同样地,Cargo知道如果包目录中包含src/lib.rs,则包带有与其同名的库(library) crate。crate根文件将由Cargo传递给rustc来构建库(library)或者二进制(binary)。

这里,我们有一个只包含src/main.rs的包,意味着它只含有一个名为my-project的二进制crate。如果一个包同时含有src/main.rs和src/lib.rs,则它有两个crate:一个二进制(binary)的和一个库(library),并且名字都与包相同。通过将文件放在src/bin目录下,一个包可以拥有多个二进制crate:每个src/bin下的文件都会被编译成一个独立的二进制crate。

模块让我们在crate中组织代码,以提高可读性和易于重用。模块还允许我们控制items的私有性,因为在module中的代码默认是私有的。Private items是内部实现的细节不对外使用。我们可以选择将模块以及其中的items设置为公共的,这样,外部代码就可以使用并依赖于它们。

例如,让我们编写一个提供餐厅服务功能的library crate。我们将会定义函数的签名,但是函数的主题留空,集中于代码的组织,而不是实现一个餐馆。

在餐厅业中,餐馆的某些部分被称为前台(front of house),其他的部分称为后台(back of house)。前台是顾客所在的空间;这包括主座的顾客的位置,服务员接单和付款,以及调酒师调酒的地方。后台是主厨和厨师们在厨房,洗碗机清理,经历做行政工作。

这样构建我们的crate,我们可以将这些功能组织成嵌套的模块。创建一个新的库(library)命名为restaurant,通过命令cargo new restaurant --lib。输入如下Listing 7-1代码到src/lib.rs来定义一些模块和函数签名。这里是前台部分:

src/lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}

fn seat_at_table() {}
}

mod serving {
fn take_order() {}

fn serve_order() {}

fn take_payment() {}
}
}

Listing 7-1: A front_of_housemodule containing other modules that then contain functions

我们用mod关键字定义一个模块(在上面的例子中定义了front_of_house模块)。并用花括号包围模块的主体。在模块内,我们还可以定义其他模块,就像本例子中的hostingserving模块。模块还可以hold一些定义的其他items,例如structs, enums, constants, traits, 以及Listing 7-1中的函数。

通过使用模块,我们可以将相关的定义分组到一起,并指出他们为什么相关。程序设计者可以通过使用这段代码,更加容易找到他们想要的定义,因为他们可以基于分组来对代码进行导航,而不需要阅读所有的定义。程序设计者向这段代码中添加一个新功能时,他们也会知道代码应该放置在何处,可以保持程序的组织性。

在前面我们提到了,src/main.rssrc/lib.rs叫做crate根。之所以这么叫它们是因为这两个文件中的任何一个内容都在crate模块结构的根组成了一个名为crate的模块,该结构被称为模块树(module tree)。

Listing 7-2 展示了示例Listing 7-1中的模块树(modules tree)的结构。

1
2
3
4
5
6
7
8
9
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment

Listing 7-2: The module tree for the code in Listing 7-1

这个模块树展示了一些模块内嵌到另一个模块;例如,hosting内嵌在front_of_house之中。这个树结构还展示了一些模块是互为兄弟(siblings)的,这意味着它们定义在同一模块中(hostingserving被一起定义在front_of_house中)。继续沿用家庭关系的比喻,如果一个模块A被包含在模块B中,我们将模块A称为模块B的子模块,B模块为A的父模块。注意,整个模块树都植根于隐式模块名为crate之下。

模块树可能会让你想起计算机上的文件系统的目录树;这是一个非常恰当的比较!就像文件系统中的目录一样,你用模块来组织你的代码。就像文件目录中的文件,我们需要一个方式来找到我们的模块(modules)。

使用包、Crate和模块管理不断增长的项目

当你编写大型程序时,组织代码将变得越来越重要。通过相关功能进行分组,并将具有不同特性的代码分开,你将可以清楚地在哪里可以找到现实特定特性的代码,以及在哪里可以更改一个特性的工作方式。

到目前为止,我们编写的代码都只在一个文件中的一个模块中。 随着项目不断增长,你应该通过多个模块和多个文件来组织拆分你的代码。一个包可以包含多个二进制crates和一个可选的crate程序库。随着你的包的增长,你可以将部分提取到单独的crates使它变成外部依赖。本章涵盖了所有这些技巧。对于由一组相互关联的包组成的非常大的项目,Cargo提供了工作区(workspaces),我们将在第14章”Cargo Workspaces”部分学习。

我们还将讨论封装实现的细节,这让你可以高水平重用代码:一旦你实现了一个操作,其他代码可以通过代码的公共接口调用,无需知道你的实现过程是如何运作。编写代码的方式定义了那些部分是其他代码使用的公共部分,那些代码是你保留更改权利的私有实现细节。这是另一种限制,减少你必须记住项目内容细节的数量的方法。

这里还一个相关的概念是作用域(scope):代码所在的嵌套上下文有一组定义”in scope”的名称。当你阅读,编写和编译代码,程序设计师和编译器需要知道模块,常量或者其他有意义的项。你可以创建作用域并改变那些名称再作用域内还是在作用域外。同一个作用域不能拥有两个相同名称的项;可以使用一些工具来解决名称冲突。

Rust有许多特性允许你来管理你的代码组织结构,包括那些内容可以被公开,那些内容作为私有部分,以及程序每个作用域中的名字。这些功能。有时被称为”模块系统(the module system)”,包括:

  • 包(Packages):Cargo的一个功能,它允许你构建,测试和分享crate。
  • Crates:一个模块的树形结构,它形成了库或者二进制项目。
  • 模块(Modules)和Use:允许你控制组织结构的作用域和路径的私有性。
  • 路径(path):一个命名例如结构体,函数或模块等项的方式。

在本章,我们将涵盖所有这些特性,讨论他们怎样相互作用,并解释如何使用他们管理作用域。最后,你应该对模块系统有一个坚实的理解,并能够像专业人士一样使用作用域!

总结

现在我们学习了如何使用枚举创建自定义类型。我们也展示了标准库的Option<T>类型是如何帮助你利用类型系统来避免出错的。当枚举值包含数据时,你可以根据你需要处理多少种情况来选择使用mathc或者if let来提取或者使用这些值。

你的Rust程序现在能够使用结构体和枚举在你自己的作用域内表现其内容了。在你的API中使用定义类型保证了类型安全:编译器会确保你的函数只会得到它期望的类型的值。

为了提供一个有条理的API给你的用户,它使用起来很简单易懂,值暴露了你的用户需要的东西,现在我们准备开始学习Rust的模块。

通用模式和_占位符(Catch-all Patterns and the _ Placeholder)

使用枚举,我们希望对一些特定的值采取特殊操作,而对其他的值采用默认操作。想象我们正在玩一个游戏,如果你掷出一个骰子的值为3,角色不会移动,而是会得到一顶新奇的帽子。如果你掷出了7,你的角色将会失去新奇的帽子。对于其他的数值,你的角色会在棋盘上移动想的格子。这是一个实现了上述逻辑的match。骰子的结果是硬编码而不是一个随机值,其他的逻辑部分使用了没有函数体的函数来表示,实现它们超出了本例的范围:

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
other => move_player(other),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
}

对于前两个分支,匹配模式是字面值3和7,最后一个分支则涵盖了所有其他可能性的值,模式使我们命名为other的一个变量。other分支的代码通过将其传递给move_player函数来使用这个变量。

即使我们没有列出u8所有可能的值,这段代码依然能够编译,因为最后一个模式将匹配所有未被特殊列出的值。这种通配模式满足了match必须穷尽的要求。请注意,我们必须将通过通配分支放在最后,因为模式是按顺序匹配的。如果哦我们在通配分支后添加其他分支,Rust将会警告我们,因为此后的分支永远不会被匹配到。

Rust还提供了一个模式,当我们不想使用通配模式的值时,请使用_,这是一个特殊模式,可以匹配任意值而不用绑定到该值。这告诉Rust我们不会使用这个值,所以Rust也不会警告我们存在未使用的变量。

让我们改变游戏规则:现在,当你掷出的值不是3或7的时候,你必须再次掷出。这种情况下我们不需要使用这个值,所以我们改动代码使用_来替代变量other

1
2
3
4
5
6
7
8
9
10
11
12
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => reroll(),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}
}

这个例子也满足穷举性要求,因为我们在最后一个分支中明确地忽略了其他的值。我们没有忘记任何东西。

最后,我们再次改动游戏规则,如果投掷出3或者7以外的值,你的回合将无事发生。我们可以使用单元值(unit value:就是空的tuple,在”The Tuple Type”部分有提及)作为_分支的代码:

1
2
3
4
5
6
7
8
9
10
11
fn main() {
let dice_roll = 9;
match dice_roll {
3 => add_fancy_hat(),
7 => remove_fancy_hat(),
_ => (),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
}

在这里,我们明确告诉Rust我们不会使用与前面模式不匹配的值,并且这种情况下我们不想运行任何代码。

更详细的模式匹配相关我们将会在Chapter 18进行讲解。目前,我们继续讨论if let语法,这在match表达式有点冗长的情况下比较有用。

The Option Enum and Its Advantages Over Null Values

这一部分探索学习Option,另外一个标准库的枚举定义。Option类型编码了非常常见的场景,其中可以是某个值,也可以都不是。

例如,如果请求一个包含项的列表的第一个值,会得到一个值,如果请求一个空的列表,就什么也不会得到。从类型系统的角度来表达这个概念意思就是编译器需要检查是否处理了所有应该处理的情况,这样就可以避免在其他语言中非常常见的bug.

编程语言的设计通常考虑的是包含哪些特性,但是你排除的特性也很重要。Rust没有许多其他语言所具有的null特性。Null是一个值,表示什么也没有。在带有null特性的语言中,变量总是可以处于两种状态之一:null 或 not-null.

Null的发明者Tony Hoare在他2009年的报告”Null Reference: The Billon Dollar Mistake”中这样说:

我称之为十亿美元的错误。当时,我在为一个面向对象语言设计第一个综合性的面向引用的类型系统。我的目标是通过编译器的自动检查来保证所引用的使用都应该是绝对安全的。不过我未能抵抗住引入一个空引用的诱惑,仅仅是因为它是这么的容易实现。这引发了无数错误,漏洞和系统崩溃,在之后的四十多年中造成了十亿美元的苦痛和伤害。

空值的问题在于,如果你试图将空值用作非空值,会出现某种形式的错误。因为空和非空的属性到处都是,非常容易出现这类错误。

然而,null试图表达的概念仍然是有用的:null是当前由于某种原因无效或者不存在的值。

问题不在于概念,而在于具体的实现。因此,Rust没有空值,但它有一个枚举,可以编码值存在或者不存在的概念。这个枚举就是Option<T>,它由标准库定义如下:

1
2
3
4
enum Option<T> {
None,
Some(T),
}

Option<T>枚举非常有用,以至于它被包含在relude中;你不需要将它显式地引入作用域。另外,它的成员也是如此(被包含在prelude中),可以不需要Option::前缀来使用SomeNone。即便如此Option<T>也仍是常规的枚举,Some<T>None仍是Option<T>的成员。

<T>语法是我们还没讲到的Rust特性。它是一个泛型参数,在Chapter 10我们将会详细讲解。目前,你需要知道的就是<T>意味着Option枚举的Some成员可以包含任意类型的数据,同事每一个用于T位置的具体类型使得Option<T>整体作为不同的类型。这里是一些包含数字类型和字符串类型Option值的例子:

1
2
3
4
5
6
fn main() {
let some_number = Some(5);
let some_char = Some('e');

let absent_number: Option<i32> = None;
}

some_number的类型是Option<i32>some_char的类型是Option<char>,是不同的类型。因为我们在Some成员中指定了值,Rust可以推断出其类型。对于absent_number,Rust需要我们指定Option整体的类型,因为编译器只通过None值无法推断出Some成员保存的值的类型。这里我们告诉Rust我们需要absent_number的类型是Option<i32>

当我们有一个Some值,我们就知道存在一个值,这个值保存在Some中。当我们有一个值None,在某种意义上,它跟空值是有相同的意义:并没有一个有效的值。那么Option<T>为什么就比Null要好呢?

简单来说,因为Option<T>TT可以是任何类型)是不同的类型,编译器不允许像一个肯定有效的值那样使用Option<T>。例如:这段代码不能编译,因为它尝试将Option<i8>i8相加:

1
2
3
4
5
6
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;
}

如果我们运行这段代码,我们会得到一个错误信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
= help: the following other types implement trait `Add<Rhs>`:
<&'a f32 as Add<f32>>
<&'a f64 as Add<f64>>
<&'a i128 as Add<i128>>
<&'a i16 as Add<i16>>
<&'a i32 as Add<i32>>
<&'a i64 as Add<i64>>
<&'a i8 as Add<i8>>
<&'a isize as Add<isize>>
and 48 others

For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` due to previous error

铛!铛!铛!事实上,这个错误意味着Rust不知道如何将i8Option<i8>相加,因为它们是不同类型。在Rust中,当我们有一个值像i8,编译器将会确保我们总是有一个有效的值。我们可以自信地使用而无需做空检查。只有当使用Option<i8>(或者任何用到的类型)我们需要担心可能没有一个有效值,并且编译器将会确保我们在使用值之前处理了空的情况。

换句话说,你必须将Option<T>转换成T之后才能对T进行运算。通常这能帮助我们捕获到空值最常见的问题之一:假设某值不为空,但实际上是空的情况。

消除错误地假设非空值的风险有助于你对代码更有信心。为了有一个可能为空的值,你必须显式地使用Option<T>。然后,在使用该值时,需要显式地处理值为空的情况。任何地方,一个值不是Option<T>类型,你都可以放心地假设他不是null。这是Rust有意为之的设计决定,目的就是限制null的滥用,以增加Rust代码的安全性。

所以,当你有一个类型为Option<T>的值时,你如何从Some变量中获得T的值呢?Option<T>枚举有大量很有用的方法适用于各种情况;你可以查看它的文档(https://doc.rust-lang.org/std/option/enum.Option.html)。熟悉`Option`上的方法将对你的Rust之旅很有用哦!

通常,为了使用Option<T>值,你需要有处理每个变量的代码。你希望一些代码只在具有Some<T>值时运行,并且允许这些代码使用内部的T。你希望如果有个None值时运行一些其他的代码,而且这些代码也没有一个T的有效值。match表达式就是这么一个处理枚举的控制流结构:它会根据枚举的成员运行不同的代码,这些代码可以使用匹配到的值中的数据。

Enums and Pattern Matching

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

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

总结(Summary)

结构体让你创建自定义的类型在你的domain中。通过使用结构体,你可以将数据关联起来,使得它看起来更加清晰。在impl块内,你可以定义函数并关联你的类型,而且方法是一个很好的关联方式,它让你的结构体有了可以指定它特定行为的能力。

但是结构体不是唯一的方式来创建自定义类型:让我们转到Rust的枚举(enum)特性来,为你的工具箱再添加一个工具。

An Example Program Using Structs

为了理解何时需要使用结构体,让我们编写一个计算矩形面积的程序。我们将通过使用单个变量开始,然后重构这个程序,直到用结构体替代它。

让我们用cargo创建一个新的项目叫做rectangles的二进制程序,它获取以像素为单位的矩形的宽度和高度,并计算出矩形的面积。Listing 5-8 展示一个简短的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};

dbg!(&rect1);
}

我们可以把dbg!放在表达式放在30 * scale周围,以为你dbg!返回表达式的值的所有权,所以width字段将获得相同的值,就像我们在这里没有dbg!调用一样。我们不希望dbg!拥有rect1的所有权,所以我们在下一次调用dbg!时传递一个引用。下面是这个例子的输出结果:

1
2
3
4
5
6
7
8
9
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
width: 60,
height: 50,
}

我们可以看到第一条输出来自src/main.rs第10行,我们正在调用表达式30 * scale,其结果值是60(为整数实现的Debug格式化是只打印它们的值)。在src/main.rs第14行的dbg!调用输出&rect1的值,即Rectangle类型。dbg!宏确实很有用。

除了Debugtrait,Rust还为我们提供了很多通过derive属性来使用的trait,它们可以为我们的自定义类型增加实用的行为。这些trait和行为被列举在附录C(Appendix C)。第十章会介绍如何通过自定义行为来实现这些trait,同时还有如何创建你自己的trait。除了derive之外,还有很多属性;更多信息见”Rust Reference”的Attribute部分。

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

0%