Rust Notes


Variables

We decrare variable using let keyword.

let x = 2;

by default variables are immutable.

let  x = 2;
println!("{}", x);
x = 4; // error!

you can make variable mutable by adding mut in front of the variable name

let mut x = 2;
x = 4; // ok

Adding mut also conveys intent to future readers of the code by indicating that other parts of the code will be changing this variable’s value.

you can declare a new variable with the same name as a previous variable. We can 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.

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

 

Data Types

Every value in Rust is of a certain data type, which tells Rust what kind of data is being specified so it knows how to work with that data. There are two data type subsets: scalar and compound.

A scalar type represents a single value. Rust has four primary scalar types: integers, floating-point numbers, Booleans, and characters.

Integer types: i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize

the isize and usize types depend on the architecture of the computer your program is running on.

Rust’s floating-point types are f32 and f64, which are 32 bits and 64 bits in size, respectively. The default type is f64. All floating-point types are signed.

Boolean type in Rust has two possible values: true and false. Booleans are one byte in size. The Boolean type in Rust is specified using bool.

Rust’s char type is the language’s most primitive alphabetic type. we specify char literals with single quotes. Rust’s char type is four bytes in size and represents a Unicode Scalar Value, which means it can represent a lot more than just ASCII. Accented letters; Chinese, Japanese, and Korean characters; emoji; and zero-width spaces are all valid char values in Rust.

Data type is specified after the variable name with colon.

let a = 4;
let b: i32 = 2;
let c = 2.0; // f64
let d: f32 = 3.0; // f32
let e = true;
let f: bool = false;
let g = 'z';
let z: char = 'z';
let heart_eyed_cat = '😻';

constants are values that are bound to a name and are not allowed to change. you aren’t allowed to use mut with constants. Constants are immutable by default and they’re always immutable. You declare constants using the const keyword instead of the let keyword, and the type of the value must be annotated. 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.

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

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

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. We create a tuple by writing a comma-separated list of values inside parentheses. Each position in the tuple has a type, and the types of the different values in the tuple don’t have to be the same.

let tup1 = (42, "Hello", 2.4);
let tup2: (i32, f64, u8) = (500, 6.4, 1);

To get the individual values out of a tuple, we can use pattern matching to destructure a tuple value.

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. We can also access a tuple element directly by using a period (.) followed by the index of the value we want to access.

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 has a special name, unit. This value and its corresponding type are both written () and represent an empty value or an empty return type.

Another way to have a collection of multiple values is with an array. Unlike a tuple, every element of an array must have the same type. arrays in Rust have a fixed length. We write the values in an array as a comma-separated list inside square brackets.

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

Arrays are useful when you want your data allocated on the stack rather than the heap or when you want to ensure you always have a fixed number of elements.

You write an array’s type using square brackets with the type of each element, a semicolon, and then the number of elements in the array.

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

Here, i32 is the type of each element. After the semicolon, the number 5 indicates the array contains five elements.

You can also initialize an array to contain the same value for each element by specifying the initial value, followed by a semicolon, and then the length of the array in square brackets

let a = [0; 5];

The array named a will contain 5 elements that will all be set to the value 0 initially.

An array is a single chunk of memory of a known, fixed size that can be allocated on the stack. So, you can access elements of an array using indexing.

let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];

 

Control Flow

if expression allows you to branch your code depending on conditions. You provide a condition and then state, "If this condition is met, run this block of code. If the condition is not met, do not run this block of code.

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

condition of if must be a bool. Rust will not automatically try to convert non-Boolean types to a Boolean.

Because if is an expression, we can use it on the right side of a let statement to assign the outcome to a variable.

let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");

Blocks of code associated with the conditions in if expressions are sometimes called arms. values that have the potential to be results from each arm of the if must be the same type.

let number = if condition { 5 } else { "six" }; // error

 

Loops

Rust has three kinds of loops: loop, while, and for.

The loop keyword tells Rust to execute a block of code over and over again forever or until you explicitly tell it to stop. Rust also provides a way to break out of a loop using code. You can place the break keyword within the loop to tell the program when to stop executing the loop. One of the uses of a loop is to retry an operation you know might fail, such as checking whether a thread has completed its job. You might also need to pass the result of that operation out of the loop to the rest of your code. To do this, you can 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.

