Basics I

Let's go though the some basics of the basics of the Rust programming language.

This is just a fast introduction, so you don't worry if you don't understand everything: with the help of the additional materials in the "More Resources" section, things will fit together as it goes.

Main function

To compile the binary, your Rust programs must be wrapped around the fn main() {} function:

fn main() {
    println!("Hello world!");
}

But for simplicity, I will often hide the fn main() {} function on this page:

println!("Hello world!");

Variables

let x = "hello";
let y = "rustacean!";
println!("{x} {y}");
let x = "hello";
// we can also create a new variable called x
// (the old one loses scope)
let x = "hi";
println!("{x}");
let x = "hello";
x = "hi"; // ERROR! x cannot be changed!
println!("{x}");
let mut x = "hello"; // now x is mutable
x = "hi";
println!("{x}");
// now with numbers
let x = 2;
let x = 5;
println!("{x}");
// this also works
let x = 23;
let x = x + 3;
println!("{x}");
// or this
let mut x = 23;
x = x + 3;
println!("{x}");
// or even better
let mut x = 23;
x += 3;
println!("{x}");

Type declaration

Rust is a strong typed language but if you don't have to declare the type whenever the compiler is able to deduce it automatically (but you can).

let x: &str = "yow";
println!("{x}");
let x: String = "yow".to_owned();
println!("{x}");

// same thing
let x: String = "yow".to_string();
println!("{x}");

The difference between &str and String will be become clearer as we go, but for now, note that the methods to_owned and to_string converts a &str to a String.

// Integer of size 8 bytes (64 bits)
let mut x: i64 = 23;
x += 3;
println!("{x}");
println!("the size of i64 is {} bytes",
    std::mem::size_of::<i64>());
// Double precision float
let mut x: f64 = 23.0;
x += 3.0;
println!("{x}");
// Unsigned 16 bits integer
// You can add a suffix of the type
// to number instead of annotating the
// variable
let mut x = 23u16;
x += 3;
println!("{x}");

// Type casting to a
// single precision float
let mut x = x as f32;
x += 3.0;
println!("{x}");

Functions

// A function that takes a String
// type declaration of the function
// arguments is always mandatory
fn somefunc(mut x: String) {
    println!("{x}");
}

fn main() {
    let x = "Hello ".to_owned();
    somefunc(x);
}
// A function that takes a String and an &str
// and returns a String
// A function that takes a String
// type declaration of the function
// return is also always mandatory
fn somefunc(mut x: String, y: &str) -> String {
    x.push_str(y);
    x // function return x
}

fn main() {
    let x = "Hello ".to_owned();
    let y = "from someone";
    let z = somefunc(x, y);
    println!("{z}");
}

The last statement of a function which must end without a ; indicates that this the return of the function. You can also use the return statement to return it at any point:

fn somefunc(mut x: String) -> String {
    if x.starts_with("Hi") {
        return x;
    }
    x.push_str(" (checked)");
    x
}

fn main() {
    let x = "Hi there".to_owned();
    let x = somefunc(x);
    println!("{x}");

    let x = "Hello there".to_owned();
    let x = somefunc(x);
    println!("{x}");
}

The default return type of a function is the unit type (), so this

fn func() {
    println!("Hi");
}
fn main() {
    func()
}

is equivalent to this:

fn func() -> () {
    println!("Hi");
}
fn main() {
    func()
}

Ownership

Some data types in Rust are copy types, so when you assign a variable from another, the old variable is still valid: data is copied from a variable to another and the old variable is still valid and accessible:

let x = "hello"; // x is an &str type
let y = x;
let z = x;
let mut w = z;
w = "hi";
println!("{x} {y} {z} {w}");

The same applies if you pass it as a function argument:

fn print_the_var(mut x: i64) {
    x += 433;
    println!("{x}");
}
fn main() {
    let x = 344;
    print_the_var(x);
    println!("{x}"); // x is still accessible
}

However, for most type, like String, once you assign a variable, the old variable is no longer accessible, a move happens and the ownership passes to the new variable:

let x = "hello".to_owned(); // x is of String type
let y = x; // x gives ownership to y
println!("{x}"); // ERROR! x is not valid anymore

To make a copy, in this case you need an explicit clone

let x = "hello".to_owned(); // x is of String type
let y = x.clone(); // explicitly makes a clone of y
println!("{x}"); // x is still valid
println!("{y}");
let mut z = y.clone();
z.push_str(" friends");
println!("{y}");
println!("{z}");

Same thing also applies to passing variables as function arguments:

