Notes

Rust basics

Install

# install rustup
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

# update
rustup update

# uninstall
rustup self uninstall

# rust version
rustc --version

# local doc
rustup doc
bash

Hello World

/* The main function is the entry of any program.
BTW this is a block comment.
*/
fn main() {
    // macro call, BTW this is a line comment.
    println!("Hello World!");
}
rust

compile and run:

# rust compiler
rustc hello.rs
./hello
bash

Package manager: Cargo

# check cargo version
cargo --version

# create a project
cargo new hello_cargo
cd hello_cargo
bash

It creates a Cargo.toml config file:

[package] 
name = "hello_cargo" 
version = "0.1.0" 
edition = "2018" 

[dependencies]
toml

and a src folder with main.rs in it. To build and run a cargo project, go to the project directory and:

# first build then manually run
cargo build
# by default, it builds into debug mode
./target/debug/hello_cargo

# Or two steps in one:
cargo run

# just check compilation process, do not generate executable
# much faster for debugging!
cargo check 

# build for release
cargo build --release
./target/release/hello_cargo
bash

To add a dependency in Cargo.toml:

[dependencies] 
rand = "0.8.3"
toml

Then cargo build will automatically download and build it. A Cargo.lock is generated to keep track of the exact dependency versions. To update all dependencies:

# only update PATCH version! e.g., 0.8.3 --> 0.8.4
cargo update

# to update MAJOR or MINOR version, you must change the toml manually.
bash

Variables

Rust is a statically and strongly typed language. However, rust can infer the data type from code, so you can declare variables without annotating data type.

Scalar types

  • integers
    • signed: i8, i16, i32, i64, i128, isize (isize means arch-dependent)
    • unsigned: u8, u16, u32, u64, u128, usize
    • i32 is the default.
let x = 0; // i32 by default
let x: u8 = 0; // u8
let x = 0u8; // u8
let x = 1_000; // i32, 1000
let x = 0xff // i32, hex
let x = 0b1111_0000 // i32, binary
rust
  • float
    • f32, f64
    • f64 is the default.
let x = 2.0; // f64
let x: f32 = 2.0; // f32
rust
  • boolean
    • bool
let t = true; // bool
let f: bool = false; // bool
rust
  • character
    • char, but it is 4-byte for unicode encoding. (not 1-byte ASCII as in c)
let c = 'z'; // char
let c = '😻'; // char, supports unicode emoji
rust

Compound types

  • tuple
    • A general way of grouping a number of values with any type.
    • fixed-length.
let t = (1, 2.0, 'c'); // simple tuple
let t: (i32, f64, char) = (1, 2.0, 'c'); // with type annotaiton

let x = t.0; // indexing
let y = t.1;
let z = t.2;

let (x, y, z) = t; // destructuring

let u = (); // empty tuple, or the unit value. (default value for empty expression)

// tuples can be useful in function 
fn foo(s: String) -> (String, u32) {
	(s, s.len())
}
rust
  • array
    • can only hold values of the same data type.
    • also fixed-length! (instead, use Vec for python-like list)
let a = [0, 1, 2]; // simple i32 array
let a: [i32; 3] = [0, 1, 2]; // type annotation [dtype; length]
let a = [0; 3]; // equals let a = [0, 0, 0];

println!("{:?}", a); // debug print for array

let x = a[0]; // indexing

// slicing
let arr = [1, 2, 3, 4, 5];
let s1: &[i32] = &arr; // full slice
let s2 = &arr[0..2] // [1, 2], partial slice
let s3 = &arr[1..] // [2, 3, 4, 5]

// iterator
for i in arr {}
for i in arr.iter() {} // same
for i in &arr {} // same


// pass array by reference to function
// to handle any-length arrays, we use slice of array, noted by &[dtype]
// thankfully, array slice knows its length, unlike the array pointer in c.
fn sum(xs: &[i32]) -> i32 {
	let mut res = 0;
	for i in 0..xs.len() {
		res += xs[i];
	}
	res
}

let arr = [1, 2, 3];
let res = sum(&arr); // 6
rust

Mutability

fn main() {

	let x = 0; // immutable, infered as i32
	// x = 1; // compilation error

	let mut y = 0; // mutable
	y = 1; // Ok

	const z = 0; // constant

	let x = x + 1; // shadowing, OK. (reassigned the value)
	{
		let x = 2; // shadowing, only in current scope
		println!("x = {}", x); // x = 2
	}
	println!("x = {}", x); // x = 1
}
rust

const are more than immutable variables:

  • must be declared with data type.
  • can be declared in any scope (e.g., the global scope)
  • can only be set to a constant expression, not a value computed at runtime. (3*6 is OK, but 3*x is not.)
  • (convention) name should be UPPER_CASE connected by underscores.

Functions

rust will find the definition of function automatically, so you can define it anywhere like python and not like c.

  • use fn to define a function
  • must declare the parameter data type.
  • if you return some value, must declare the return data type too.
  • the last statement without semicolon will be returned implicitly, else it returns the unit value ().
// MUST declare the data type for parameters!
fn foo(x: i32) {
	println!("x = {}", x);
}

// return value as if the function is an expression
// MUST declare the data type for return value! if not declared, it is default to ()
fn bar() -> i32 {
	let y = { // start an expression block
		let x = 1;
		x + 1 // WITHOUT semicolon to serve as return value!!! if with semicolon, this expr returns () implicitly
	}; // y == 2
	y + 1 // equals `return y + 1;`
}

