面向对象的Rust(《rust书》第17章博客)

Object Orientated Rust (The rust book chapter 17 blog)

我想实现有关进一步使用Rust编程语言创建博客的选项的最后一个要点:

Allow users to add text content only when a post is in the Draft state. Hint: have the state object responsible for what might change about the content but not responsible for modifying the Post.

我想实现add_text方法,该方法将具有默认实现,该默认实现对State特征不执行任何操作,但是如果状态为Draft,则会将字符串推入post.content

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
28
29
30
31
32
33
34
35
36
pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, string: &str) {
        self.state.as_ref().unwrap().add_text(&mut self, string)
    }

    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(&self)
    }
}

trait State {
    fn content<'a>(&self, post: &'a Post) -> &'a str {
       ""
    }
    fn add_text<'a>(&self, post: &'a mut Post, string: &str) {
       "";
    }
}

impl State for Draft {
    fn add_text<'a>(&self, post: &'a mut Post, string: &str) {
        &mut post.content.push_str(string);
    }
}

我的问题围绕Post::add_text

1
self.state.as_ref().unwrap().add_text(&mut self, string)

我得到了一个不能借来的可变的尝试删除


问题是statePost的成员。如果借用state,则不能同时可变地借用self(即Post),这是正确的。考虑如果您的Draft::add_text()函数分配给接收到的post.state

会发生什么情况

1
2
3
4
5
impl State for Draft {
    fn add_text<'a>(&self, post: &'a mut Post, string: &str) {
        post.state = None; //killed self!
    }
}

此功能运行时,会使self消失!那将是非常不安全的。

Rust中不允许使用此使用模式,但是您有几种解决方法。最好的IMO是将要突变的数据分离为另一种类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pub struct PostData {
    content: String,
}
pub struct Post {
    state: Option<Box<dyn State>>,
    data: PostData,
}
trait State {
    fn add_text<'a>(&self, post: &'a mut PostData, string: &str) {
       "";
    }
    //...
}
impl Post {
    pub fn add_text(&mut self, string: &str) {
        self.state.as_ref().unwrap().add_text(&mut self.data, string)
    }
}

之所以有效,是因为您要借用self的各个部分,一方面是self.state,另一方面是self.data

另一种简单的解决方法是从Post中删除状态并在以后重新添加:

1
2
3
4
5
6
7
8
impl Post {
    pub fn add_text(&mut self, string: &str) {
        let state = self.state.take();
        state.as_ref().unwrap().add_text(&mut self, string);
        assert!(self.state.is_none());
        self.state = Some(state);
    }
}

我个人认为此解决方案有点怪异。


这里提供了一个解决方案

但是此解决方案有一些弃用警告。这是我的版本,用于修复警告

为了后人,这是提供的游乐场链接中的代码:

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
pub struct Post {
    state: Option<Box<dyn State>>,
    content: String,
}

impl Post {
    pub fn new() -> Post {
        Post {
            state: Some(Box::new(Draft {})),
            content: String::new(),
        }
    }

    pub fn add_text(&mut self, text: &str) {
        self.content = self.state.as_ref().unwrap().add_text(&self.content, text);
    }

    pub fn content(&self) -> &str {
        self.state.as_ref().unwrap().content(&self)
    }

    pub fn request_review(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.request_review())
        }
    }

    pub fn approve(&mut self) {
        if let Some(s) = self.state.take() {
            self.state = Some(s.approve())
        }
    }
}

trait State {
    fn request_review(self: Box<Self>) -> Box<dyn State>;
    fn approve(self: Box<Self>) -> Box<dyn State>;

    fn content<'a>(&self, _post: &'a Post) -> &'a str {
       ""
    }

    fn add_text(&self, original_text: &str, _text_to_add: &str) -> String {
        original_text.to_string()
    }
}

struct Draft {}

impl State for Draft {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        Box::new(PendingReview {})
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn add_text(&self, original_text: &str, text_to_add: &str) -> String {
        format!("{}{}", original_text, text_to_add)
    }
}

struct PendingReview {}

impl State for PendingReview {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        Box::new(Published {})
    }
}

struct Published {}

impl State for Published {
    fn request_review(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn approve(self: Box<Self>) -> Box<dyn State> {
        self
    }

    fn content<'a>(&self, post: &'a Post) -> &'a str {
        &post.content
    }
}


fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    post.add_text("\
And a steak!");
    assert_eq!("", post.content());

    post.request_review();
    post.add_text("\
And dessert!");
    assert_eq!("", post.content());

    post.approve();
    post.add_text("\
And coffee!");
    assert_eq!("I ate a salad for lunch today\
And a steak!", post.content());
}

这是我的主意。

由于有人暗示国家将对可能发生的变化负责,而不是对Post进行修改,因此请在State特质中添加add_text(或更合适的函数名)。使其接受字符串切片。

1
2
3
4
5
6
trait State {
    // Some codes here
    fn add_text<'a>(&self, _text: &'a str) -> &'a str {
       ""
    }
}

为Draft结构实现add_text方法:

1
2
3
4
5
6
impl State for Draft {
    // Some codes here
    fn add_text<'a>(&self, text: &'a str) -> &'a str {
        text
    }
}

以这样的方式重新编写Post结构的add_text:它将调用状态的add_text并返回应添加到当前内容的"更改"。

1
2
3
4
5
6
7
8
9
10
impl Post {
    // Some codes here
    pub fn add_text(&mut self, text: &str) {
        if let Some(s) = self.state.as_ref() {
            let text = s.add_text(text);
            self.content.push_str(text)
        }
    }
    // Some codes here
}

仅草稿状态将返回我们传递给它的字符串切片。我们将使用状态的add_text来确定要追加到当前内容的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fn main() {
    let mut post = Post::new();

    post.add_text("I ate a salad for lunch today");
    post.add_text("\
Then slept");
    assert_eq!("", post.content());

    post.request_review();
    post.add_text("\
Then woke up");
    assert_eq!("", post.content());

    post.approve();
    post.add_text("\
Then got back to work");
    assert_eq!("I ate a salad for lunch today\
Then slept", post.content());
}