17、可变引用(练习)
可变引用
在上一节的习题中,我们完成了这样的代码
impl Ticket {
pub fn title(&self) -> &String {
&self.title
}
pub fn description(&self) -> &String {
&self.description
}
pub fn status(&self) -> &String {
&self.status
}
}
这段代码的作用是提供了Ticket结构体的封装,有三个方法可以返回内部的私有成员。
但是在调用时,会出现所有权丢失的问题,所以我们引入了借用机制,向方法传递实例的借用,并且也返回借用。
现在,我们有办法更改 Ticket 实例的字段,而无需在此过程中占用所有权。接下来,让我们看看如何通过setter方法来增强我们的 Ticket 结构。
Setters
Setter方法允许用户更改Ticket私有字段的值,同时确保一些该字段的规则限制(比如不能将Ticket的title设置为空)。
有两个常见的方式实现setters:
- 使用
self
作为输入 - 使用
&mut self
作为输入
使用self作为输入
我们大概要这样编码:
impl Ticket {
pub fn set_title(mut self, new_title: String) -> Self {
// Validate the new title [...]
self.title = new_title;
self
}
}
这段代码占用了self
的所有权,改变了title,最后返回一个修改过后的Ticket实例
如果我们想要使用的话,就必须在左侧用原变量接收返回值,不然会丢失所有权:
let ticket = Ticket::new(
"Title".into(),
"Description".into(),
"To-Do".into()
);
let ticket = ticket.set_title("New title".into());
上面的代码使用了variable shadowing
(变量遮蔽)机制。
有一个很酷的地方,当我们需要一次修改多个字段时,我们可以使用多个调用链接在一起方式
let ticket = ticket
.set_title("New title".into())
.set_description("New description".into())
.set_status("In Progress".into());
使用&mut self
作为输入
impl Ticket {
pub fn set_title(&mut self, new_title: String) {
// Validate the new title [...]
self.title = new_title;
}
}
和上面最大的不同是,我们不需要返回这个实例了。
这次的方式使用self
的可变引用作为输入,改变了title。不需要返回任何东西了。
这种使用方法也是很舒服的:
let mut ticket = Ticket::new(
"Title".into(),
"Description".into(),
"To-Do".into()
);
ticket.set_title("New title".into());
// Use the modified ticket
所有权保留在调用者手中,因此原始的Ticket实例依然有效。我们不需要重新分配结果,不过我们需要将ticket标记为可变引用
,这里的重点在可变
上,上一节的重点是引用
。
&mut-setter
相比前面的第一种方案有一个缺点:我们不能将多个调用链接在一起,因为它们不返回修改后的Ticket实例,我们必须单独调用每个setter
练习
这道题目让我们实现setter方法,同时封装三个字段校验器,并修改new方法里面的逻辑,使用字段校验器
// TODO: Add &mut-setters to the `Ticket` struct for each of its fields.
// Make sure to enforce the same validation rules you have in `Ticket::new`!
// Even better, extract that logic and reuse it in both places. You can use
// private functions or private static methods for that.
pub struct Ticket {
title: String,
description: String,
status: String,
}
impl Ticket {
pub fn new(title: String, description: String, status: String) -> Ticket {
// TODO:
validate_title(&title);
if description.is_empty() {
panic!("Description cannot be empty");
}
validate_description(&description);
validate_status(&status);
Ticket {
title,
description,
status,
}
}
pub fn title(&self) -> &String {
&self.title
}
pub fn description(&self) -> &String {
&self.description
}
pub fn status(&self) -> &String {
&self.status
}
pub fn set_title(&mut self, title: String) {
validate_title(&title);
self.title = title;
}
pub fn set_description(&mut self, description: String) {
validate_description(&description);
self.description = description;
}
pub fn set_status(&mut self, status: String) {
validate_status(&status);
self.status = status;
}
}
fn validate_title(title: &String) {
if title.is_empty() {
panic!("Title cannot be empty");
}
if title.len() > 50 {
panic!("Title cannot be longer than 50 bytes");
}
}
fn validate_description(description: &String) {
if description.is_empty() {
panic!("Description cannot be empty");
}
if description.len() > 500 {
panic!("Description cannot be longer than 500 bytes");
}
}
fn validate_status(status: &String) {
if status != "To-Do" && status != "In Progress" && status != "Done" {
panic!("Only `To-Do`, `In Progress`, and `Done` statuses are allowed");
}
}
字段校验器的参数要传递String的引用,这是因为如果不传递引用,调用者就没有这个String的所有权啦,所以必须传递引用。
另外我想吐槽一下,起码告诉我函数签名是什么吧,你不说我怎么敢确定参数就是&mut self
和一个String类型的变量呢?
难道这也是考察的一部分?虽然前面的理论部分给的set例子就是String变量,其实看报错信息,也能看出来(
或许这是RustRover的良苦用心吧(doge
这一节的学习就到这里了,拜拜