Rust Features

  • Borrow Checker
  • No GC
  • Lifetimes
  • Mutable References
  • Variables are Immutable by Default

Examples

fn main() { // functions are just codeblocks bound to names!
	let mut x = 5;
	x = 6;
	let z = { // this is a codeblock
		let z = 5;
		println!("{z}");
		5 + 6
	};
	println!("{z}")
}
 
fn add(x:i32) -> i32 {
	x + 5
}

Codeblocks

{ (statements;) * expression:t? } : t

So for example:

let x = {
	let x = 5;
	let y = 6;
	x + y
};
 
{
	println("bruh");
}; // type of unit, or formally ()
 
let x = {let x = 6; let y = 7; println!("{}", x+y); };
// reduces to
{} : ()
 
fn print_thing(x:i32) -> () {
	println!("{}", x);
}
fn main() {
	println!{"Hello"};
	let x = 5;
	let y = 7;
	x + 8;
}

If Expressions (not Statements!)

// WRONG!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
fn main() {
	if (true) {
		4
	} else {
		5
	} // expected (), found integer
}
 
// RIGHT
fn main() {
	{if (true) {
		4
	} else {
		5
	}};
}
 
// ALSO RIGHT
fn main() {
	let z = {
		if (true) {
			4
		} else {
			5
		}
	};
}

Ownership

Rules of Ownership

  1. Every value has an owner
  2. Every value has only one owner at a time
  3. When the owner goes out of scope, the value is dropped.

This removes the issue of the double-free. This is a GUARANTEE that there will ever only be one free for a malloc.

Ownership means that you can only use the current owner to refer to the value. For example:

fn main() {
	let x = 5;
	let y = String::from("hello");
	let z = y;
	println!("{y}"); // doesn't work. Borrow of moved value error!
	println!("{z}"); // fine and dandy
}

But cloning allows you to use both (deep copy):

fn main() {
	let x = 5;
	let y = String::from("hello");
	let z = y.clone();
	println!("{y}"); // fine and dandy
	println!("{z}"); // fine and dandy
}

Ownership solves the issue of not being able to pass ownership outside of the function - like in C if you returned a pointer to a value, but the value gets dropped after the function terminates, it doesn’t work. But Rust doesn’t have this problem, since you can just pass ownership of the value.

fn main() {
	let y = String::from("hello");
	takes_ownership(y);
	println!("{y}"); // Borrow after move error!
}
 
fn takes_ownership(a:String) {
	println!("{a}")
} // here when function returns, "hello" is dropped, as a goes out of scope

But consider

fn main() {
	let x = 5;
	let u = x;
	println!("{},{}", x, u); // this works because 5 is a primitive on the stack, not on the heap - though technically these rules apply to both the stack and the heap
 
	// but for the heap, it matters more:
	let y = String::from("hello");
	takes_ownership(y);
	let z = gives_ownership();
	println!("{z}"); // works
	let c = gives_and_takes(z); // takes back ownership
	println("{c}");
	borrows_data(&c); // doesn't transfer ownership, only borrows
	println("{c}"); // so this works fine and dandy
}
 
fn takes_ownership(a:String) /* takes ownership */ {
	println!("{a}")
}
 
fn gives_ownership() -> String {
	let b = String::from("world");
	b // gives up ownership of b
}
 
fn gives_and_takes(h:String) /* takes ownership */ -> String {
	println!("{h}");
	h // gives back ownership
}
 
fn borrows_data(g:&String) {
	println!("{g}");
}