let mut counter = 0;
let result = loop {
    counter += 1;
    if counter == 10 {
       break counter * 2;
    }
};
println!("The result is {result}");

If you have loops within loops, break and continue apply to the innermost loop at that point. You can optionally specify a loop label on a loop that we can then use with break or continue to specify that those keywords apply to the labeled loop instead of the innermost loop. Loop labels must begin with a single quote.

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;
        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }
        count += 1;
    }
    println!("End count = {count}");
}

The outer loop has the label 'counting_up, and it will count up from 0 to 2. The inner loop without a label counts down from 10 to 9. The first break that doesn’t specify a label will exit the inner loop only. The break 'counting_up; statement will exit the outer loop.

A program will often need to evaluate a condition within a loop. While the condition is true, the loop runs. When the condition ceases to be true, the program calls break, stopping the loop. Rust has a built-in language construct for it, called a while loop. While a condition holds true, the code runs; otherwise, it exits the loop.

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

You can choose to use the while construct to loop over the elements of a collection, such as an array.

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;
    while index < 5 {
        println!("the value is: {}", a[index]);
        index += 1;
    }
}

However, this approach is error prone; if you changed the definition of the a array to have four elements but forgot to update the condition to while index < 4, the code would panic. It’s also slow, because the compiler adds runtime code to perform the conditional check of whether the index is within the bounds of the array on every iteration through the loop.

As a more concise alternative, you can use a for loop and execute some code for each item in a collection.

fn main() {
    let a = [10, 20, 30, 40, 50];
    for element in a {
        println!("the value is: {element}");
    }
}

we’ve now increased the safety of the code and eliminated the chance of bugs that might result from going beyond the end of the array or not going far enough and missing some items. Using the for loop, you wouldn’t need to remember to change any other code if you changed the number of values in the array. The safety and conciseness of for loops make them the most commonly used loop construct in Rust.

 

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.

We define a function in Rust by entering fn followed by a function name and a set of parentheses. The curly brackets tell the compiler where the function body begins and ends. In function signatures, you must declare the type of each parameter. When defining multiple parameters, separate the parameter declarations with commas.

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

fn another_function() {
    println!("Another function.");
}

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