let x = bar(); // x == 3

// fibonacci example
fn fibonacci(x: i32) -> i32 {
	if x == 0 {
		1
	} else {
		x * fibonacci(x - 1)
	}
}
rust

by default, parameters are passed by value in function. To pass parameters by reference, we need reference operator & and dereference operator *.


let x = 1;
// pass by ref, and return increased value
fn inc(x: &i32) -> i32 {
	*x + 1
}
let x = inc(x);  // x == 2

// reference can be modified inplace
let mut x = 1;
// pass by ref, do not forget the mut
fn inplace_inc(x: &mut i32) {
	*x += 1;
}
inc(x); // x == 2
rust

Controls

condition

let x = 3;

// if <bool> {}
if x != 0 { // cannot use `if x {}`, no implicit casting!
	// do sth
} else if x % 2 == 0 {
	// do sth
} else {
	// do sth
}

// if in statement
let cond = true;
let x = if cond {5} else {6}; // data type must be the same in two branches.

rust

loop

// pure loop
loop { println!("again!"); }

// break nested & labeled loop
let mut i = 10;
'flag1: loop {
	let mut j = 10;
	loop {
		if foo(i, j) == 0 {
			break; // break j loop
		} else if foo(i, j) == 1 {
			break 'flag1; // break i loop
		}
		j -= 1;
		if j == 0 {break;}
	}
	i -= 1;
	if i == 0 {break;}
}

// return value with break
let mut counter = 0; 
let result = loop { 
	counter += 1; 
	if counter == 10 { break counter * 2; } 
}; // result == 20


// while loop
let mut x = 3;
while x != 0 {
	x -= 1;
}

// for loop
let a = [1, 2, 3];
for x in a {
	println!("{}", x);
}

for x in 0..5 {} // for x in [0, 1, 2, 3, 4]

for x in (0..5).rev() {} // for x in [4, 3, 2, 1, 0]
rust

Strings

// &str (string literals)
// it create on stack a hardcoded string literals, and return its immutable slice reference.
let sl = "hello"; // hardcoded, immutable, fast, on stack.

// TODO: is there mutable string literals? like `let mut sl = "hello";`

// String
let mut s = String::from("hello"); // mutable, slower, on heap.
s.push_str(", world"); // s == "hello, world"
rust

Ownership

Rules of Ownership:

  • Each value has a variable called its owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

The move semantic:

// for scalar types (stack-only), there is no moving.
let x = 5; 
let y = x; // y == 5, and x is still 5 (copied x to y)

// for complex type like String (symbol on stack, data on heap), there is moving.
let x = String::from("hello");
let y = x; // y == "hello", but x has been invalid! (copied x's symbol to y, moved x's data to y, and dropped x's symbol)

// if you really want to copy data on heap
let x = String::from("hello");
let y = x.clone(); // x and y are both valid, each with its own data allocated on heap

// passing to function also invokes moving
fn foo(s: String) {}
foo(x); // x moved to foo(), and become invalid.

// returning from function too
fn foo(s: String) -> String {
	s;
}
let x = foo(x); // x is moved to foo(), then returned to x.
rust

To avoid moving in function parameters, we need references & borrowing. Rule of references:

  • At any given time, you can have either one mutable reference or any number of immutable references.
  • References must always be valid.

// &String means **reference** of String
fn foo(s: &String) -> u32 {
	s.len()
}
let s = String::from("hello");
let l = foo(&s); // s is still valid, we only **borrow** s in foo by using `&s`

// by default, reference is immutable.
// but we can also declare mutable reference:
fn foo(s: &mut String) {
	s.push_str(", world");
}
let mut s = String::from("hello"); // the variable also should be mutable
foo(&mut s); // mutable borrow, s is now "hello, world"

// however, we can only have one mutable reference for one variable at a time:
let r1 = &mut s;
let r2 = &mut s; // Error

// we also cannot use both mutable and immutable reference at a time:
let r1 = &s;
let r2 = &mut s; // Error if both r1 and r2 are used later:
println!("{}, {}", r1, r2);

let r1 = &s;
println!("{}", r1);
let r2 = &mut s; // OK
println!("{}", r2);

// rust can prevent dangling references:
fn dangle() -> &String { let s = String::from("hello"); &s }
let d = dangle(); // Error
rust

A special type of reference is slice, which only reference to a continuous part of a collection (e.g., String, array).

let s = String::from("hello world"); 
let hello: &str = &s[..5]; // a reference to "hello" (point to 0, record length 5)
let world = &s[6..];
let ss = &s[..]; // equals to `&s`

// example
// note we use parameter type &str, instead of &String.
// this utilizes "defef coercion".
fn first_word(s: &str) -> &str { 
	let bytes = s.as_bytes(); 
	for (i, &item) in bytes.iter().enumerate() { 
		if item == b' ' { 
			return &s[0..i]; 
		} 
	} 
	&s[..] 
}

let sl = "hello, world";
let s = String::from(sl);
let w = first_word(&s);
let w = first_word(&s[0..6]);
let w = first_word(sl);
let w = first_word(&sl); // slice of slice is still slice...
rust

Type to search.