fn takes_ownership(x: String) {
    println!("{:?}", x);
}
fn main() {
    let x = "hello".to_owned(); // x is of String type
    takes_ownership(x);
    // ERROR: function took ownership of x
    println!("{:?}", x);
}

References

References are a way to point to a variable...

let x = "hello";
let y = &x; // y is a reference (points to x)
println!("{x}");
println!("{y}");
let x = 27;
let y = &x; // y is a reference (points to x)
println!("{x}");
println!("{y}");

// *y dereferences y, i.e.:
// gets the value of x
let z = *y + 4;
println!("{z}");
println!("{x}");
println!("{y}");

References are a way to point to a variable without making a copy...

let mut x = 25;
println!("{x}");

// y is a mutable reference, i.e.:
// points to x and can change it
let y = &mut x;

let z = *y + 4;
println!("{z}");

// the operator * can also be used to change x
*y += 1;
// notice that both x and y changed!
println!("{y}");
println!("{x}");

References are a way to point to a variable without making a copy and without losing ownership of it:

let mut x = "some String".to_owned();
println!("{x}");

let y = &mut x;
println!("{y}");
y.push_str(" with an extra");
println!("{y}");

println!("{x}");

Note that you cannot take a mutable reference from a variable not declared as mutable:

let x = 25;
let y = &mut x; // ERROR

References as function arguments

Functions can take variables by reference:

// Takes a string reference and display it
fn somefunc(x: &String) {
    println!("{x} from somefunc");
}

fn main() {
    let x = "Hello".to_owned();
    somefunc(&x);
    // since x was sent by reference
    // the function did not take ownership
    // of x, so x is still valid
    println!("{x} from main");
}

And with a mutable reference, you can change the variable value too:

// Takes a string reference and change it
fn somefunc(x: &mut String) {
    x.push_str("World!");
}

fn main() {
    let mut x = "Hello ".to_owned();
    println!("{x}");
    somefunc(&mut x);
    println!("{x}");
}

Vectors

Vectors are a nice way to store a varying number of elements (hence, heap allocated) of a single type.

let mut x: Vec<i64> = vec![1, 2, 3];
println!("print 1: {:?}", x);
x.push(4);
println!("print 2: {:?}", x);

// Removes the last element and returns it
println!("print 3: {}", x.pop().unwrap());

println!("print 4: {:?}", x);
while x.len() < 7 {
    x.push(0);
}
println!("print 5: {:?}", x);
println!("print 6: {}", x[1]);
let mut x = vec!["hi", "there"];
println!("print 1: {:?}", x);
x.push("to");
println!("print 2: {:?}", x);
println!("print 3: {}", x.pop().unwrap());
println!("print 4: {:?}", x);
while x.len() < 7 {
    x.push("you");
}
println!("print 5: {:?}", x);
println!("print 6: {}", x[1]);

Vec is also not a copy type, and so passing variables as function argument makes the function take ownership:

fn takes_ownership(v: Vec<i32>) {
    println!("{:?}", v);
}
fn main() {
    let x = vec![1, 2, 3, 4];
    takes_ownership(x);
    // ERROR: function took ownership of x
    println!("{:?}", x);
}

And again, this is not a problem if the function takes a reference:

fn does_not_take_ownership(v: &Vec<i32>) {
    println!("{:?}", v);
}
fn main() {
    let x = vec![1, 2, 3, 4];
    does_not_take_ownership(&x);
    println!("{:?}", x);
}

Arrays

Arrays allocate a sequence of elements of a single time. Contrary to vectors, the size of the array must be known at compiled time and they are stack allocated.

let mut x = [1, 2, 3]; // type [i32; 3]
println!("print 1: {}", x[1]);
println!("print 2: {:?}", x);
let mut x: [i32; 2] = [0, 1]; // with type annotation
x[1] = 50;
println!("print 3: {:?}", x);

Multiple references

You can have multiple constant references to a single variable at the same type, they don't create any conflict:

fn do_nothing(v: &Vec<i32>) {}

fn main() {
    let x = vec![1, 2, 3, 4]; // x is an &str type
    let y = &x;
    let z = &x;
    do_nothing(y);
    do_nothing(z);
    println!("done!");
}

The same cannot be said about mutable references though:

fn do_nothing(v: & mut Vec<i32>) {}

fn main() {
    let mut x = vec![1, 2, 3, 4]; // x is an &str type
    let y = & mut x;
    let z = & mut x; // ERROR!
    do_nothing(y);
    do_nothing(z);
    println!("done!");
}

This however is acceptable:

fn do_nothing(v: & mut Vec<i32>) {}

