Guide to Rust language (for beginners)

Published on

Rust has become more popular in recent years. Here is a guide for newcomers. It doesn't cover everything in detail, but is intended to be an overview for those who know other languages but are new to Rust. After reading through this guide you should be familiar with most concepts in Rust and be able to read and understand code.

Table of Contents

Variables

  • Declare variables with let (and less often const and static).
  • You will mostly use let
fn example() {
  let count = 1;
  let count2 = count + 1;
  println!("counted: {}", count2);
}
  • Variables are blocked scoped. (Similar to JS, PHP etc)

const / static

  • Less often used.
  • You must define the type for these (with let you can normally let Rust infer the type automatically).
  • const is for variables who's values do not change. When these vars are used, the value itself is replaced there.
  • static can be more like a global variable. It cannot be changed. When these vars are used, they have a fixed memory address which is used.
const UNLUCKY_NUMBER: u64 = 32;
static SOME_COUNTRIES: [&str; 3] = ["USA", "Canada", "Mexico"];

Note: there is a lot of complicated topics about how long variables are in memory. See section further down.

Types

Primitive types

integers

  • Two types of integers in rust. Signed (can be negative or positive) and unsigned.

  • Unsigned: u8, u16, u32, u64, u128 and usize

  • Signed: i8, i16, i32, i128 and usize

  • i8 = 8 bits = 1 byte.

  • usize and isize depend on the architecture of your computer. isize/usize on a 64 bit computer = i64 or u64

You can use underscore to make reading numbers easier. All of the following numbers are equivalent.

fn example() {
  let num1: u256 = 1234;
  let num2: u256 = 1_234;
  let num3: u256 = 1___2_3_4;
}

You can write types at the end of the number to avoid having to use : uXXX when declaring the var. Both of the following examples are equivalent.

fn example() {
  let num1 = 1234u256;
  let num2 = 1_234_u256;
}

Floats - f32 and f64

  • Floats = numbers with decimal
  • Two types, f32 and f64. By default Rust will use f64 unless you specify otherwise.
  • Turn an int into a float by adding a decimal
  let an_integer: 10;
  let a_float: 10.;
  let another_float: 10.0;

How to work with different types of floats

fn example() {
  let float1: f64 = 20.;
  let float2: f32 = 10.;
  
  let result = float1 + float2 as f64;
}

Strings & characters

Characters

  • Characters are a bit easier than full strings, so lets start with these.
  • Characters are called char in rust
  • Chars are unicode characters. a is a character, but also 😊 is a char

Example code setting chars -


fn example() {
  let letter_a = 'A';
  let unicode = 'Ƣ';
  let emoji = '😊';
}

Note: in terms of the size of the data, letter_a size in memory (1 byte) will be smaller than the emoji variable (4 bytes).

Strings

  • Two types of strings:
  • String (pointer, has functions on it)
  • &str (more simple, faster)
  • Both types are UTF-8.
&str
  • dynamically sized - you can change the size
String
  • How to create:
let s1 = String::from("use double quotes here");
let s2 = "Ben".to_string();
let s3 = format!("Hello {}", s2); 

Enums in Rust

  • enums are a type that has one value (out of a few specific options)
  • enums in rust can take arguments, which is something I've not seen in other languages
enum Size {
 Small,
 Medium,
 Large,
}

fn pickASize() -> Size {
  return Size.Medium;
}
 

Structs

  • Structs are a way of holding data, and having functions (methods) on those objects.
  • You define the shape of the struct, and then optionally can add functions to that struct.
struct Position(f64, f64, f64);

fn example() {
  let where_am_i = Struct(12.45, 30.9, 2.);
  println!("You are at position x: {}", where_am_i.0);
  println!("You are at position y: {}", where_am_i.1);
  println!("You are at position z: {}", where_am_i.2);
}
  • Structs can have keys:
struct Player {
  name: &str,
  height: f64,
}
  • Structs can contain other structs:
struct Player {
  name: &str,
  height: f64,
  pos: Position, // < struct in a struct
}

Adding methods (functions) to a struct in Rust language

  • Use impl to add a method to a struct
  • You can use &self to access the data on a struct (like this in PHP/JS)
struct Position {
  x: f64,
  y: f64,
  z: f64,
}

impl Position {
  fn moveX(&self) {
    self.x = 123.45;
  }
}

Casting numbers to chars

TODO

Type inference

TODO

Casting between different types

TODO

Outputting data

Easiest way to output is with the println!() macro.

