Based on “The Rust Programming”

Variables and Mutability

  • Constant are like immutable varibles, but always immutable.
  • declare constants using the const keyword, and the type of the value must be annotated.
  • constants can be declared in any scope, including the global scope.
  • constants may be set only to a constant expression, not the result of a value that could only be computed at runtime.
  • Rust’s naming convention for constants is to use all uppercase with underscores between words, such as THREE_HOURS_IN_SECONDS
  • the compiler is able to evaluate a limited set of operations at compile time, which lets us choose to write out this value in a way that’s easier to understand and verify
  • Shadow is different from a variable marked as mut
    • will get a compile-time error if we accidentally try to reassign to this variable without using the let keyword.
    • shadowing is effectively creating a new variable when we use the let keyword again
  • we’re not allowed to mutate a variable’s type
  • the compiler can usually infer what type we want to use based on the value and how we use it. In cases when many types are possible, we must add a type annotation

Example Code:

fn main() {
    const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
    
    //mutable variable
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);

    let y = 5;

    let y = y + 1;

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

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

Data Types

  • Scalar types, represents a single value:

    • Interger:

      LengthSignedUnsigned
      8-biti8u8
      16-biti16u16
      32-biti32u32
      64-biti64u64
      128-biti128u128
      archisizeusize
      • Each signed variant can store numbers from -(2^(n - 1)) to 2^(n - 1) - 1 inclusive, where n is the number of bits that variant uses.
      • arch depend on the architecture of the computer your program is running on
      • you can write integer literals in any of the forms:
        Number literalsExample
        Decimal98_222
        Hex0xff
        Octal0o77
        Binary0b1111_0000
        Byte (u8 only)b’A'
      • number literals that can be multiple numeric types allow a type suffix, such as 57u8, to designate the type.
      • Number literals can also use _ as a visual separator to make the number easier to read, 1_100 is the same value as 1000
      • rust integer types default to i32
      • When compiling in release mode with the --release flag, Rust does not include checks for integer overflow that cause panics. Instead, if overflow occurs, Rust performs two’s complement wrapping. In short, values greater than the maximum value the type can hold “wrap around” to the minimum of the values the type can hold. In the case of a u8, the value 256 becomes 0, the value 257 becomes 1, and so on. The program won’t panic, but the variable will have a value that probably isn’t what you were expecting it to have.
    • Floating-Point:

      • f32 is 32 bits in size, single-precision float
      • f64 is 64 bits in size, double-precision float
      • rust floating-point types default to f64
      • floating-point numbers are represented according to the IEEE-754 standard
    • Boolean:

      • true or false
      • 1 byte in size
      • specified using bool keyword
    • Character:

      • specified using char keyword, with single quotes
      • 4 bytes in size, represents a Unicode Scalar Value, lot more than just ASCII, including Accented letters, CJK charaters, emoji, zero-with spaces.
      • Unicode range from U+0000 - U+D7FF, and U+E000 - U+10FFFF inclusive
  • Compound types, grouping multiple values into one type:

    • Tuple

      • a general way of grouping together a number of values with a variety of types into one compound type
      • fixed length, once declared, cannot grow or shrink in size,
        fn main() {
            let tup: (i32, f64, u8) = (500, 6.4, 1);
        }
        
      • the variable tup binds to the entire tuple
      • use pattern matching to destructure a tuple value
        fn main() {
            let tup = (500, 6.4, 1);
        
            let (x, y, z) = tup;
        
            println!("The value of y is: {}", y);
        }
        
      • we can also access a tuple element directly by using a period . followed by the index of the value we want to access.
        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;
        }
        
      • the tuple without any values (), is a special type that has only one value, also written (). The type is called the unit type and the value is called the unit value. Expressions implicitly return the unit value if they don’t return any other value.
    • Array

      • elements must have the same type.
      • length of the array is fixed in rust.
      • useful when you want your data allocated on the stack rather than the heap.
      • arrays are more useful when you know the number of elements will not need to change.
        // creating array
        let a = [1, 2, 3, 4, 5];
        
        // indicate type and size
        let a: [i32; 5] = [1, 2, 3, 4, 5];
        
        // initialize the same value for all elements
        let a = [3; 5];
        // is the same as
        let a = [3, 3, 3, 3, 3];
        
      • When trying to access element out of the bound, Rust’s memory safety principles will be in action, protects you against this kind of error by immediately exiting instead of allowing the memory access and continuing.