fn another_function3(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

Statements are instructions that perform some action and do not return a value. Expressions evaluate to a resulting value.

Expressions do not include ending semicolons. If you add a semicolon to the end of an expression, you turn it into a statement, and it will then not return a value.

Functions can return values to the code that calls them. We don’t name return values, but we must declare their type after an arrow (->). In Rust, the return value of the function is synonymous with the value of the final expression in the block of the body of a function. You can return early from a function by using the return keyword and specifying a value, but most functions return the last expression implicitly.

fn five() -> i32 {
    5
}

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

body of the five function is a lonely 5 with no semicolon because it’s an expression whose value we want to return. if you add a semicolon, 5 becomes statement. statements don’t evaluate to a value. Therefore, nothing is returned, which contradicts the function definition and results in an error. Alternatively, you can use return 5;

 

Ownership

Ownership is Rust’s most unique feature and has deep implications for the rest of the language. It enables Rust to make memory safety guarantees without needing a garbage collector. Ownership is a set of rules that governs how a Rust program manages memory. Some languages have garbage collection that regularly looks for no-longer used memory as the program runs; in other languages, the programmer must explicitly allocate and free the memory. Rust uses a third approach: memory is managed through a system of ownership with a set of rules that the compiler checks. If any of the rules are violated, the program won’t compile. None of the features of ownership will slow down your program while it’s running.

let’s take a look at the ownership rules.

As a first example of ownership, we’ll look at the scope of some variables. A scope is the range within a program for which an item is valid.

{                      // s is not valid here, it’s not yet declared
   let s = "hello";   // s is valid from this point forward

   // do stuff with s
}                      // this scope is now over, and s is no longer valid

To illustrate the rules of ownership, we need a data type that is more complex than simpler ones. we want to look at data that is stored on the heap and explore how Rust knows when to clean up that data, and the String type is a great example. We’ll concentrate on the parts of String that relate to ownership. These aspects also apply to other complex data types, whether they are provided by the standard library or created by you.

We’ve already seen string literals, where a string value is hardcoded into our program. String literals are convenient, but they aren’t suitable for every situation in which we may want to use text. One reason is that they’re immutable. Another is that not every string value can be known when we write our code: for example, what if we want to take user input and store it? For these situations, Rust has a second string type, String. This type manages data allocated on the heap and as such is able to store an amount of text that is unknown to us at compile time. You can create a String from a string literal using the from function

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

The double colon :: operator allows us to namespace this particular from function under the String type rather than using some sort of name like string_from.

let mut s = String::from("hello");
s.push_str(", world!"); // push_str() appends a literal to a String
println!("{}", s); // This will print `hello, world!`

Multiple variables can interact with the same data in different ways in Rust. Let’s look at an example:

let x = 5;
let y = x;

We can probably guess what this is doing: “bind the value 5 to x; then make a copy of the value in x and bind it to y.” We now have two variables, x and y, and both equal 5. integers are simple values with a known, fixed size, and these two 5 values are pushed onto the stack.

let s1 = String::from("hello");
let s2 = s1;

we might assume that above code works would be the same. The second line would make a copy of the value in s1 and bind it to s2. But this isn’t quite what happens.

A String is made up of three parts. A pointer to the heap memory that holds the contents of the string, a length, and a capacity. This group of data is stored on the stack. When we assign s1 to s2, the String stack data is copied, meaning we copy the pointer, the length, and the capacity that are on the stack. We do not copy the data on the heap that the pointer refers to.

Earlier, we said that when a variable goes out of scope, Rust automatically cleans up the heap memory for that variable. Since s1 and s2 both refer to the same heap location, when s2 and s1 go out of scope, they will both try to free the same memory. This is known as a double free error and freeing memory twice can lead to memory corruption, which can potentially lead to security vulnerabilities.

To ensure memory safety, after the line let s2 = s1, Rust considers s1 as no longer valid. Therefore, Rust doesn’t need to free anything when s1 goes out of scope. when you try to use s1 after s2 is assigned; it won’t work:

let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1); // error

If you’ve heard the terms shallow copy and deep copy while working with other languages, the concept of copying the pointer, length, and capacity without copying the data probably sounds like making a shallow copy. But because Rust also invalidates the first variable, instead of calling it a shallow copy, it’s known as a move. In this example, we would say that s1 was moved into s2.

Moving solves our problem! With only s2 valid, when it goes out of scope, it alone will free the memory.

If we do want to deeply copy the heap data of the String, not just the stack data, we can use a common method called clone.

let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);

Following code seems to contradict what we just learned: we don’t have a call to clone, but x is still valid and wasn’t moved into y.

let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);

The reason is that types such as integers that have a known size at compile time are stored entirely on the stack, so copies of the actual values are quick to make. That means there’s no reason we would want to prevent x from being valid after we create the variable y. In other words, there’s no difference between deep and shallow copying here, so calling clone wouldn’t do anything different from the usual shallow copying and we can leave it out.

Rust has a special annotation called the Copy trait that we can place on types that are stored on the stack. If a type implements the Copy trait, variables that use it do not move, but rather are trivially copied, making them still valid after assignment to another variable.

Here are some of the types that implement Copy:

The mechanics of passing a value to a function are similar to those when assigning a value to a variable. Passing a variable to a function will move or copy, just as assignment does.

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 memory is freed/drop.

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

If we tried to use s after the call to takes_ownership, Rust would throw a compile-time error. These static checks protect us from mistakes. Try adding code to main that uses s and x to see where you can use them and where the ownership rules prevent you from doing so.

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
}

The ownership of a variable follows the same pattern every time: assigning a value to another variable moves it. When a variable that includes data on the heap goes out of scope, the value will be cleaned up unless ownership of the data has been moved to another variable.

While this works, taking ownership and then returning ownership with every function is a bit tedious. What if we want to let a function use a value but not take ownership? It’s quite annoying that anything we pass in also needs to be passed back if we want to use it again. Luckily for us, Rust has a feature for using a value without transferring ownership, called references.

