Rust - Part 3

Written on
This article is for those who already have some experience with rust, and want to refresh the concepts.
All the articles from this series are condensed versions of Rust official book, this post covers Chapter 3 from the book.

 

3.1 Variables and Mutability:

Variables are immutable by default.  The following will throw an error during compile time.

fn main() {
    let x = 5;
    x = 6;
}

You can try the above by first creating a project with cargo new variables and run it with cargo run 

We can override immutability by adding the mut keyword

fn main() {
    let mut x = 5;
    x = 6;
}

Constants and immutable objects are different.  Constants require the datatype to be given, and constants can't take values of a function or anything, it should be statically given.

Example:

const MAX_POINTS: u32 = 100_000;

Shadowing:

We can declare multiple variables with the same name, like the following:

fn main() {
    let x = 5;
    let x = x + 1;
    let x = x * 2;
    println!("The value of x is: {}", x);
}

Note: Here x is not mutable, but we are re-assigning by using let command, or in rust terms, we are shadowing the first initialization.  Since it's reassiging we can change the data type.

 

3.2 Data Types

 

Rust is statically typed, it must know the type before execution time, during compile time itself.  For simple assignments, rust can automatically infer the variable type, but for some conversion, like string to number, since there can be multiple types of number data types, we should provide the data type, like we did in the guess game.

let guess: u32 = "42".parse().expect("Nan!");

Failing to provide type, we will get a compile time error.

Rust like many languages has two broad classifications - Scalar data types and Compound data types.  Think of scalar like primitive type in Java.

 

Scalar Types:

Rust has four primary scalar types:

 

Integer types:

Rust has 8 bit to 128 bit size integers, with declarations like i8, i16 up to i128, and for unsigned, it's u8, u16 till u128.

Apart from this, it has a special integer type arch, for signed we declare it as isize, and for unsigned, usize.  What it means is, in a 32bit system, 32bits will be assigned, and in 64 system, 64 bits will be assigned, arch stands for architecture.

Underscore can be used for visual separator such as 1_000 in decimal and 0b1111_0000 in binary.

Rust defaults to i32, and is generally the fastest, even in 64 bit systems.

Integer overflow will cause the program to panic in debug mode, but it will wrap around in release mode.  We shouldn't rely on integer overflow for program logic.  There are some methods like wrapping_*, checked_*, overflowing_* and saturating_* available in std lib, for utility.

 

Floating-point types:

Tow options, f32 and f64, and the default is f64, because on modern CPUs it's roughly the same speed as f32, but it's more precise. 

f32 uses single-precision float, and f64 double precision, the distinction is explained here.

 

Boolean:

Single byte, takes true or false, declared with bool.

 

Character:

Four bytes, represents an Unicode Scalar value.  Chinese, Japanese, Korean, accented letter, emoji and lot more can be assigned.  Like java, characters use single quotes, and strings use double quotes.

 

Compound Types:

Two basic compound types - tuples and arrays

Tuples:

Tuple - each element can be of different data type.

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
    let (x, y, z) = tup; //called destructuring
    let five_hundred = x.0; //can also be accessed with dot notation
    let six_point_four = x.1;
    let one = x.2;
}

 

Arrays:

All elements needs to be of the same data type.

fn main() {
    let a = [1, 2, 3, 4, 5];
    let a: [i32; 5] = [1, 2, 3, 4, 5]; //this is how we define data type
	let a = [3; 5]; // is equal to  [3, 3, 3, 3, 3]
    let first = a[0];
    let second = a[1]; 
    let random = a[10]; // will throw the following error
    //thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:19:19
	//note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
}

 

3.3 Functions:

 

Function keyword - fn

Default style - snake case - example - another_function

Rust doesn't care about the order you define functions, as long as it's defined somewhere.

main function is executed by default.

Function parameters:

TIL: Arguments: concrete values. Parameters: other variable, function returns as parameter.

fn main() {
    another_function(5, 6);
}

fn another_function(x: i32, y: i32) {
    println!("The value of x is: {}", x);
    println!("The value of y is: {}", y);
}

Function bodies:

Statements - Assignments usually, like an assertive sentence.

Expressions - Returns something, like invoking a function.

fn main() {
    let x = 5;
    let y = {
        let x = 3;
        x + 1 //Expressions should not have semicolons at the end.
    };
    println!("The value of y is: {}", y);
}

The block inside the method is an expression because it returns 4.

Function return values:

fn main() {
    let x = plus_one(5);
    println!("The value of x is: {}", x);
}
fn plus_one(x: i32) -> i32 {
    x + 1
}

Note, how the return type is mentioned with an arrow.

Like blocks, expressions should not have a semicolon at the end.  If we do have a semicolon, we'll have a compile time issue, saying mismatched types, what it means is it was expecting an i32 but was given () an empty tuple as result.

3.4 Comments:

Nothing really to look here, just use // if you wanna use comments, even for multi-line comments; more commenting options will be explored in the later part.

3.5 Control Flow

 

if expressions:

 

Rust doesn't automatically convert integers to boolean like Javascript, it needs boolean as return values.

fn main() {
    let number = 6;
    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

fn another_method() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {}", number);
}

All the if-arms should return the same type of value

let number = if condition { 5 } else { "six" }; //error, because it can't identify the type

 

Loops:

Three kinds of loops:

loop:

 

fn main() {
    loop {
        println!("again!");
    }
}
fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;
        }
    };
    println!("The result is {}", result); //will be 20
} 

break can be used to return values as well.

while:

fn main() {
    let mut number = 3;
    while number != 0 {
        println!("{}!", number);
        number -= 1;
    }
    println!("LIFTOFF!!!");
}

for:

 

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a.iter() {
        println!("the value is: {}", element);
    }
}

fn main() {
    for number in 1..10 {
        println!("Current number is: {}", number);
    }
    println!("gg");
}

fn another_method() {
    for number in (1..4).rev() {
        println!("{}!", number);
    }
    println!("LIFTOFF!!!");
}



Tags · Rust, Tech, Blog