Traits and Generics II and Smart Pointers
Overview
We’ll wrap up our discussion of traits/generics and discuss smart pointers in this lecture.
Trait Syntax in Functions and Composing Traits
use std::fmt;
// identity_fn, print_excited, print_sum
fn identity_fn<T>(x: T) -> T {x}
fn print_excited<T: fmt::Display>(x: T) {
println!("{}!!!!!!!!!!!!!! :DDDDDDDDDD", x);
}
// An example of trait composition -- T must impl Display and PartialOrd
fn print_min<T: fmt::Display + PartialOrd>(x: T, y: T) {
if x < y {
println!("The minimum is {}", x);
} else {
println!("The minimum is {}", y)
}
}
fn main() {
println!("{:?}", identity_fn(Some(110)));
print_excited(110);
print_min(1, 10)
}
There are a couple of things about traits that we won’t have time to discuss in
lecture: among them are trait objects, which you can read about
here. You will see
polymorphic traits in this week’s assignment (in particular, From<T>
) and you
can read about them in the CS242 notes – you don’t have to know too much about them
for the purposees of the assignment. By now you should have the foundations to
incorporate traits into your own code to enable good code reuse and design!
The Rc<T>
Smart Pointer
Rc
unlike Box
allows you to have multiple pointers to the same chunk of
heap memory. This happens through the magic of reference counting – every time
to create a new pointer to this memory, the reference count is incremented. When
one of these pointers is dropped, the reference count is decremented. Once the
reference count is equal to 0, we deallocate the memory. Under the hood, this
implementation (like the Box
) implementation, needs to use unsafe
code, but
the interface as provided to you is safe. One thing that’s important to note is
that you have an immutable reference to the heap memory so as to not violate
Rust’s borrowing rules. We’ll see Rc
in action in the following example:
use std::fmt;
use std::rc::Rc;
struct LinkedList {
head: Option<Rc<Node>>,
size: usize,
}
struct Node {
value: u32,
next: Option<Rc<Node>>,
}
impl Node {
pub fn new(value: u32, next: Option<Rc<Node>>) -> Node {
Node {value: value, next: next}
}
}
impl LinkedList {
pub fn new() -> LinkedList {
LinkedList {head: None, size: 0}
}
pub fn get_size(&self) -> usize {
self.size
}
pub fn is_empty(&self) -> bool {
self.get_size() == 0
}
pub fn push_front(&self, value: u32) -> LinkedList {
let new_node: Rc<Node> = Rc::new(Node::new(value, self.head.clone()));
LinkedList {head: Some(new_node), size: self.size + 1}
}
// A tuple in Rust is like a struct -- you can access the zeroth element of
// a tuple name tup by writing "tup.0", you can access the element at index
// 2 by writing "tup.2", etc.
pub fn pop_front(&self) -> (Option<LinkedList>, Option<u32>) {
match &self.head {
Some(node) => {
let val = node.value;
let new_head: Option<Rc<Node>> = node.next.clone();
let new_list = LinkedList {head: new_head, size: self.size - 1};
(Some(new_list), Some(val))
},
None => (None, None)
}
}
}
impl fmt::Display for LinkedList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut current: &Option<Rc<Node>> = &self.head;
let mut result = String::new();
loop {
match current {
Some(node) => {
result = format!("{} {}", result, node.value);
current = &node.next;
},
None => break,
}
}
write!(f, "{}", result)
}
}
// For sake of simplicity, we removed the impl Drop here. You can still override
// it, you just might have to use some Rust features that are currently unstable.
// If you really wanted it to be efficient, you might just use unsafe rust.
fn main() {
let list: LinkedList = LinkedList::new();
let version1 = list.push_front(10);
let version2 = version1.push_front(5);
let version3 = version2.push_front(3);
let version4 = version2.push_front(4);
let (version5, result) = version4.pop_front();
println!("version1: {} has size {}", version1, version1.get_size());
println!("version2: {} has size {}", version2, version2.get_size());
println!("version3: {} has size {}", version3, version3.get_size());
println!("version4: {} has size {}", version4, version4.get_size());
println!("version5: {}, popped value: {}", version5.unwrap(), result.unwrap());
}
The RefCell<T>
Smart Pointer
RefCell
allows us to wrap a mutable piece of data behind an immutable reference.
We can call borrow
and borrow_mut
on the RefCell
to acquire references to
the internal data – the borrowing rules are now enforced at runtime. Therefore,
RefCell
is safe, but it’s slightly more inefficient because it shifts what would
have been a compile-time check to runtime. You commonly see Rc<RefCell<T>>
, which
lets us have a flexible handle to heap allocated data – just like Rc
except
now we can mutate things!