A reference is like a pointer in that it’s an address we can follow to access the data stored at that address; that data is owned by some other variable. Unlike a pointer, a reference is guaranteed to point to a valid value of a particular type for the life of that reference.

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1);
    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

The &s1 syntax lets us create a reference that refers to the value of s1 but does not own it. Because it does not own it, the value it points to will not be dropped when the reference stops being used.

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.

The scope in which the variable s is valid is the same as any function parameter’s scope, but the value pointed to by the reference is not dropped when s stops being used because s doesn’t have ownership. When functions have references as parameters instead of the actual values, we won’t need to return the values in order to give back ownership, because we never had ownership.

We call the action of creating a reference borrowing. As in real life, if a person owns something, you can borrow it from them. When you’re done, you have to give it back. You don’t own it. So what happens if we try to modify something we’re borrowing?

fn main() {
    let s = String::from("hello");
    change(&s);
}

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

Just as variables are immutable by default, so are references. We can fix the code to allow us to modify a borrowed value with just a few small tweak. Use a mutable reference.

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

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

First, we change s to be mut. Then we create a mutable reference with &mut s where we call the change function, and update the function signature to accept a mutable reference with some_string: &mut String. This makes it very clear that the change function will mutate the value it borrows.

Mutable references have one big restriction: if you have a mutable reference to a value, you can have no other references to that value. Following code that attempts to create two mutable references to s will fail:

let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{}, {}", r1, r2);

The restriction preventing multiple mutable references to the same data at the same time allows for mutation but in a very controlled fashion. It’s something that new comers struggle with, because most languages let you mutate whenever you’d like. The benefit of having this restriction is that Rust can prevent data races at compile time. A data race is similar to a race condition and happens when these three behaviors occur:

Data races cause undefined behavior and can be difficult to diagnose and fix when you’re trying to track them down at runtime; Rust prevents this problem by refusing to compile code with data races!

As always, we can use curly brackets to create a new scope, allowing for multiple mutable references, just not simultaneous ones:

let mut s = String::from("hello");
{
    let r1 = &mut s;
} // r1 goes out of scope here, so we can make a new reference with no problems.

let r2 = &mut s;

We also cannot have a mutable reference while we have an immutable one to the same value.

let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // error
println!("{}, {}, and {}", r1, r2, r3);

Users of an immutable reference don’t expect the value to suddenly change out from under them! However, multiple immutable references are allowed because no one who is just reading the data has the ability to affect anyone else’s reading of the data.

Note that a reference’s scope starts from where it is introduced and continues through the last time that reference is used. For instance, following code will compile because the last usage of the immutable references, the println!, occurs before the mutable reference is introduced:

let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{} and {}", r1, r2);
// variables r1 and r2 will not be used after this point

let r3 = &mut s; // no problem
println!("{}", r3);

The scopes of the immutable references r1 and r2 end after the println! where they are last used, which is before the mutable reference r3 is created. These scopes don’t overlap, so this code is allowed. The ability of the compiler to tell that a reference is no longer being used at a point before the end of the scope is called Non-Lexical Lifetimes (NLL for short).

In languages with pointers, it’s easy to erroneously create a dangling pointer--a pointer that references a location in memory that may have been given to someone else--by freeing some memory while preserving a pointer to that memory. In Rust, by contrast, the compiler guarantees that references will never be dangling references: if you have a reference to some data, the compiler will ensure that the data will not go out of scope before the reference to the data does.

fn dangle() -> &String { // dangle returns a reference to a String
    let s = String::from("hello"); // s is a new String
    &s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
  // Danger!

Because s is created inside dangle, when the code of dangle is finished, s will be deallocated. But we tried to return a reference to it. That means this reference would be pointing to an invalid String. That’s no good! Rust won’t let us do this. The solution here is to return the String directly.

fn no_dangle() -> String {
    let s = String::from("hello");
    s
}

This works without any problems. Ownership is moved out, and nothing is deallocated.

Let’s recap what we’ve discussed about references:

 

Slices

Slices let you reference a contiguous sequence of elements in a collection rather than the whole collection. A slice is a kind of reference, so it does not have ownership. 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];
let slice = &s[..2]; // starts from 0
let slice2 = &s[3..]; // till end

