CMSC330

Rust

Rust

Slices
Structs and Enums
Lifetime Elison
Closures
Smart Pointers

Slices

Slices

Recall the following memory issue


char* str = "hello world";
char* sub = *str[5]
free(str);
printf("%s",sub);
          

Similar in Rust


let x = String::from ("Hello World");
let y = &x[6..10];
drop(x);
println!("{}",y);
          

Rust will not compile this

Slices reference a part of a continuous piece of memeory


let x = String::from ("Hello World");
let hello = &x[0..5];
let world = &x[6..11];

x: String
  ptr -> Hello World
  len: 11
  capacity: 11

hello: &str
  ptr -> x.ptr
  len: 5

world: &str
  ptr -> x.ptr[6]
  len: 5
          

If x is dropped, so is everything else

x cannot be modifed while slices exist

Slices reference a part of a continuous piece of memeory


          let a = [1,2,3,4];
          let v = Vec::new();;
          v.push(1);
          let b = &a[1..3]
          let w = &v[..2]
          

Arrays and vectors also have slices

Slices reference a part of a continuous piece of memeory

Arrays and vectors also have slices

This is because we need to talk about parts of already existing data

This is because we need to talk about parts of already existing data


          fn first_word(x:&String) -> &String{
            x[0..x.find(" ").unwrap()] //error
          }
          

A String is something from String::from(...)

Above does not return a string, but a substring

Enter "substring type": &str

Enter "substring type": &str

Slices reference a part of a continuous piece of memeory

There are String Slices, Array Slices, Vector Slices, etc

There are String Slices, Array Slices, Vector Slices, etc

Common to use a slice instead of the data structure itself

Subtyping: all Strings are string slices

Structs and Enums


struct User {
    username: String,
    active: bool,
}
let user1 = User {
  username: String::from("clyff"),
  active: true,
}
println!("{}",user1.username);
          

Mutability describes a variable, not the data


let mut x = User { ... };
x.active = false;
          

struct User {
    username: String,
    active: bool,
}
let user1 = User {
  username: String::from("clyff"),
  active: true,
}
println!("{}",user1.username);
          

Structs have associated functions


struct User {
    username: String,
    active: bool,
}

impl User{
  fn is_online(&self){
    self.active
  }
}
          

Enums also are helpful for custom data types


enum Color{
  Green,
  Red,
  Blue,
  Other(String)
}

let r = Color::Red;
let o = Color::Other(String::from("Grey"));
          

Enums also have associated functions


impl Color{
  fn is_green(&self)->bool{
    match self{
      Color::green => true,
      _ => false,
    }
  }
}
          

Both Enums and Structs allow for Generics


struct Point<T>{
  x:T,
  y:T,
}

enum option<T>{
  None,
  Some(T),
}
          

Same for associated Functions


impl<T > Point<T>{
  fn x(&self) -> &T{
    &self.x
  }
}
          

Hashmaps are built into Rust


let x = HashMap<i32,String>::new();
          

Traits

Like an Java intrerface


pub trait Printable{
  fn print(&self) -> String;
}

impl Printable for User{
  fn print(&self){
    format!("{}:{}",self.username,self.active)
  }
}

impl Printable for Point{
  fn print(&self){
    format!("({},{})",self.x,self.y)
  }
}
          

default implemention: so also like abstract class


pub trait Printable{
  fn print(&self) -> String{
    "I was supposed to have this overridden"
  }
}
          

Can have multiple and on a generic


struct Point<T:Clone + PartialOrd>{
  x:T,
  y:T,
}
          

Common Traits

  • Copy
  • Clone
  • Move
  • Deref
  • Display

Lifetime Elison

Lifetimes are an implicit generic

Lifetime: scope for which a reference is valid


        let r; // delayed initialization
        {
            let x = 5;
            r = &x;
        }
        println!("r: {}", r); // dangling pointer!
          

r and x have different lifetimes

Lifetimes are an implicit generic

They are elided (ommited)


fn longest(x: &str, y: &str) -> &str {
  if x.len() > y.len() {
    x
  } else {
    y
  }
}

Rust Compiler makes deductions about lifetimes

Sometime's it doesn't have enough data

Programmer has to step in and stop eliding

Programmer has to step in and stop eliding