Functions

  • Rust code uses snake case as the conventional style for function and variable names.
  • Function can have parameters, you can provide it with concrete values(arguments) for those parameters.
  • In function signatures, you must declare the type of each parameter.
    fn main() {
        print_labeled_measurement(5, 'kg');
    }
    
    fn print_labeled_measurement(value: i32, unit_label: char) {
        println!("The measurement is: {}{}", value, unit_label);
    }
    
  • Rust is an expression-based language.
  • Statements are instructions that perform some action and do not return a value.
  • Expressions evaluate to a resulting value.
  • The following will fail at compile, because (let y = 6) does not return a value:
    fn main() {
        let x = (let y = 6);
    }
    
    Calling a function is an expression. Calling a macro is an expression. A new scope block created with curly brackets is an expression, for example:
    fn main() {
    let y = {
        let x = 3;
        x + 1
    };
    
    println!("The value of y is: {}", y);
    }
    
  • We don’t name return values, but we must declare their type after an arrow (->).
  • You can return early from a function by using the return keyword and specifying a value, but most functions return the last expression implicitly.
  • Example:
    fn five() -> i32 {
        5
    }
    
    fn main() {
        let x = five();
        println!("The value of x is: {}", x);
    }
    

Comments

  • In Rust, the idiomatic comment style starts a comment with two slashes, and the comment continues until the end of the line.
  • For multiline comments , you’ll need to include // on each line.
    // This is a comment
    // A very very very very very very very long comment
    
  • Rust also has another kind of comment, documentation comments.

Control Flow

  • if expression

    • Blocks of code associated with the conditions in if expressions are sometimes called arms, just like the arms in match expressions.
    • Using too many else if expressions can clutter your code, so if you have more than one, you might want to refactor your code, a powerful Rust branching construct called match for these cases.
    • Because if is an expression, we can use it on the right side of a let statement to assign the outcome to a variable, example:
      fn main() {
          let condition = true;
          let number = if condition { 5 } else { 6 };
      
          println!("The value of number is: {}", number);
      }
      
      it will output The value of number is: 5.
    • But if in the following example, we’ll get an error:
      fn main() {
          let condition = true;
      
          let number = if condition { 5 } else { "six" };
      
          println!("The value of number is: {}", number);
      }
      
      This won’t work because variables must have a single type, and Rust needs to know at compile time what type the number variable is, definitively.
    • The the values that have the potential to be results from each arm of the if must be the same type.
  • Repetition with Loops

    • loop

      • The loop keyword tells Rust to execute a block of code over and over again forever or until you explicitly tell it to stop.
      • Place the break keyword within the loop to tell the program when to stop executing the loop.
      • Use continue in a loop tells the program to skip over any remaining code in this iteration of the loop and go to the next iteration.
      • If you have loops within loops, break and continue apply to the innermost loop at that point.
      • To pass the result of that operation out of the loop to the rest of your code, add the value you want returned after the break expression you use to stop the loop; that value will be returned out of the loop so you can use it, example:
        fn main() {
            let mut counter = 0;
        
            let result = loop {
                counter += 1;
        
                if counter == 10 {
                    break counter * 2;
                }
            };
        
            println!("The result is {}", result);
        }
        
        this will output The result is 20.
    • while

      • Using a while loop to run code while a condition holds true:
        fn main() {
            let mut number = 3;
        
            while number != 0 {
                println!("{}!", number);
        
                number -= 1;
            }
        
            println!("START!!");
        }
        
    • for

      • use a for loop and execute some code for each item in a collection.
      • In situations in which you want to run some code a certain number of times, most Rustaceans would use a for loop.
      • The way to do that would be to use a Range, provided by the standard library, which generates all numbers in sequence starting from one number and ending before another number.
      • rev, to reverse the range
        fn main() {
            for number in (1..4).rev() {
                println!("{}!", number);
            }
            println!("LIFTOFF!!!");
        }
        
    • Example Code:

      fn main() {
          let mut count = 0;
      
          //The outer loop has the label 'counting_up, and it will count up from 0 to 2. 
          'counting_up: loop {
              println!("count = {}", count);
              let mut remaining = 10;
      
              // The inner loop without a label counts down from 10 to 9.
              loop {
                  println!("remaining = {}", remaining);
                  if remaining == 9 {
                      break; //The first break that doesn’t specify a label will exit the inner loop only.
                  }
                  if count == 2 {
                      break 'counting_up; // The break 'counting_up; statement will exit the outer loop. This code prints:
                  }
                  remaining -= 1;
              }
      
              count += 1;
          }
          println!("End count = {}", count);
      }