Rather than a reference to the entire String, hello is a reference to a portion of the String, specified in the extra [0..5] bit. We create slices using a range within brackets by specifying [starting_index..ending_index], where starting_index is the first position in the slice and ending_index is one more than the last position in the slice.

let s = "Hello, world!";

The type of s here is &str: it’s a slice pointing to that specific point of the binary. This is also why string literals are immutable; &str is an immutable reference.

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3]; // contains 2,3

 

Structures

A struct, or structure, is a custom data type that lets you package together and name multiple related values that make up a meaningful group. If you’re familiar with an object-oriented language, a struct is like an object’s data attributes. Structs are similar to tuples, both hold multiple related values. Like tuples, the pieces of a struct can be different types. Unlike with tuples, 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.

To define a struct, we enter the keyword struct and name the entire struct. A struct’s name should describe the significance of the pieces of data being grouped together. Then, inside curly brackets, we define the names and types of the pieces of data, which we call fields.

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

To use a struct after we’ve defined it, we create an instance of that struct by specifying concrete values for each of the fields. We create an instance by stating the name of the struct and then add curly brackets containing key: value pairs, where the keys are the names of the fields and the values are the data we want to store in those fields. We don’t have to specify the fields in the same order in which we declared them in the struct. In other words, the struct definition is like a general template for the type, and instances fill in that template with particular data to create values of the type.

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

To get a specific value from a struct, we use dot notation. For example, to access this user’s email address, we use user1.email. If the instance is mutable, we can change a value by using the dot notation and assigning into a particular field.

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

Note that the entire instance must be mutable; Rust doesn’t allow us to mark only certain fields as mutable. As with any expression, we can construct a new instance of the struct as the last expression in the function body to implicitly return that new instance.

fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}

 

Enums

Enums allow you to define a type by enumerating its possible variants. Where structs give you a way of grouping together related fields and data, like a Rectangle with its width and height, enums give you a way of saying a value is one of a possible set of values. We can express this concept in code by defining an IpAddrKind enumeration and listing the possible kinds an IP address can be, V4 and V6. These are the variants of the enum.

enum IpAddrKind {
    V4,
    V6,
}

We can create instances of each of the two variants of IpAddrKind like this:

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

Using enums has even more advantages. Thinking more about our IP address type, at the moment we don’t have a way to store the actual IP address data; we only know what kind it is. We can use struts to solve this issue.

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

However, representing the same concept using just an enum is more concise: rather than an enum inside a struct, we can put data directly into each enum variant. This new definition of the IpAddr enum says that both V4 and V6 variants will have associated String values.

enum IpAddr {
   V4(String),
   V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));
let loopback = IpAddr::V6(String::from("::1"));

We attach data to each variant of the enum directly, so there is no need for an extra struct. Here it’s also easier to see another detail of how enums work: the name of each enum variant that we define also becomes a function that constructs an instance of the enum. That is, IpAddr::V4() is a function call that takes a String argument and returns an instance of the IpAddr type. We automatically get this constructor function defined as a result of defining the enum.

There’s another advantage to using an enum rather than a struct: each variant can have different types and amounts of associated data. Version four type IP addresses will always have four numeric components that will have values between 0 and 255. If we wanted to store V4 addresses as four u8 values but still express V6 addresses as one String value, we wouldn’t be able to with a struct. Enums handle this case with ease.

