47、第四章总练习
总结
谈到domain modelling
(领域建模)时,细节决定成败。
Rust 提供了丰富的工具,可以帮助我们将领域中的约束直接表达在类型系统中,但要想掌握并写出符合惯例的代码,仍需一些实践和经验积累。
让我们以对Ticket
模型的最后改进来结束本章。
我们将为Ticket
中的每个字段引入一种新类型,以封装相应的约束。
每当有人访问Ticket
字段时,他们都会返回一个保证有效的值(即TicketTitle
而不是String)。他们不必担心代码中其他地方的title
为空:只要他们有一个TicketTitle
,他们就知道它通过construction
是有效的。
这只是如何使用Rust的类型系统时我们的代码更安全,更具表现力的一个示例。
补充阅读
练习
这次的练习算是一个较大的练习了,一共有四个rs文件
lib.rs:
// TODO: you have something to do in each of the modules in this crate!
// TODO: 在这个crate的每一个模块中,你都有一些事情要做!
mod description;
mod status;
mod title;
// A common pattern in Rust is to split code into multiple (private) modules
// and then re-export the public parts of those modules at the root of the crate.
// Rust 中一种常见模式是将代码拆分为多个(私有的)模块,然后在 crate 的根部重新导出这些模块中的公共部分。
// This hides the internal structure of the crate from your users, while still
// allowing you to organize your code however you like.
// 这隐藏了 crate 的内部结构,对用户透明,同时仍允许你以自己喜欢的方式组织代码。
pub use description::TicketDescription;
pub use status::Status;
pub use title::TicketTitle;
#[derive(Debug, PartialEq, Clone)]
// We no longer need to make the fields private!
// 我们不再需要将字段设为私有!
// Since each field encapsulates its own validation logic, there is no risk of
// a user of `Ticket` modifying the fields in a way that would break the
// invariants of the struct.
// 由于每个字段都封装了自身的验证逻辑,因此 Ticket 的使用者无法以破坏该结构体不变量的方式修改其字段。
//
// Careful though: if you had any invariants that spanned multiple fields, you
// would need to ensure that those invariants are still maintained and go back
// to making the fields private.
// 不过要小心:如果你存在跨多个字段的不变量(invariants),
// 那么就需要确保这些不变量仍然得到维护,此时你仍需将字段设为私有(private)。
pub struct Ticket {
pub title: TicketTitle,
pub description: TicketDescription,
pub status: Status,
}
title.rs:
// TODO: Implement `TryFrom<String>` and `TryFrom<&str>` for the `TicketTitle` type,
// enforcing that the title is not empty and is not longer than 50 bytes.
// Implement the traits required to make the tests pass too.
//TODO: 为 TicketTitle 类型实现 TryFrom<String> 和 TryFrom<&str>,
// 确保标题不为空且长度不超过 50 字节。
// 同时实现必要的 trait 以通过测试。
/* TODO */
pub struct TicketTitle(String);
impl TicketTitle {
pub fn value(&self) -> &str {
&self.0
}
}
/* TODO */}
description.rs:
// TODO: Implement `TryFrom<String>` and `TryFrom<&str>` for the `TicketTitle` type,
// enforcing that the title is not empty and is not longer than 50 bytes.
// Implement the traits required to make the tests pass too.
//TODO: 为 TicketTitle 类型实现 TryFrom<String> 和 TryFrom<&str>,
// 确保标题不为空且长度不超过 50 字节。
// 同时实现必要的 trait 以通过测试。
/* TODO */
pub struct TicketTitle(String);
impl TicketTitle {
pub fn value(&self) -> &str {
&self.0
}
}
/* TODO */}
status.rs:
// TODO: Implement `TryFrom<String>` and `TryFrom<&str>` for the `Status` enum.
// The parsing should be case-insensitive.
// TODO: 为 `Status` 枚举实现 `TryFrom<String>` 和 `TryFrom<&str>`。
// 解析应该不区分大小写。
/* TODO */
pub enum Status {
ToDo,
InProgress,
Done,
}
/* TODO */
}
分析这几个文件,我们需要在description.rs
、status.rs
、title.rs
这几个文件中进行修改。
完成测试文件的所有测试:
#[cfg(test)]
mod tests {
use std::convert::TryFrom;
use task_ticket_v2_outro::*;
// description
#[test]
fn description_test_try_from_string() {
let description = TicketDescription::try_from("A description".to_string()).unwrap();
assert_eq!(description.value(), "A description");
}
#[test]
fn description_test_try_from_empty_string() {
let err = TicketDescription::try_from("".to_string()).unwrap_err();
assert_eq!(err.to_string(), "The description cannot be empty");
}
#[test]
fn description_test_try_from_long_string() {
let description = "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.".to_string();
let err = TicketDescription::try_from(description).unwrap_err();
assert_eq!(
err.to_string(),
"The description cannot be longer than 500 bytes"
);
}
#[test]
fn description_test_try_from_str() {
let description = TicketDescription::try_from("A description").unwrap();
assert_eq!(description.value(), "A description");
}
// status
#[test]
fn status_test_try_from_string() {
let status = Status::try_from("ToDO".to_string()).unwrap();
assert_eq!(status, Status::ToDo);
let status = Status::try_from("inproGress".to_string()).unwrap();
assert_eq!(status, Status::InProgress);
let status = Status::try_from("Done".to_string()).unwrap();
assert_eq!(status, Status::Done);
}
#[test]
fn status_test_try_from_str() {
let status = Status::try_from("ToDO").unwrap();
assert_eq!(status, Status::ToDo);
let status = Status::try_from("inproGress").unwrap();
assert_eq!(status, Status::InProgress);
let status = Status::try_from("Done").unwrap();
assert_eq!(status, Status::Done);
}
#[test]
fn status_test_try_from_invalid() {
let status = Status::try_from("Invalid");
assert!(status.is_err());
}
// title
#[test]
fn title_test_try_from_string() {
let title = TicketTitle::try_from("A title".to_string()).unwrap();
assert_eq!(title.value(), "A title");
}
#[test]
fn title_test_try_from_empty_string() {
let err = TicketTitle::try_from("".to_string()).unwrap_err();
assert_eq!(err.to_string(), "The title cannot be empty");
}
#[test]
fn title_test_try_from_long_string() {
let title =
"A title that's definitely longer than what should be allowed in a development ticket"
.to_string();
let err = TicketTitle::try_from(title).unwrap_err();
assert_eq!(err.to_string(), "The title cannot be longer than 50 bytes");
}
#[test]
fn title_test_try_from_str() {
let title = TicketTitle::try_from("A title").unwrap();
assert_eq!(title.value(), "A title");
}
}
先看这个测试函数:
#[test]
fn description_test_try_from_empty_string() {
let err = TicketDescription::try_from("".to_string()).unwrap_err();
assert_eq!(err.to_string(), "The description cannot be empty");
}
错误提示为:
Trait `Debug` is not implemented for `TicketDescription` [E0277]
所以我们要为它实现一个处理错误的方法,unwarp_err()
这个方法,鼠标移上去之后可以看到这个方法的使用示例:
Panics
Panics if the value is an Ok, with a custom panic message provided by the Ok'svalue.
Examples
let x: Result<u32, &str> = Ok(2);
x.unwrap_err(); // panics with `2`
let x: Result<u32, &str> = Err("emergency failure");
assert_eq!(x.unwrap_err(), "emergency failure");
从上面的示例可以看到,unwarp_err()
这个方法是Result
类型的方法,源代码如下:
#[inline]
#[track_caller]
#[stable(feature = "rust1", since = "1.0.0")]
pub fn unwrap_err(self) -> E
where
T: fmt::Debug,
{
match self {
Ok(t) => unwrap_failed("called `Result::unwrap_err()` on an `Ok` value", &t),
Err(e) => e,
}
}
#[cfg(feature = "panic_immediate_abort")]
#[inline]
#[cold]
#[track_caller]
fn unwrap_failed<T>(_msg: &str, _error: &T) -> ! {
panic!()
}
可以看到,这个方法的作用是把Err
变体里面的值提取出来,如果匹配到的是Result::Ok
的话,那么就会panic。
所以,这个函数让我们实现的就是:description为空的时候,绑定一个错误枚举变体,错误枚举变体绑定一个字符串表示错误信息,具体内容是:“The description cannot be empty”。
看下一个测试函数:
#[test]
fn description_test_try_from_long_string() {
let description = "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.".to_string();
let err = TicketDescription::try_from(description).unwrap_err();
assert_eq!(
err.to_string(),
"The description cannot be longer than 500 bytes"
);
}
跟上面那个一模一样,实现的是相同的功能。
看下一个测试函数:
#[test]
fn description_test_try_from_str() {
let description = TicketDescription::try_from("A description").unwrap();
assert_eq!(description.value(), "A description");
}
也是一样的。
总结:
// description
#[test]
fn description_test_try_from_string() {
let description = TicketDescription::try_from("A description".to_string()).unwrap();
assert_eq!(description.value(), "A description");
}
#[test]
fn description_test_try_from_empty_string() {
let err = TicketDescription::try_from("".to_string()).unwrap_err();
assert_eq!(err.to_string(), "The description cannot be empty");
}
#[test]
fn description_test_try_from_long_string() {
let description = "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.".to_string();
let err = TicketDescription::try_from(description).unwrap_err();
assert_eq!(
err.to_string(),
"The description cannot be longer than 500 bytes"
);
}
#[test]
fn description_test_try_from_str() {
let description = TicketDescription::try_from("A description").unwrap();
assert_eq!(description.value(), "A description");
}
这四个函数,都是让我们对description
字段实现特定的错误处理功能,当遇到Err
时,绑定的对应的错误信息字符串要输出,当遇到Ok
时,就输出对应的具体值。
再来看status
的三个函数:
// status
#[test]
fn status_test_try_from_string() {
let status = Status::try_from("ToDO".to_string()).unwrap();
assert_eq!(status, Status::ToDo);
let status = Status::try_from("inproGress".to_string()).unwrap();
assert_eq!(status, Status::InProgress);
let status = Status::try_from("Done".to_string()).unwrap();
assert_eq!(status, Status::Done);
}
#[test]
fn status_test_try_from_str() {
let status = Status::try_from("ToDO").unwrap();
assert_eq!(status, Status::ToDo);
let status = Status::try_from("inproGress").unwrap();
assert_eq!(status, Status::InProgress);
let status = Status::try_from("Done").unwrap();
assert_eq!(status, Status::Done);
}
#[test]
fn status_test_try_from_invalid() {
let status = Status::try_from("Invalid");
assert!(status.is_err());
}
首先这里的断言有三个报错:
Binary operation `==` cannot be applied to type `Status` [E0369]
Trait `Debug` is not implemented for `Status` [E0277]
Trait `Debug` is not implemented for `Status` [E0277]
我们要实现PartialEq
(比较trait),Debug trait。这两个到时候应该是作为宏来添加的。
同时这里时实现了从String
和&str
到Status枚举变体的转换,我们要完成这里的from
转换,用到的还是带错误处理的转换,也就是try_from()
。
总结,我们要为Status
枚举实现从String
和&str
到Status
枚举变体的try_from()
方法,并且实现Status枚举变体之间的Debug
trait和PartialEq
trait。
对于title这里的测试函数
// title
#[test]
fn title_test_try_from_string() {
let title = TicketTitle::try_from("A title".to_string()).unwrap();
assert_eq!(title.value(), "A title");
}
#[test]
fn title_test_try_from_empty_string() {
let err = TicketTitle::try_from("".to_string()).unwrap_err();
assert_eq!(err.to_string(), "The title cannot be empty");
}
#[test]
fn title_test_try_from_long_string() {
let title =
"A title that's definitely longer than what should be allowed in a development ticket"
.to_string();
let err = TicketTitle::try_from(title).unwrap_err();
assert_eq!(err.to_string(), "The title cannot be longer than 50 bytes");
}
#[test]
fn title_test_try_from_str() {
let title = TicketTitle::try_from("A title").unwrap();
assert_eq!(title.value(), "A title");
}
其实还是那些东西,一点一点实现就可以了。
首先完成description的代码,按照上面提到的任务,实现tryfrom
方法,我们可以参考第四十五节的学习内容:
这是Status
状态枚举:
#[derive(Debug, PartialEq, Clone)]
pub enum Status {
ToDo,
InProgress,
Done,
}
这是针对该枚举类型的错误信息枚举:
#[derive(Debug, thiserror::Error)]
#[error("{invalid_status} is not a valid status")]
pub struct ParseStatusError {
invalid_status: String,
}
上面的错误枚举实现了Debug
trait,为的就是能够在断言语句中使用。还用到了this::error::Error
。我看了一下,这次的复习题没有cargo.toml
文件,所以我们不用引入this::error
这个第三方crate。
impl TryFrom<String> for Status {
type Error = ParseStatusError;
fn try_from(value: String) -> Result<Self, Self::Error> {
value.as_str().try_into()
}
}
上面这个是正在针对String
类型实现try_from()
方法,里面定义了关联类型Error的类型为ParseStatusError
,也就是错误枚举类型。
在这个实现体里面有一个try_from()
方法,这里塞着的就是具体的实现了,接受一个参数String
,返回值是Result,Ok的值是Self,也就是Status,Err的话,值就是Self::Error,也就是Status::ParseStatusError
。
我现在明白了,Self::Error
能够生效的原因,就是type Error = ParseStatusError;
这一行代码,把他们关联了起来,将 Self::Error(即 Status 的错误类型)绑定为 ParseStatusError
。
然后方法内部的代码是value.as_str().try_into()
,这说明传进这个方法的参数被转换为了&str
类型,然后调用了try_into()
,这里的try_into()
其实就可以看成try_from()
,因为它们之间互为对偶trait。那么我们还要实现&str
的try_from()
。
最终,我们要实现try_from()
方法,同时,value.as_str().try_into()
就等同于了<Status>.try_from(value.as_str())
,这个时候,我们的try_from()
方法就算完成了。
那么接下来实现一下Description
的内容吧:
我一共修改了两处内容,一处是派生宏的部分,另外一处是错误枚举定义和两个TryFrom
trait的实现:
#[derive(Debug,PartialEq,Clone)]
pub struct TicketDescription(String);
和
pub struct ParseDescriptionError{
pub reason: String,
}
impl TryFrom<String> for TicketDescription {
type Error = ParseDescriptionError;
fn try_from(value: String) -> Result<Self, Self::Error> {
if value.is_empty() {
Err(ParseDescriptionError { reason: "The description cannot be empty".to_string() })
}
else{
Ok(Self(value))
}
}
}
impl TryFrom<&str> for TicketDescription {
type Error = ParseDescriptionError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::try_from(value.to_string())
}
}
目前看来是没有报错的。
但是,测试函数还是有报错,我们可以看到
#[test]
fn description_test_try_from_string() {
let description = TicketDescription::try_from("A description".to_string()).unwrap();
assert_eq!(description.value(), "A description");
}
这个函数存在报错,unwarp方法错误Trait Debug is not implemented for ParseDescriptionError [E0277]
,原来是没有为错误枚举结构体实现Debug trait,我们手动添加一下派生宏就好了。
欸不对,还有一个功能没完成呢,就是字节过长的错误,简单添加一下:
impl TryFrom<String> for TicketDescription {
type Error = ParseDescriptionError;
fn try_from(value: String) -> Result<Self, Self::Error> {
if value.is_empty() {
Err(ParseDescriptionError { reason: "The description cannot be empty".to_string() })
}else if value.len() > 500 {
Err(ParseDescriptionError { reason: "The description cannot be longer than 500 bytes".to_string() })
}
else{
Ok(Self(value))
}
}
}
这样,就完成了Description的内容。
接下来,我们完成Status的代码,Status代码大部分都是枚举变体之间的断言匹配,我们要做的就是完成把String转换为枚举变体的方法。
其实还是try_from
方法,唯一的区别是返回类型是明确的Result<Ok(Status),Err(Status)>
,至于错误处理这一块,我们要保证的是,匹配出现错误时,返回的值不是Ok,而是Err。
这是测试函数的源代码:
#[test]
fn status_test_try_from_invalid() {
let status = Status::try_from("Invalid");
assert!(status.is_err());
}
和is_err()
方法的源代码:
#[must_use = "if you intended to assert that this is err, consider `.unwrap_err()` instead"]
#[rustc_const_stable(feature = "const_result_basics", since = "1.48.0")]
#[inline]
#[stable(feature = "rust1", since = "1.0.0")]
pub const fn is_err(&self) -> bool {
!self.is_ok()
}
返回类型是bool
类型,有个有意思的地方欸,is_ok()
方法实现了!操作符的重载?
呃呃呃,在想什么啊,is_ok()
返回值是bool方法罢了,我这是脑子出问题了的感觉……
#[must_use = "if you intended to assert that this is ok, consider `.unwrap()` instead"]
#[rustc_const_stable(feature = "const_result_basics", since = "1.48.0")]
#[inline]
#[stable(feature = "rust1", since = "1.0.0")]
pub const fn is_ok(&self) -> bool {
matches!(*self, Ok(_))
}
那么我们现在开始写代码吧。
好的,代码写好了,我主要还是做了两处修改,第一处,也是派生宏
#[derive(Debug,PartialEq,Clone,Copy)]
pub enum Status {
ToDo,
InProgress,
Done,
}
第二处,就是TryFrom了:
impl TryFrom<String> for Status {
type Error = String;
fn try_from(value: String) -> Result<Self, Self::Error> {
let value=value.into();
match value {
"ToDo"=>Ok(Self::ToDo),
"InProgress"=>Ok(Self::InProgress),
"Done"=>Ok(Self::Done),
_=>Err(value.to_string()),
}
}
}
impl TryFrom<&str> for Status {
type Error = String;
fn try_from(value: &str) -> Result<Self, Self::Error> {
match value {
"ToDo"=>Ok(Self::ToDo),
"InProgress"=>Ok(Self::InProgress),
"Done"=>Ok(Self::Done),
_=>Err(value.to_string()),
}
}
}
不太确定对不对,但是起码测试函数都不报错了。
那么我们最后完成title相关的代码:
// title
#[test]
fn title_test_try_from_string() {
let title = TicketTitle::try_from("A title".to_string()).unwrap();
assert_eq!(title.value(), "A title");
}
#[test]
fn title_test_try_from_empty_string() {
let err = TicketTitle::try_from("".to_string()).unwrap_err();
assert_eq!(err.to_string(), "The title cannot be empty");
}
#[test]
fn title_test_try_from_long_string() {
let title =
"A title that's definitely longer than what should be allowed in a development ticket"
.to_string();
let err = TicketTitle::try_from(title).unwrap_err();
assert_eq!(err.to_string(), "The title cannot be longer than 50 bytes");
}
#[test]
fn title_test_try_from_str() {
let title = TicketTitle::try_from("A title").unwrap();
assert_eq!(title.value(), "A title");
}
懒了,直接拷贝description
的代码吧干脆。
这个时候突然发现status.rs
有报错,我赶紧去看了一下,是let value=value.into();
这一行的错误,分析一下:
the trait bound `&str: From<String>` is not satisfied [E0277]
the trait `From<String>` is not implemented for `&str`
Note: to coerce a `String` into a `&str`, use `&*` as a prefix
Help: the following other types implement trait `From<T>`:
`String` implements `From<&String>`
`String` implements `From<&mut str>`
`String` implements `From<&str>`
`String` implements `From<Box<str>>`
`String` implements `From<Cow<'_, str>>`
`String` implements `From<char>`
Note: required for `String` to implement `Into<&str>`
哦哦,原来是用错方法了,准换成str,还是用as_str()
,我给忘了…
这个时候我突然意识到一个问题,我的description好像把代码写复杂了,似乎根本就不需要那个错误枚举,直接设定关联类型为String
就可以了。
OK,兄弟们,现在五个文件都没有报错了,俨然一副工整代码的样子,难道这次能一遍通过吗?我来试一下
啊,好多报错:
error[E0599]: `task_ticket_v2_outro::description::ParseDescriptionError` doesn't implement `std::fmt::Display`
--> TicketV2\Outro\Task\tests\integration.rs:16:24
|
16 | assert_eq!(err.to_string(), "The description cannot be empty");
| ^^^^^^^^^ method cannot be called due to unsatisfied trait bounds
|
::: C:\Users\CN059\RustroverProjects\100 Exercises to Learn Rust\TicketV2\Outro\Task\src\description.rs:18:1
|
18 | pub struct ParseDescriptionError{
| -------------------------------- doesn't satisfy `_: Display` or `_: ToString`
|
= note: the following trait bounds were not satisfied:
`task_ticket_v2_outro::description::ParseDescriptionError: std::fmt::Display`
which is required by `task_ticket_v2_outro::description::ParseDescriptionError: ToString`
error[E0599]: `task_ticket_v2_outro::description::ParseDescriptionError` doesn't implement `std::fmt::Display`
--> TicketV2\Outro\Task\tests\integration.rs:24:17
|
24 | err.to_string(),
| ^^^^^^^^^ method cannot be called due to unsatisfied trait bounds
|
::: C:\Users\CN059\RustroverProjects\100 Exercises to Learn Rust\TicketV2\Outro\Task\src\description.rs:18:1
|
18 | pub struct ParseDescriptionError{
| -------------------------------- doesn't satisfy `_: Display` or `_: ToString`
|
= note: the following trait bounds were not satisfied:
`task_ticket_v2_outro::description::ParseDescriptionError: std::fmt::Display`
which is required by `task_ticket_v2_outro::description::ParseDescriptionError: ToString`
哦,原来都是Decription的代码,那我把我自己瞎弄的错误枚举去掉试试呢?
没有完全通过,有两个failed了。
我又认真看了一下测试函数,会不会是这个的原因?
#[test]
fn status_test_try_from_str() {
let status = Status::try_from("ToDO").unwrap();
assert_eq!(status, Status::ToDo);
let status = Status::try_from("inproGress").unwrap();
assert_eq!(status, Status::InProgress);
let status = Status::try_from("Done").unwrap();
assert_eq!(status, Status::Done);
}
测试函数里的好像是ToDO还是ToD0?反正肯定不是ToDo,我修改一下。
错误明确给出了:
called `Result::unwrap()` on an `Err` value: "ToDO"
什么,居然是inproGress
的问题,测试里面是:
let status = Status::try_from("inproGress").unwrap();
assert_eq!(status, Status::InProgress);
他妈的,离了个大谱了,通过了我的天,什么奇奇怪怪的字符。
不是??????
哥们????????????
你要是不提示我报错,我还以为是代码逻辑问题呢?!!
你见过谁把ToDo
写成ToDO
,把InProgress
写成inproGress
?
终究是我才疏学浅,没有见过这种写法,估计有可能是JB官方专门这样做的,为了锻炼我们的排错能力?
幸亏你给了测试文件,我能看到测试函数怎么写的,你要是不给我,硬是让我瞎猜去了,那我高低得骂你几句,就算RustRover,你的课程,你的习题做得再好,我也得骂你。
不过还是挺高兴的,我基本上也算是依靠自己的努力完成了这个章节期末测试题,最多借助的是IDE的一个关键词提示的帮助,其实我还看了之前做的笔记。也得感谢我自己一直坚持做笔记,不然再看这纯英文的题目和知识点,我又得捏着鼻子去看……
这一节的学习就先到这了,希望我的Rust水平能越来越好!还是挺感谢RustRover的。