fn main() {
    let mut x = vec![1, 2, 3, 4]; // x is an &str type

    let y = & mut x;
    do_nothing(y);

    let z = & mut x;
    do_nothing(z);

    // uncomment the line bellow to cause an error
    // y;

    println!("{:?}", x);
}

The reason is that when second mutable reference to x is created, the first one is done (i.e.: we don't use it any more in our code).

Once you take a mutable reference, you cannot use the original variable either unless you stop using the reference, i.e.: this is not valid:

fn do_nothing(v: & mut Vec<i32>) {}

fn main() {
    let mut x = vec![1, 2, 3, 4]; // x is an &str type

    let y = & mut x;

    println!("{:?}", x);

    // ERROR
    // but will work if you move it above the println!
    do_nothing(y);
}

Tuples

Tuple are collections of values of different types with fixed size.

let x = (23, "Hello".to_owned());
println!("{:?}", x);

// Explicit type annotation
let x: (i32, String) = (23, "Hello".to_owned());
println!("{:?}", x);

// Destructuring
let (x1, x2) = x;
println!("x1: {}, x2: {}", x1, x2);

// Single element tuple
let x: (i32,) = (23,);
println!("{:?}", x);

Conditionals

Conditionals are straightforward in Rust:

let x: i64 = 2;
let y: i64 = 3;
if x.pow(2) >= 8 {
    println!("x to the power of 2 is greater than or equal to 8");
} else if y.pow(2) >= 8 {
    print!("x to the power of 2 less than 8 ");
    println!("but y to the power of 2 is greater than or equal to 8");
} else {
    println!("both x and y to the power of 2 are less than 8");
}
let x: i64 = 2;
let y: i64 = 3;
// conditionals can return a value
// and we can assign a variable to
// this value
let res = if x.pow(2) >= 8 {
    1
} else if y.pow(2) >= 8 {
    2
} else {
    3
};
println!("{res}");
fn somefunc(mut x: String) -> String {
    if x.starts_with("Hello") {
        x.push_str("World!");
    } else {
        x.push_str("Rust!");
    }
    x
}

fn main() {
    let x = "Hello ".to_owned();
    let y = somefunc(x);
    println!("{y}");

    let x = "Hi ".to_owned();
    let y = somefunc(x);
    println!("{y}");
}

Structs

Structs are fixed size collections of different types of variables:

struct SomeStruct {
    x: i32,
    y: String,
    z: f64,
}

fn main() {
    let s = SomeStruct {
        x: 1,
        y: "Hello".to_string(),
        z: 3.54,
    };
    println!("{} {} {}", s.x, s.y, s.z);

    // We declared s as immutable
    // but we can create a new s and move
    // ownership of s to it
    let mut s = s;
    s.x += 3;
    println!("{} {} {}", s.x, s.y, s.z);
}
struct SomeStruct {
    x: i32,
    y: String,
    z: f64,
}

fn main() {
    let mut s = SomeStruct {
        x: 1,
        y: "Hello".to_string(),
        z: 3.54,
    };

    let mut s = s;
    s.x += 3;
    println!("{} {} {}", s.x, s.y, s.z);
}
#[derive(Debug)] // enables printing
struct SomeStruct {
    x: i32,
    y: String,
    z: f64,
}

fn main() {
    let s = SomeStruct {
        x: 1,
        y: "Hello".to_string(),
        z: 3.54,
    };

    println!("{:?}", s);
}

Struct methods

We can define methods for structs which we can them call for its instances:

#[derive(Debug)]
struct SomeStruct {
    x: i32,
    y: String,
    z: f64,
}

impl SomeStruct {
    fn print_it(&self) {
        println!("{:?}", self);
    }

    fn print_x_plus_something(&self, v: i32) {
        println!("{:?}", self.x + v);
    }

    fn add_to_x(& mut self, v: i32) {
        self.x += v;
    }
}

fn main() {
    let mut s = SomeStruct {
        x: 1,
        y: "Hello".to_string(),
        z: 3.54,
    };

    s.print_it();
    s.print_x_plus_something(1000);
    s.add_to_x(120);
    s.print_it();
}

Loops

Here's some intuitive examples of loops:

for i in 0..10 {
    println!("Hello, world {i}");
}
let mut i = 0;
while i < 10 {
    println!("Hello, world {i}");
    i += 1;
}
let mut i = 0;
loop { // loops until it finds a break clause
    println!("Hello, world {i}");
    i += 1;
    if i >= 10 {
        break;
    }
}

If you found this project helpful, please consider making a donation.