Install Rust

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


Hello World Example


vi main.rs


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


Compile

rustc ./main.rs


Run

./main
Hello, world!


Using the Package Manager

Create a project using Cargo

cargo new hello_cargo
    Creating binary (application) `hello_cargo` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

This command creates a folder with the name of the new project containing a src directory and the dependency file called Cargo.toml.


hello_cargo
├── .git
│   ├── HEAD
│   ├── config
│   ├── description
│   ├── hooks
│   │   └── README.sample
│   ├── info
│   │   └── exclude
│   ├── objects
│   │   ├── info
│   │   └── pack
│   └── refs
│       ├── heads
│       └── tags
├── .gitignore
├── Cargo.toml
└── src
    └── main.rs

It has also initialized a new Git repository along with a .gitignore file. Git files won’t be generated if you run cargo new within an existing Git repository; you can override this behavior by using cargo new --vcs=git.


Cargo.toml

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

[dependencies]                


Building

cargo build


Build and Run

cargo run

Build for Release 

cargo build --release

This command will create an executable in target/release instead of target/debug. The optimizations make your Rust code run faster, but turning them on lengthens the time it takes for your program to compile. This is why there are two different profiles: one for development, when you want to rebuild quickly and often, and another for building the final program you’ll give to a user that won’t be rebuilt repeatedly and that will run as fast as possible. If you’re benchmarking your code’s running time, be sure to run cargo build --release and benchmark with the executable in target/release.


Naming Conventions

Rust code uses snake case as the conventional style for function and variable names, in which all letters are lowercase and underscores separate words.

Constants are all uppercase.

const PI: f32 = 3.14;

fn main() {
	let my_var = 5;
    another_function(my_var);
}

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


Variables


Immutable/Mutable

Variables by default are immutable, meaning they can't be changed once set.

fn main() {
    let x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

The above code would generate an error since we are trying to overwrite the value of x.


We can make the variable mutable by adding mut the initial assignment:

fn main() {
    let mut x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

The above code compiles.


Constants

You declare constants using the const keyword instead of the let keyword, and the type of the value must be annotated.

Constants can be declared in any scope, including the global scope, which makes them useful for values that many parts of code need to know about.

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3; 
const PI: f32 = 3.14;

Rust’s naming convention for constants is to use all uppercase with underscores between words.


Shadowing

You can declare a new variable with the same name as a previous variable. Rustaceans say that the first variable is shadowed by the second, which means that the second variable is what the compiler will see when you use the name of the variable.

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

    println!("The value of x is: {x}");
}
$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6


Data Types

Integer Types

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize


Number Literals

Number literals can also use _ as a visual separator to make the number easier to read, such as 1_000, which will have the same value as if you had specified 1000.

Number literalsExample
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte (u8 only)b'A'


Floating-Point Types

Rust’s floating-point types are f32 and f64, which are 32 bits and 64 bits in size, respectively. The default type is f64 because on modern CPUs, it’s roughly the same speed as f32 but is capable of more precision. All floating-point types are signed.


fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}


Boolean Type

Booleans are one byte in size. The Boolean type in Rust is specified using bool.

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}


Character Type

Rust’s char type is the language’s most primitive alphabetic type.

Note that we specify char literals with single quotes, as opposed to string literals, which use double quotes.

fn main() {
    let c = 'z';
    let z: char = 'ℤ'; // with explicit type annotation
    let heart_eyed_cat = '😻';
}


Compound Types

Compound types can group multiple values into one type. Rust has two primitive compound types: tuples and arrays.

Tuple Type

A tuple is a general way of grouping together a number of values with a variety of types into one compound type. Tuples have a fixed length: once declared, they cannot grow or shrink in size.


fn main() {
    let tup = (500, 6.4, 1);
    let (x, y, z) = tup;
    println!("The value of y is: {y}");
}

This program first creates a tuple and binds it to the variable tup. It then uses a pattern with let to take tup and turn it into three separate variables, x, y, and z. This is called destructuring because it breaks the single tuple into three parts. Finally, the program prints the value of y, which is 6.4.


We can also access a tuple element directly by using a period (.) followed by the index of the value we want to access. For example:

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);
    let five_hundred = x.0;
    let six_point_four = x.1;
    let one = x.2;
}


Array Type

Unlike a tuple, every element of an array must have the same type. Unlike arrays in some other languages, arrays in Rust have a fixed length.