enum IpAddr {
   V4(u8, u8, u8, u8),
   V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

Let’s look at another example of an enum. this one has a wide variety of types embedded in its variants.

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

This enum has four variants with different types:

 

Match

Rust has an extremely powerful control flow construct called match that allows you to compare a value against a series of patterns and then execute code based on which pattern matches. Patterns can be made up of literal values, variable names, wildcards, and many other things. The power of match comes from the expressiveness of the patterns and the fact that the compiler confirms that all possible cases are handled.

Think of a match expression as being like a coin-sorting machine: coins slide down a track with variously sized holes along it, and each coin falls through the first hole it encounters that it fits into. In the same way, values go through each pattern in a match, and at the first pattern the value “fits,” the value falls into the associated code block to be used during execution.

We can write a function that takes an unknown United States coin and, in a similar way as the counting machine, determines which coin it is and return its value in cents.

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Let’s break down the match in the value_in_cents function. First, we list the match keyword followed by an expression, which in this case is the value coin. This seems very similar to an expression used with if, but there’s a big difference: with if, the expression needs to return a Boolean value, but here, it can return any type. The type of coin in this example is the Coin enum that we defined on the first line. Next are the match arms. An arm has two parts: a pattern and some code. The first arm here has a pattern that is the value Coin::Penny and then the => operator that separates the pattern and the code to run. The code in this case is just the value 1. Each arm is separated from the next with a comma. When the match expression executes, it compares the resulting value against the pattern of each arm, in order. If a pattern matches the value, the code associated with that pattern is executed. If that pattern doesn’t match the value, execution continues to the next arm, much as in a coin-sorting machine.

We don’t typically use curly brackets if the match arm code is short. If you want to run multiple lines of code in a match arm, you must use curly brackets, and the comma following the arm is then optional.

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

Another useful feature of match arms is that they can bind to the parts of the values that match the pattern. This is how we can extract values out of enum variants.

In the match expression for following code, we add a variable called state to the pattern that matches values of the variant Coin::Quarter. When a Coin::Quarter matches, the state variable will bind to the value of that quarter’s state. Then we can use state in the code for that arm.

enum UsState {
    Alabama,
    Alaska,
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            if state == UsState::Alaska {
                24
            } else {
                25
            }
        }
    }
}

If we were to call value_in_cents(Coin::Quarter(UsState::Alaska)), coin would be Coin::Quarter(UsState::Alaska). When we compare that value with each of the match arms, none of them match until we reach Coin::Quarter(state). At that point, the binding for state will be the value UsState::Alaska.

Using enums, we can also take special actions for a few particular values, but for all other values take one default action.

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    other => move_player(other),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}

For the first two arms, the patterns are the literal values 3 and 7. For the last arm that covers every other possible value, the pattern is the variable we’ve chosen to name other. The code that runs for the other arm uses the variable by passing it to the move_player function. Above code compiles, even though we haven’t listed all the possible values a u8 can have, because the last pattern will match all values not specifically listed. This catch-all pattern meets the requirement that match must be exhaustive. Note that we have to put the catch-all arm last because the patterns are evaluated in order. If we put the catch-all arm earlier, the other arms would never run, so Rust will warn us if we add arms after a catch-all!

Rust also has a pattern we can use when we want a catch-all but don’t want to use the value in the catch-all pattern: _ is a special pattern that matches any value and does not bind to that value. This tells Rust we aren’t going to use the value, so Rust won’t warn us about an unused variable.

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => reroll(),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}

we’ll change the rules of the game one more time, so that nothing else happens on your turn if you roll anything other than a 3 or a 7. We can express that by using the unit value (the empty tuple type) as the code that goes with the _ arm:

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => (),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}

 

Methods

Methods are similar to functions: we declare them with the fn keyword and a name, they can have parameters and a return value, and they contain some code that’s run when the method is called from somewhere else. Unlike functions, methods are defined within the context of a struct or an enum or a trait object. And their first parameter is always self, which represents the instance of the struct the method is being called on.

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

To define the function within the context of Rectangle, we start an impl (implementation) block for Rectangle. Everything within this impl block will be associated with the Rectangle type. Then we move the area function within the impl curly brackets and change the first parameter to be self in the signature and everywhere within the body. In main, we use method syntax to call the area method on our Rectangle instance. The method syntax goes after an instance: we add a dot followed by the method name, parentheses, and any arguments.

In the signature for area, we use &self instead of rectangle: &Rectangle. The &self is actually short for self: &Self. Within an impl block, the type Self is an alias for the type that the impl block is for. Methods must have a parameter named self of type Self for their first parameter, so Rust lets you abbreviate this with only the name self in the first parameter spot. Note that we still need to use the & in front of the self shorthand to indicate this method borrows the Self instance. Methods can take ownership of self, borrow self immutably as we’ve done here, or borrow self mutably, just as they can any other parameter.

