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
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();
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
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)
fn somefunc(x:&str, y: &str) -> usize {
...
}
gets elided to
fn somefunc<'a, 'b>(x:&'a str, y: &'b str) -> usize {
...
}
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 };
}
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
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)