fn longest(x: &str, y: &str) -> &str {
  if x.len() > y.len() {
    x
  } else {
    y
  }
}

Return type has no gaurantee it's a valid reference


fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  if x.len() > y.len() {
    x
  } else {
    y
  }
}

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  if x.len() > y.len() {
    x
  } else {
    y
  }
}

Return type has no gaurantee it's a valid reference

Why? Rules of lifetime elison

Rules of lifetime elison (of functions)

  • Every input reference gets a unique lifetime
  • If one input lifetime, output matches that lifetime
  • If one param is &(mut)self, output matches that lifetime
  • Every input reference gets a unique lifetime

fn somefunc(x:&str, y: &str) -> usize {
...
}

gets elided to


fn somefunc<'a, 'b>(x:&'a str, y: &'b str) -> usize {
...
}
  • Every input reference gets a unique lifetime

fn somefunc(x:&str, y:i32, z:&str) -> usize {
...
}

gets elided to


fn somefunc<'a, 'b>(x:&'a str, y:i32, z:&'b str) -> usize {
...
}

If one input lifetime, output matches that lifetime


fn somefunc(x:&str) -> &str{
...
}

gets elided to


fn somefunc<'a>(x:&'a str) -> &'a str{
...
}

If one input lifetime, output matches that lifetime


fn somefunc(x:&str, y:i32) -> &str{
...
}

gets elided to


fn somefunc<'a>(x:&'a str, y:i32) -> &'a str{
...
}

If one param is &(mut)self, output matches that lifetime


fn somefunc(&self, x:&str) -> &str{
...
}

gets elided to


fn somefunc<'a,'b>(&'a self, x:&'b str) -> &'a str{
...
}

x is 'b due to rule one

Structs with references need explicit lifetimes as well


struct ImportantString<'a>{
  istring: &'a str,
}
          

The instance of ImportantString cannot outlive the reference it holds


let x = String::from("Hello world");
{
  let y = ImportantString {istring:&x };
}
          

Closures

Like OCaml, we can capture parts of our environment

We can do anonymous functions


let lambda = |x| x;
println!("{}",lambda(5));
          

We can do anonymous functions

Ownership and borrowing still apply


let s = String::from("hello");
let f = |x| println!("{x}");
f(s);
println!("{s}") // error
          

let s = String::from("hello");
let f = || println!("{s}");
f();
println!("{s}") // ok!
          

let mut s = String::from("hello");
let mut f = || s.push_str(" world");
f();
println!("{s}");
          

function traits are applied based off behavior

Smart Pointers


enum Tree{
  Leaf,
  Node(i32,Tree,Tree),
}
          

Issue: compiler does not know the size of Tree


enum Tree{
  Leaf,
  Node(i32,Box<Tree>,Box<Tree>),
}
          

References are pointers


let x = 5;
let y = &x;

assert_eq!(5, x);
assert_eq!(5, *y);
assert_eq!(5, y); //error here
          

Why do we not need to unpack the Box first?


let x = 5;
let y = Box::new(&x);

assert_eq!(5, x);
assert_eq!(5, *y);
          

Box has Deref and Drop traits

We can use this for functions


fn f(x: &str){
  println!("echoing {x}")
}

let a = Box::new(String::from("echo..."));
f(&a);
          

Use DerefMut for mutability

(Cannot go from immut to mut)

Sometimes Box is not sufficient


let a = Node(3, Box::new(Box::new(Nil)),Box::new(Nil));
let b = Node(2, Box::new(Box::new(Nil)),Box::new(Nil));
let c = Node(1, Box::new(a),Box::new(b));
let d = Node(4, Box::new(a),Box::new(a)); //not ok
          

We can fix with RC (Reference counting)

We can fix with RC (Reference counting)

Allows for multiple owners with trade off of potential memory leaks


enum Tree{
  Node(i32,Rc<Tree>,Rc<Tree>),
  Nil
}
let a = Tree(3, Rc::new(Rc::new(Nil)),Rc::new(Nil));
let b = Tree(2, Rc::new(Rc::new(Nil)),Rc::new(Nil));
let c = Tree(1, Rc::clone(&a),Rc::clone(&b));
let d = Cons(4, Rc::clone(&a),Rc::clone(&a)); //now ok
          

Has all tradeoffs of RC

(like cyclic structures)