fn main() {
  println!("hello world");

  let some_var = 123;
  println("hello {}:, some_var);
}

Comments in Rust

You can add comments like in many C based languages

// this is a comment
/* so is this */

let a_variable = true; // here is a comment

Mutating data

  • by default vars are immutable.
fn example() {
  let count1 = 1; // cannot change this!
  let mut count2 = 1; // mutable - can change
  count2++;
}

Shadowing vars

In some languages like JS you cannot redeclare a variable in the same scope. In Rust you can. This is typical rust code:

fn example() {
  let age = 64;
  let age = 70;
  println("{}", age); // outputs 70
}

You can even change the type of the variable (age in this case) every time you redeclare it with let.

Matching / flow control

  • There is no switch/case in Rust like in many other languages.
  • But there is match which is similar.
fn main() {
  let what_char = 'a';
  match what_char {
    'a' => println!("first letter"),
    'z' => println!("last letter"),
    _ => println!("another letter"),

  }
}
  • Matches must cover all options (which for char would be very large/impossible to write- but for things such as a enum it is possible) or include a default case _.

Match shortform

You can also write them inline and assign a variable to the matched output:

fn main() {
    let points = 5;
    let group = match points {
        0 => 1,
        1 => 5,
        _ => 10,
    };
}

References and ownership in Rust

Creating a reference in Rust

  • Use & to create a ref.
fn example() {
  let name = String::from("Jack");
  let name_ref = &name;
  println!("Hello {}", name_ref); // outputs Hello Jack
}

Mutable references

Just like normal variables can have the mut keyword, you can also do this for references. Here I will show an example with changing a variable via its (mutable) ref. It also shows dereferencing with the *

fn example() {
  let mut age = 64;
  let age_ref = &mut age;
   
  *age_ref++;

  println!("{}", age);
}

Mutable reference rules

  • You can have as many immutable references for a var as you want - as long as they're all immutable
  • or you can have example 1 mutable reference. (you cannot have an immutable and mutable ref for the same variable).

Ownership

Ownership rules for copy types

  • Copy types are simple types, of small size.
  • As they are small, and the compiler knows their size they do not need ownership
  • copy types = integers, floats, booleans, char (characters)
  • no need to think about ownership for these. They are copied every time they're used. they stay on the stack. quick, easy and small.
  • Note: char is a copy type, but &str and String are not.

Ownership rules for all other types

For all other types, we must think about ownership.

A concept in Rust is ownership. A value is owned only by one owner.

As soon as you use a value (e.g. to pass it to a function), the ownership is removed (and given to something else).

This will not work:

fn say_hi(name: String) {
  println!("Hi, {}", name);
}

fn main() {
  let who = String::from("Micky"); // note: this is not a `copy type`! its a String
  say_hi(who); // outputs "Hi, Micky"
  say_hi(who); // fails <<
}

This fails as the first call to say_hi() passes who to the say_hi function, which is now the owner.

Most of the time you will fix this by passing references:

fn say_hi(name: &String) { // note: &String this time!
  println("Hi, {}", name);
}

fn main() {
  let who = String::from("Donald");
  say_hi(&who);
  say_hi(&who);
}

this time as the reference (and not value itself) was passed in, the owner is still around after the first call to say_hi so the second call also works.

You can also return name in say_hi(), and use it again like this:

fn say_hi(name: String) { // back to just String
 println!("hi {}", name);
 return name;
}

fn main() {
 let name = String::from("Scrooge");
 let name = say_hi(name);
 say_hi(name);
}

Uninitialized variables

  • These are vars that have no value. These cannot be used in Rust.
  • In other languages you can often declare a var (without setting value), and then set the value later on.

Example

fn example() {
let magic_number = if (something > 2) { 
  400
 } else {
  0
 };
}

Lists, arrays, collections in Rust

  • Lots of types exist in Rust to do things that other languages might call arrays, lists or collections.

Arrays

  • Arrays in Rust are very fast
  • Arrays are of fixed size
  • Arrays in Rust are defined with square brackets []. You first define the type, then how many in the array. Example: [&str; 5] for an array of 5 &str strings.
  • Arrays contain the same type of data

Create an array of a number of same items

  • If you wanted to create a new array in Rust with 5 items in it, all with the same value of "x" then you can do this: let five_xs = ["x": 5]. It is more common to use it to initalise an array with empty data (e.g. let mut some_buffer = [0, 200])

Accessing data in arrays

  • Use square brackets or dot notation
  • They are zero indexed - 0 item on an array if the first item
fn example() {
  let top_letters = ["a", "b", "c"];
  println!("{}", top_letters.0); // 'a'

  let num = 0;
  println!("{}", top_letters[num]); // 'a'
}

Create mutable arrays

  • You cannot change the size of arrays.
  • But use the mut keyword to make their contents mutable.
let mut fav_letters = ["a", "b", "c"];
fav_letters[

Vectors

  • Vectors are like arrays
  • they are slower than arrays
  • The size of vectors are dynamic
  • Vectors allocate data onm the heap

Creating vectors

Can create vectors with the vec! macro:

fn example() {
  let nums = vec![1, 2, 6];  // Vec<i32>
}

Adding items to a vector in rust

Use the .push() method.


fn example() {
  let mut names = Vec::new();

   names.push(String::from("Harry"));
   names.push(String::from("Ron"));
}

More helper functions on vectors

fn example() {
   let mut names = Vec::new();
   println!("{}", names.capacity()); // 0

   // after adding items, capacity increases (to 4 then doubles)
   names.push("Micky");
   println!("{}", names.capacity()); // 4;
   println!("{}", names.len()); // 1 - as there is one item in the vector
}

If we already know the expected capacity, we can assign it at initialisation of the vector for a slight performance increase:

fn example() {
  let mut names = Vec::with_capacity(4);
  // ...
}

Slices

Slices is a reference to an rray.

fn example() {
  let array_of_nums = [1, 2, 3, 4, 5, 6];
  let slice_ref = &array_of_nums = &array_of_nums[1..3];
  
  for num in slice_ref.iter() {
    println!("{}", num);
  }
}

Tuples

  • Arrays, vectors and slices are always lists of the same type.
  • Tuples can be a fixed amount of different types.
  • Tuples in rust are defined with ( and )
fn example() {
  let some_result = ("something", true, 200);

// access with dot notation:
   println!("the name: {:?}", some_result.0);
   println!("the bool: {:?}", some_result.1);
   println!("the number: {:?}", some_result.2);

// same as accessing with square brackets:
   println!("the name: {:?}", some_result[0]);
   println!("the bool: {:?}", some_result[1]);
   println!("the number: {:?}", some_result[2]);
}

Mutating data in functions

fn main() {
 let name = String::from("Micky");
 add_last_name(name);
}

fn add_last_name(mut name: String) {
  // `add_last_name` owns `name` now - it wasn't passed by reference
  name.push_str(" Mouse");
  println!("{}", name);
}