Object-Oriented Programming Features of Rust
The notes reflect the topics in Chapter 17
Using Trait for dynamic dispatch
Let’s say we want to have a gui lib, which support drawing a bunch of components. How would we implement that?
Let’s consider C++ first. An good example would be the
Qt framework (let’s forget Qt QML for now). Stuff there
are all Widget, and stuff got complicated inheritence chain. Here’s an
example.
Recall that we need dynamic dispatch so that we can do stuff like
// C inherits B
B* b = new C();
b->bar(); // call C's method if bar is a virtual function.Virtual in C++ basically means dymamic dispatch. The compiler stores a vpointer
in each class, add sizeof(vpointer) to each class. The pointer points to
vtable, which maps function calls to the right function pointer address.
What does Rust do? We don’t have inheritence here. All we got is trait. So we have dynamic dispatch that look like duck typing. Still using vtable under the hood though.
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>, // (1)
}
- That
dynmeans dynamic dispatch. It’s needed in order to make a trait “type-like”. This is similar to the case when we use traints in a method bound, we doitem: &impl Summary. Of courseBoxis needed to make it a pointer.
What’s the difference vs using generic type (template)? With template like
pub trait Draw {
fn draw(&self);
}
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
impl<T> Screen<T>
where
T: Draw,
{
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}Only one typeo of the component would be initiated.
Implementing an Object-Oriented Design Pattern
This part of the book is quite cryptic. Pay close attention to my annotation of the code to understand some design choice over others.
The example given is to write a blog post struct that does this thing:
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}Let’s first have a special state trait to do all the state transition stuff. It looks like this:
pub struct Post {
state: Option<Box<dyn State>>, // (1)
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.push_str(text);
}
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(self) // (3)
}
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() { // (4)
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>; // (2)
fn approve(self: Box<Self>) -> Box<dyn State>;
fn content<'a>(&self, post: &'a Post) -> &'a str {
""
}
}
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
}
}
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
}
}
-
Why the
Optionhere? Please look at the annotation atrequest_reviewmethod. -
Why no default method is being pr
-
ovided? If I provide one, the compiler would complain:
error[E0277]: the size for values of type `Self` cannot be known at compilation time --> main.rs:38:9 | 37 | fn request_review(self: Box<Self>) -> Box<dyn State> { | - help: consider further restricting `Self`: `where Self: std::marker::Sized` 38 | self | ^^^^ doesn't have a size known at compile-time | = help: the trait `std::marker::Sized` is not implemented for `Self` = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait> = note: required for the cast to the object type `dyn State`So even though
Boxis actually sized, Rust does not allow us to return a Box of unknown size in the trait. It’s related to somenthing called object safety.The idea of “object-safety” is that you must be able to call methods of the trait the same way for any instance of the trait. So the properties guarantee that, no matter what, the size and shape of the arguments and return value only depend on the bare trait — not on the instance (on Self) or any type arguments (which will have been “forgotten” by runtime). (From Reddit thread)
-
We call the
as_refmethod on theOptionbecause we want a reference to the value inside theOptionrather than ownership of the value. Because state is anOption<Box<dyn State>>, when we callas_ref, anOption<&Box<dyn State>>is returned. If we didn’t callas_ref, we would get an error because we can’t movestateout of the borrowed&selfof the function parameter.We then call the
unwrapmethod, which we know will never panic, because we know the methods onPostensure thatstatewill always contain aSomevalue when those methods are done. -
To consume the old state, the
request_reviewmethod needs to take ownership of thestatevalue. This is where theOptionin thestatefield ofPostcomes in: we call thetakemethod to take theSomevalue out of thestatefield and leave aNonein its place, because Rust doesn’t let us have unpopulated fields in structs. This lets us move thestatevalue out ofPostrather than borrowing it. Then we’ll set the post’sstatevalue to the result of this operation.We need to set
statetoNonetemporarily rather than setting it directly with code likeself.state = self.state.request_review();to get ownership of thestatevalue. This ensuresPostcan’t use the oldstatevalue after we’ve transformed it into a new state.There’s this StackOverFlow question about what does this actually mean. I don’t find that useful. My guess is that if panic happen in the middle, there could be a state where the state is being moved out, but no new value is being assigned. Then we set
self.stateto this undefined value, which is again undefined. That’s why we need to explicitly set aNonethere.
Instead of doing things like this, we can also just forget the state and just have method on individual state struct (isn’t that more intuitive)?
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
pub fn request_review(self) -> PendingReviewPost {
PendingReviewPost {
content: self.content,
}
}
}
pub struct PendingReviewPost {
content: String,
}
impl PendingReviewPost {
pub fn approve(self) -> Post {
Post {
content: self.content,
}
}
}