we don’t want to take ownership, and we just want to read the data in the struct, not write to it. If we wanted to change the instance that we’ve called the method on as part of what the method does, we’d use &mut self as the first parameter. Having a method that takes ownership of the instance by using just self as the first parameter is rare; this technique is usually used when the method transforms self into something else and you want to prevent the caller from using the original instance after the transformation.

All functions defined within an impl block are called associated functions because they’re associated with the type named after the impl. We can define associated functions that don’t have self as their first parameter (and thus are not methods) because they don’t need an instance of the type to work with. We’ve already used one function like this: the String::from function that’s defined on the String type. Associated functions that aren’t methods are often used for constructors that will return a new instance of the struct. These are often called new, but new isn’t a special name and isn’t built into the language.

we could choose to provide an associated function named square that would have one dimension parameter and use that as both width and height, thus making it easier to create a square Rectangle rather than having to specify the same value twice:

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

The Self keywords in the return type and in the body of the function are aliases for the type that appears after the impl keyword, which in this case is Rectangle. To call this associated function, we use the :: syntax with the struct name; let sq = Rectangle::square(3); is an example. This function is namespaced by the struct. (the :: syntax is used for both associated functions and namespaces created by modules.)

 

Vectors

Vectors allow you to store more than one value in a single data structure that puts all the values next to each other in memory. Vectors can only store values of the same type. They are useful when you have a list of items, such as the lines of text in a file or the prices of items in a shopping cart. To create a new empty vector, we call the Vec::new function.

let v: Vec<i32> = Vec::new();

Note that we added a type annotation here. Because we aren’t inserting any values into this vector, Rust doesn’t know what kind of elements we intend to store. This is an important point. Vectors are implemented using generics. When we create a vector to hold a specific type, we can specify the type within angle brackets. More often, you’ll create a Vec<T> with initial values and Rust will infer the type of value you want to store, so you rarely need to do this type annotation. Rust conveniently provides the vec! macro, which will create a new vector that holds the values you give it. Following code creates a new Vec<i32> that holds the values 1, 2, and 3. The integer type is i32 because that’s the default integer type.

let v = vec![1, 2, 3];

To create a vector and then add elements to it, we can use the push method.

let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);

As with any variable, if we want to be able to change its value, we need to make it mutable using the mut keyword. The numbers we place inside are all of type i32, and Rust infers this from the data, so we don’t need the Vec<i32> annotation.

There are two ways to reference a value stored in a vector: via indexing or using the get method.

let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {}", third);

let third: Option<&i32> = v.get(2);
match third {
   Some(third) => println!("The third element is {}", third),
   None => println!("There is no third element."),
}

We use the index value of 2 to get the third element because vectors are indexed by number, starting at zero. Using & and [] gives us a reference to the element at the index value. When we use the get method with the index passed as an argument, we get an Option<&T> that we can use with match.

The reason Rust provides these two ways to reference an element is so you can choose how the program behaves when you try to use an index value outside the range of existing elements. As an example, let’s see what happens when we have a vector of five elements and then we try to access an element at index 100 with each technique.

let v = vec![1, 2, 3, 4, 5];

let does_not_exist = &v[100];
let does_not_exist = v.get(100);

When we run this code, the first [] method will cause the program to panic because it references a nonexistent element. This method is best used when you want your program to crash if there’s an attempt to access an element past the end of the vector.

When the get method is passed an index that is outside the vector, it returns None without panicking. You would use this method if accessing an element beyond the range of the vector may happen occasionally under normal circumstances. Your code will then have logic to handle having either Some(&element) or None. That would be more user-friendly than crashing the program.

When the program has a valid reference, the borrow checker enforces the ownership and borrowing rules to ensure this reference and any other references to the contents of the vector remain valid. Recall the rule that states you can’t have mutable and immutable references in the same scope. In following code, we hold an immutable reference to the first element in a vector and try to add an element to the end. Following program won’t compile.

let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {}", first);

why should a reference to the first element care about changes at the end of the vector? This error is due to the way vectors work: because vectors put the values next to each other in memory, adding a new element onto the end of the vector might require allocating new memory and copying the old elements to the new space, if there isn’t enough room to put all the elements next to each other where the vector is currently stored. In that case, the reference to the first element would be pointing to deallocated memory. The borrowing rules prevent programs from ending up in that situation.