fn main() {
	let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];

	let a: [i32; 5] = [1, 2, 3, 4, 5];

	//access values

	let first = a[0];
	let second = a[1];
}


Functions

Rust code uses snake case as the conventional style for function and variable names, in which all letters are lowercase and underscores separate words

fn main() {
    another_function(5);
}

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


Returning Values

fn main() {
    let x = plus_one(5);

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

fn plus_one(x: i32) -> i32 {
    x + 1
}

The value returned from function plus_one is the value of x+1 or 6. Notice that there is no semi-colon at the end of the line. This is an expression.


Comments

Rust uses two forward slashes ( // ) to indicate a comment. 

For comments that extend beyond a single line, you’ll need to include // on each line.

Comments can also be placed at the end of lines containing code

// So we’re doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what’s going on.

fn main() {
    let lucky_number = 7; // I’m feeling lucky today
}


Control Flow

If/Else

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");
    }
}


Using if in a let Statement

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

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


Loops

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {result}");
}


While

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{number}!");

        number -= 1;
    }

    println!("LIFTOFF!!!");
}


For

Looping Through a Collection with for

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

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


Here’s what the countdown would look like using a for loop and another method we’ve not yet talked about, rev, to reverse the range:

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


Ownership

When you assign a variable to another or pass a variable to a function, the receiver takes ownership and also becomes responsible for dropping he memory when out of scope


See https://doc.rust-lang.org/book/ch04-01-what-is-ownership.html


Example1

fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // x would move into the function,
                                    // but i32 is Copy, so it's okay to still
                                    // use x afterward

} // Here, x goes out of scope, then s. But because s's value was moved, nothing
  // special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{some_string}");
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{some_integer}");
} // Here, some_integer goes out of scope. Nothing special happens.


Example2

fn main() {
    let s1 = gives_ownership();         // gives_ownership moves its return
                                        // value into s1

    let s2 = String::from("hello");     // s2 comes into scope

    let s3 = takes_and_gives_back(s2);  // s2 is moved into
                                        // takes_and_gives_back, which also
                                        // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
  // happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {             // gives_ownership will move its
                                             // return value into the function
                                             // that calls it

    let some_string = String::from("yours"); // some_string comes into scope

    some_string                              // some_string is returned and
                                             // moves out to the calling
                                             // function
}

// This function takes a String and returns one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
                                                      // scope

    a_string  // a_string is returned and moves out to the calling function
}


References

We can send a variable by reference by pre-pending with an ampersand. This way, the ownership is not transfered.

Note: The opposite of referencing by using & is dereferencing, which is accomplished with the dereference operator, *. 


fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
  // it refers to, it is not dropped.


So, what happens if we try to modify something we’re borrowing? Spoiler alert: it doesn’t work!


Mutable Reference

We can allow the reference to be changed by adding the mutable (mut) keyword.

fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}


Mutable references have one big restriction: if you have a mutable reference to a value, you can have no other references to that value. 

The following code would generate an error:

    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);


Slices

String Slices

A string slice is a reference to part of a String, and it looks like this:

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];


With Rust’s .. range syntax, if you want to start at index 0, you can drop the value before the two periods.

By the same token, if your slice includes the last byte of the String, you can drop the trailing number. 

You can also drop both values to take a slice of the entire string.

let s = String::from("hello world");

let hello = &s[..5];
let world = &s[6..];

let sentence = &s[..];
let sentence = &s[0..len];


Structs

 In a struct you’ll name each piece of data so it’s clear what the values mean. Adding these names means that structs are more flexible than tuples: you don’t have to rely on the order of the data to specify or access the values of an instance.


struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };

    user1.email = String::from("anotheremail@example.com");
}


Creating Instances from Other Instances with Struct Update Syntax

fn main() {
    // --snip--

    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
    };
}


The syntax .. specifies that the remaining fields not explicitly set should have the same value as the fields in the given instance.

fn main() {
    // --snip--

    let user2 = User {
        email: String::from("another@example.com"),
        ..user1
    };
}


Defining Methods

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}


Enums

enum IpAddrKind {
    V4,
    V6,
}

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
enum IpAddrKind {
	V4,
	V6,
}

struct IpAddr {
	kind: IpAddrKind,
	address: String,
}

let home = IpAddr {
	kind: IpAddrKind::V4,
	address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
	kind: IpAddrKind::V6,
	address: String::from("::1"),
};




IntelliJ IDE

To use Rust in IntelliJ install the following plugins:

  • Rust
  • Native Debugging Support


References

  • No labels