To access each element in a vector in turn, we would iterate through all of the elements rather than use indices to access one at a time. Following code use a for loop to get immutable references to each element in a vector of i32 values and print them.

let v = vec![100, 32, 57];
for i in &v {
  println!("{}", i);
}

We can also iterate over mutable references to each element in a mutable vector in order to make changes to all the elements.

let mut v = vec![100, 32, 57];
for i in &mut v {
   *i += 50;
}

To change the value that the mutable reference refers to, we have to use the * dereference operator to get to the value in i before we can use the += operator.

Vectors can only store values that are the same type. This can be inconvenient; there are definitely use cases for needing to store a list of items of different types. Fortunately, the variants of an enum are defined under the same enum type, so when we need one type to represent elements of different types, we can define and use an enum!

enum SpreadsheetCell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    SpreadsheetCell::Int(3),
    SpreadsheetCell::Text(String::from("blue")),
    SpreadsheetCell::Float(10.12),
];

 

Borrow Checker

References are pointers to a memory location. Whether it's a struct or a shared data reference, a rule that must be followed to write safe programs is that References must always point to a valid memory location. More importantly a reference must point to data that it is intended to refer to. This is non-negotiable for rust. What it also means is that at no point in time can an object be dropped from memory if it has a reference. At no point can an invalid reference exist. And rust must ensure this at compile time.

The borrow checker is part of Rust’s compiler that ensures that references are always valid. There must be no ambiguity in the code that points to an invalid reference in memory.

The rules:

All Variables are Initialized before they are used:

fn main() {
    let a: i32;
    let b: &i32; 
    println!("Value of a is {} {} ", a ,b ) // error
}

Whether it’s a primitive type, or a reference type, every single variable must be initialized. println also borrows your variable or clone it if it’s a primitive type. Since we don’t have a value associated with it, we cannot borrow / clone it. Hence the error.

You can’t move the same value twice:

struct Employee {
    id: i32
}

fn main() {
    let a = Employee{id: 43} ;
    let b = a ;
    let c = a ; // error
}

We first define a new Struct Employee and store it in the variable a . As soon as we say b = a . Rust will move the value from a to b. At this point a cannot be used anymore. But again on the next line we try assigning c = a ; . This is when the borrow checker realizes that a has already been moved and its lifetime has ended. We cannot move the same value again.

You can’t move a value while it is borrowed:

struct Employee {
    id: i32
}

fn main() {
    let a = Employee{id: 43};
    let b = &a;
    let c = a; // a moved. now b is invalid. b cannot be used anymore!
    println!("{}", b.id); // error
}

we create a reference to a in variable b. At this point, both a and b are valid. Now in the next line, we move the value of a into c by assigning c to a. At this point, a is invalid. And if we try to use b which points to a rust knows that there is a reference to a, which is used after we move the value. So this means there will be a dangling pointer.

You can’t access a place while it is mutably borrowed:

struct Employee {
    id: i32
}

fn main() {
    let mut a = Employee{id: 43};
    let b  = &mut a;
    let c = &a; // error! a already mutually borrowed by b and we are within the lifetime of b.
    b.id = 55; // since b is last used here, lifetime of b ends at this line.
    let d = &a; // ok.(out of the lifetime of b)
}

we created a mutable employee. This means at any point of time we can update the value of my employee id. We assign a variable b a mutable reference to a. which means b can now update the value inside a because it’s mutable. Now let’s create another variable c, which is an immutable reference to a. Since we already have a mutable reference to a, and we use the referenced object in future, rust doesn’t allow an immutable borrow.

We cannot create immutable references if the original value is being updated by a mutable reference. There can only be one mutable reference.

You can’t mutate a place while it is immutably borrowed:

struct Employee {
    id: i32
}

fn main() {
    let mut a = Employee{id: 43};
    let c = &a;
    let b = &mut a; // error! we are within the lifetime of c.
    println!("{}", c.id); // lifetime of c ends here.
}

 

 

References:

https://doc.rust-lang.org/book/

https://blog.devgenius.io/rust-lifetimes-simplified-part-1-the-borrow-checker-b851b570d043