Follow

Follow
Introduction to Rust  - Part 2

Introduction to Rust - Part 2

Sangam Biradar's photo
Sangam Biradar
·Dec 27, 2022·

74 min read

Table of contents

Introduction to Functions

What Is a Function?

A function is a block of code that can be reused. It is used to perform specific tasks.

main Function

The simplest possible function that we have studied so far is the main function that is declared with fn keyword. This is where the program execution starts. However, it is possible to define a user-defined function.

User-defined Functions

The functions are customized and are written by the programmer to perform the specified tasks.

Define a Function

A function is declared with the fn keyword.

  • Syntax The general syntax is :

Naming Convention: The convention for writing a function name is in a snake case, i.e., - All letters should be lowercase

- All words should be separated by underscores

Call a Function

The function executes when it is invoked.

  • Syntax

The expression starts with the function name followed by the round brackets and then the semicolon. The function parameters may be added between the parentheses if needed.

The general syntax for invoking a function:

Note: A user-defined function can be invoked from another function or the main function. It can be defined anywhere, above or below the main function.

If a function is invoked but its definition does not exist, the compiler will throw a compilation error, ❌, such as error[E0425]: cannot find function function_xyz in this scope.

Example

The following example makes a user-defined function and invokes it from within the main function.

//define a function
fn display_message(){
  println!("Hi, this is my user defined function");
}
//driver function
fn main() {
     //invoke a function
      display_message();
      println!("Function ended");
}

Output

Hi, this is my user defined function
Function ended

Explanation

The above program comprises two functions, the user-defined function display_message() and the driver function main() where the function is being called.

  • User-defined function

The function display_message() is defined from line 2 to line 4. On line 3 a message is printed.

  • Driver function

The driver function main() is defined from line 6 to line 10. On line 8 the function display_message() is invoked.

Functions With Parameters

In the previous example, a function was defined with nothing inside the round brackets. But certain functions require some information on which they should operate. For instance, a function that is expected to compute the square of a number needs to be provided with the number itself. That’s what a parameter is.

What Are the Parameters?

Variable or values that go in the function definition are parameters.

What Are Arguments?

Variables or values that go in their place in the function invocation are known as arguments.

Example

To understand the above concept, let’s look at the example below:

//function definition
fn my_func(param_1:i32, param_2:i32) {
  println!("The first value passed inside function : {}", param_1);
  println!("The second value passed inside function : {}", param_2);
}
fn main() {
  let value_1 = 1;
  let value_2 = 2;
  //calling the function
  my_func( value_1, value_2 );
  println!("Function ended");
}

output

The first value passed inside function : 1
The second value passed inside function : 2
Function ended

Explanation

The above program comprises two functions, the user-defined function my_func() and the driver function main() where the function is being called.

  • User-defined function

The function my_func() is defined from the line 2 to line 5.

  • Two parameters param_1 and param_2 are passed to the function.

  • The values of passed parameters are printed online 3 and line 4.

  • Driver function

  • The driver function main() is defined from the line 6 to line 12.

    • On line 7 and line 8, two variables value_1 and value_2 are defined.

    • On line 10, the function is invoked while passing the value of the variable value_1 as the first argument and that of value_2 as the second.

Types of Arguments

  • Arguments can be passed to a function in two different ways:

    • Pass by value

    • Pass by reference

Pass by Value

  • Arguments Pass by Value The values from the calling function are copied to the parameters in the called function at the time the function is called. The called function can change the values of the parameter variables all it wants. This change will not be reflected in the variables passed as arguments in the calling function.

  • Syntax The general syntax of passing arguments by value is:

Example

The following example makes a function square() that takes a number n as a parameter to the function and prints the square of the function within the function.

fn square(mut n:i32){
  n = n * n;
  println!("The value of n inside function : {}", n);
}
fn main() {
  let n = 4;
  println!("The value of n before function call : {}", n);
  println!("Invoke Function");
  square(n);
  println!("\nThe value of n after function call : {}", n);
}

Output


The value of n before function call : 4
Invoke Function
The value of n inside function : 16
The value of n after function call : 4

Explanation

The above program is of two parts, the user-defined function square() and the driver function main() where the function is being called.

  • User-defined function The function square()is defined from the line 1 to line 4.

    On line 2 n is multiplied by itself and the value is saved in n. The square of the argument thus calculated is displayed on the screen online 3.

  • Driver function

The driver function main() is defined from line 5 to line 11.

On line 6, a variable n is defined. On line 9, the function square() is invoked which takes n as an argument to the function. After the function call, the value of the n is printed

Note: The value of n is not changed

Arguments Pass by Reference

When we want the called function to make changes to the parameters such that the changes are seen by the calling function when the call returns. The mechanism to do this is called pass arguments by reference.

  • Syntax The general syntax of passing arguments by value is:

Example

The following example makes a function square() that takes a number n which is being passed by reference as a parameter to the function and prints the square of the function within the function.

fn square(n:&mut i32){
  *n = *n * *n;
  println!("The value of n inside function : {}", n);
}  
fn main() {
  let  mut n = 4;
  println!("The value of n before function call : {}", n);
  println!("Invoke Function");
  square(&mut n);
  println!("The value of n after function call : {}", n);
}

Output

The value of n before function call : 4
Invoke Function
The value of n inside function : 16
The value of n after function call : 16

Explanation

The above program comprises two functions, the user-defined function square() and the driver function main() where the function is being called.

User-defined function

  • The function square() is defined from line 1 to line 4 which takes a mutable reference (&mut) to the parameter n of type i32.

    • On line 2, the square of the variable n is calculated. Since n is a reference to a variable, to access the referenced variable’s value, de-referencing is required. That is achieved with the *n. On the right-hand side, the value referenced by n is accessed and multiplied by itself. The assignment is also to *n, which means the calculated result is stored in the variable that n is referencing.

    • The square of the function is printed on line 3.

Driver function

The driver function main() is defined from line 5 to line 11.

  • On line 6, a mutable variable n is defined.

  • On line 9, the function square() is invoked.The argument to this function is & mut n. Here, & indicates that it is a reference to the variable n and mut indicates that n can be changed inside the function square(). - After the function call, the value of the n is printed.

Note: The value of n is changed within the function.

Note: The argument, as well as the parameter, is set as a mutable reference when the value is passed by reference. If the value is to be updated it is dereferenced first and then the update operation is performed.

Returning a Value From a Function

  • Returning Functions

The functions can return a value using the return keyword inside the function definition. After the return statement is executed, the control gets back to the caller. A function invocation is replaced with the value that the call returns. Thus, that value can be saved in a variable.

  • Syntax The function definition for returning a value from a function:

There are two ways to return the value.

The general syntax for returning a value from a function using the return keyword:

The following syntax can be used to return a value from a function without using the return keyword:

Just write the return value, and the compiler will interpret it because of the -> sign in the function definition.

Example 1

The following example makes a function square() that takes a number n as a parameter to the function and stores the square of n in the local variable m and returns the variable m.

fn square(n:i32)->i32{
  println!("The value of n inside function : {}", n);
  let m = n * n;
  m // return the square of the number n
}  
fn main() {
  let  n = 4;
  println!("The value of n before function call : {}", n);
  println!("Invoke Function");
  println!("\nOutput : {}",square(n));
}

output

The value of n before function call : 4
Invoke Function
The value of n inside function : 4

Output : 16

Example 2

The following example makes a function square() that takes a number n as a parameter to the function and returns the square of the number n by using the return keyword.

fn square(n:i32)->i32{
  println!("The value of n inside function : {}", n);
  return n * n;
}  
fn main() {
  let  n = 4;
  println!("The value of n before function call : {}", n);
  println!("Invoke Function");
  println!("\nOutput : {}", square(n));
}

output

The value of n before function call : 4
Invoke Function
The value of n inside function : 4

Output : 16

Explanation

The above program is of two parts, the user-defined function square() and the driver function main() where the function is being called.

  • User-defined function

The function square() is defined from line 1 to line 4.

  • On line 3 n is multiplied by itself and the value is saved in n and the value of type i32 is returned using the return keyword.

  • Driver function

The driver function main() is defined from line 5 to line 10.

  • On line 6, a variable n is defined.

  • On line 9, the function square() is invoked which takes n as an argument to the function and the value of the square is printed using the println!() macro.

Function With Multiple Return Values

  • Returning Multiple Values In system programming languages like C++ and C, it is only possible to return a single value or a pointer to an array from a function. However, Rust allows you to return multiple values using a tuple.

  • Syntax The function definition for returning multiple values:

The way to return a tuple from a function is to just write the tuple:

Defining a function by returning a tuple

Example

The following example makes a function calculate_area_perimeter() that takes a x and y( length and width of a rectangle) as a parameter to the function and returns a tuple (area, perimeter).

// driver function
fn main() {
    let length = 4;
    let width = 3;
    println!("Rectangle lenth:{}", length);
    println!("Rectangle width:{}", width);
    let (area, perimeter) = calculate_area_perimeter(length, width);
    println!("Area: {}, Perimeter: {}", area, perimeter);
}
// calculate area and perimeter
fn calculate_area_perimeter(x: i32, y: i32) -> (i32, i32) {
    // calculate the area and perimeter of rectangle
    let area = x * y;
    let perimeter = 2 * (x + y);
    // return the area and perimeter of rectangle
    (area, perimeter)
}

output

Rectangle lenth:4
Rectangle width:3
Area: 12, Perimeter: 14

Explanation

The above program comprises two functions, the user-defined function calculate_area_perimeter() and the driver function main() where the function is being called.

  • User-defined function The function calculate_area_perimeter() is defined from line 11 to line 17.

    • On line 13, the area of the rectangle is calculated by multiplying the parameters x and y and the result is saved in the area.

    • On line 14, the perimeter of the rectangle is calculated by adding parameters x andyand then multiplying the result with 2 and then, the final result is saved in the perimeter.

    • On line 16, a tuple (area, perimeter) is returned.

  • Driver function The driver function main() is defined from the line 2 to line 9

    • On line 3, a variable length is initialized with the value 4.

    • On line 4, a variable width is initialized with the value 3.

    • On line 5 and 6, the value of length and width is displayed respectively.

    • On line 7, the function calculate_area_perimeter() is invoked which takes length and width as an argument to the function and the return value of the function is saved in a tuple.

Functions With Arrays as Arguments

It is often necessary to pass arrays as arguments to functions. Rust allows the programmer to pass arrays either by value or by reference.

- Pass by Value
Arrays can be passed to a function by value. What that means is that a copy of the array from the calling function is made to the called function. The general syntax for passing an array by value to a function is:

bash fn function_name( mut array_name:[datatype;size])

- Example 1

The following example takes the array arr by value in the function parameter.

fn main() {
   let arr = [1, 2, 3, 4, 5];
   modify_my_array(arr);
   println!("Array in Driver Function : {:?}", arr);
}
fn modify_my_array(mut arr:[i32;5]){
   arr[2] = 8;
   arr[3] = 9;
   println!("Array in my Function : {:?}", arr);
}

output

Array in my Function : [1, 2, 8, 9, 5]
Array in Driver Function : [1, 2, 3, 4, 5]

Note: The mut the keyword when passing an array by value is optional. It is written along with the array name if it is desired to make local changes. It can be omitted otherwise.

Example 2

The following example makes a function calculate_mean which calculates the mean of values in an array by first taking a summation inside a for loop and then dividing the result by 5.

fn main() {
   let arr = [1, 2, 3, 4, 5];
   println!("Array in Driver Function : {:?}", arr);
   calculate_mean(arr);
}
fn calculate_mean(arr:[i32;5]){
   let mut sum = 0;
   for i in 0..5 {
       sum += arr[i];
   }
   println!("Mean of array values: {}", sum/5);
}

output

Array in Driver Function : [1, 2, 3, 4, 5]
Mean of array values: 3

Pass by Reference

Arrays can be passed by reference in the function parameter. In other words, changes are made in the original array and no copy is made when passed by reference in the function.

The general syntax for passing an array by reference to a function is:

fn function_name(array_name:&mut [datatype;size])
  • Example The following example takes the array arr by reference in the function parameter.
fn main() {
   let mut arr = [1, 2, 3, 4, 5];
   modify_my_array(&mut arr);
   println!("Array in Driver Function : {:?}", arr);
}
fn modify_my_array(arr:&mut [i32;5]){
   arr[2] = 8;
   arr[3] = 9;
   println!("Array in my Function : {:?}", arr);
}

output

Array in my Function : [1, 2, 8, 9, 5]
Array in Driver Function : [1, 2, 8, 9, 5]

Return an Array

Arrays can be returned from the function.

The general syntax for returning an array from a function is:

fn function_name()->[datatype;size]

Note: Here the parameters can also be passed.

Example

The following example takes the array arr, modifies it within the function and returns it.

fn main() {
   let arr = [1, 2, 3, 4, 5];
   modify_my_array(arr);
   println!("Array in Driver Function : {:?}", arr);
   println!("Array after Function Call : {:?}", modify_my_array(arr));
}
fn modify_my_array(mut arr:[i32;5])->[i32;5]{
   arr[2] = 8;
   arr[3] = 9;
   arr
}

output

Array in Driver Function : [1, 2, 3, 4, 5]
Array after Function Call : [1, 2, 8, 9, 5]

What Is Recursion?

Recursion is a method of function calling in which a function calls itself during execution.

There are problems which are naturally recursively defined. For instance, the factorial of a number nnn is defined as n times the factorial of n−1

factorial(n) = n * factorial(n-1)

Parts of Recursion

In terms of programming, a recursive function must comprise two parts:

  • Base case

    • A recursive function must contain a base case. This is a condition for the termination of execution.
  • Recursive case

    • The function keeps calling itself again and again until the base case is reached.

Example

The following example computes the factorial of a number using recursion:

Note: A factorial is defined only for non-negative integer numbers.

// main function
fn main(){
   // call the function
   let n = 4;
   let fact = factorial(n);
   // print the factorial
   println!("factorial({}): {}", n, fact);
}
// define the factorial function
fn factorial(n: i64) -> i64 {
   if n == 0 { // base case
       1
   }
   else {
       n * factorial(n-1) // recursive case
   }
}

output

factorial(4): 24

Explanation

main function

  • The main function is defined from line 2 to line 7.

    • On line 4, a call is made to function factorial with an argument passed to the function and the return value is saved in the variable fact.

    • On line 6, the value of the variable fact is printed, i.e., the factorial of the number being passed as an argument.

  • factorial function

    • The factorial function is defined from line 9 to line 16.
  • function definition

    • The function takes a parameter n of type i64.
  • function body

  • The recursive function is made up of two parts.

    • base case
  • On line 10, the base case is defined. Since the value of n is decremented in every recursive function call, the function terminates when the value of n becomes equal to 0 on successive recursive calls.

  • recursive case

  • On line 14, the recursive case is defined. The value n gets multiplied with factorial(n-1) and gets pushed on the memory stack. Since the value of n is decremented in every function call, the function keeps on calling itself repeatedly until the base case is reached. As soon as the base case is reached, factorial(0) is calculated and the value is used in the immediate expression in the memory stack. The factorial(1) is calculated from 1∗factorial(0). factorial(2) is calculated from 2∗factorial(1) This process n∗factorial(n−1)continues until the last value is freed from the memory stack

What Is Recursion?

Recursion is a method of function calling in which a function calls itself during execution.

There are problems which are naturally recursively defined. For instance, the factorial of a number nnn is defined as n times the factorial of n−1

factorial(n) = n * factorial(n-1)

Parts of Recursion

In terms of programming, a recursive function must comprise two parts:

  • Base case

    • A recursive function must contain a base case. This is a condition for the termination of execution.
  • Recursive case

    • The function keeps calling itself again and again until the base case is reached.

Example

The following example computes the factorial of a number using recursion:

Note: A factorial is defined only for non-negative integer numbers.

// main function
fn main(){
   // call the function
   let n = 4;
   let fact = factorial(n);
   // print the factorial
   println!("factorial({}): {}", n, fact);
}
// define the factorial function
fn factorial(n: i64) -> i64 {
   if n == 0 { // base case
       1
   }
   else {
       n * factorial(n-1) // recursive case
   }
}

output


factorial(4): 24

Explanation

main function

  • The main function is defined from line 2 to line 7.

    • On line 4, a call is made to function factorial with an argument passed to the function and the return value is saved in the variable fact.

    • On line 6, the value of the variable fact is printed, i.e., the factorial of the number being passed as an argument.

  • factorial function

    • The factorial function is defined from line 9 to line 16.
  • function definition

    • The function takes a parameter n of type i64.
  • function body

  • The recursive function is made up of two parts.

    • base case

    • On line 10, the base case is defined. Since the value of n is decremented in every recursive function call, the function terminates when the value of n becomes equal to 0 on successive recursive calls.

  • recursive case

  • On line 14, the recursive case is defined. The value n gets multiplied with factorial(n-1) and gets pushed on the memory stack. Since the value of n is decremented in every function call, the function keeps on calling itself repeatedly until the base case is reached. As soon as the base case is reached, factorial(0) is calculated and the value is used in the immediate expression in the memory stack. The factorial(1) is calculated from 1∗factorial(0). factorial(2) is calculated from 2∗factorial(1) This process n∗factorial(n−1)continues until the last value is freed from the memory stack

Introduction to Strings

  • What are Strings?

Strings are a sequence of Unicode characters. In Rust, a String is not null-terminated unlike strings in other programming languages. They can contain null characters.

Note: Have a look at the Unicode characters

Types of Strings

  • Strings are of two types: &str and String

    • String Literal (&str)
  • A String literal has the following properties:

    • Primitive type

    • Immutable

    • The fixed-length string stored somewhere in memory

    • The value of string is known at compile time

Note: A String literal is also known as a String slice.

Create a String Literal

The following illustration shows how to create a primitive string:

fn main() {
  //define a primitive String variable
  let language:&str = "Rust";
  //print the String literal
  println!("String literal: {}", language);
  //print the length of the String literal
  println!("Length of the string literal: {}", language.len());
}

output

String literal: Rust
Length of the string literal: 4

String Object (String)

  • A String object has the following properties:

    • A string is encoded as a UTF-8 sequence

    • Heap-allocated data structure

    • The size of this string can be modified

    • Not null-terminated

    • Encode string values that are given at the run time

Create a String Object

This method converts the empty String or a String literal to a String object using the .tostring method.

The following illustration creates an empty String and then converts it into the string object using the .to_string() method.

Creating an Initialized String Object

This method creates a string with some default value passed as an argument to the from() method.

The following illustration creates a String literal and then converts it into the String object.

fn main() {
  // create an empty String
  let course1 = String::new();
  // convert String literal to String object using .to_string
  let s_course1 = course1.to_string();
  // print the String object
  println!("This is an empty string {}.", s_course1);
  // print the length of an empty String  object
  println!("This is a length of my empty string {}.", s_course1.len());

  // create a String literal
  let course2 = "Rust Programming";
  // convert String literal to string object using .to_string
  let s_course2 = course2.to_string();
  // print the String object
  println!("This is a string literal : {}.", s_course2);
  // print the length of a String object
  println!("This is a length of my string literal {}.", s_course2.len());

  // define a String object using from method
  let course3 = String::from("Rust Language");
  // print the String object
  println!("This is a string object : {}.", course3);
  // print the length of an string object
  println!("This is the length of my string object {}.", course3.len());
}

Note: len() is a built-in function used to find the length of a String literal and String object.

Core Methods of String Objects

Some of the core methods are discussed in this lesson. You can find a list of all the String methods in Rust documentation of Strings.

Capacity in Bytes

The capacity gives the number of bytes allocated to the String, unlike len which gives the number of bytes taken by the String object. To get the capacity of a variable in bytes, use the built-in function capacity().

  • Syntax

The general syntax is:

str.capacity()

Here str is the string whose capacity is to be found.

Note: The length of the String will always be less than or equal to the capacity.

fn main() {
  // define a growable string variable
  let course = String::from("Rust");
  println!("This is a beginner course in {}.", course);
  //capacity in bytes
  println!("Capacity: {}.", course.capacity());
}

output

This is a beginner course in Rust.
Capacity: 4.

Finding a Substring

To find if one string contains another string, use the contains() built-in function.

  • Syntax The general syntax is :
str.contains("sub_str")

Here str is the original string and "sub_str" is a substring which is to be found in a string.

fn main() {
  // define a growable string variable
  let str = String::from("Rust Programming"); 
  let sub_str = String::from("Rust"); 
  println!("This is a beginner course in {}.", str);
  // find if string contains a substring
  println!("{} is a substring of {}: {}.", sub_str, str, str.contains("Rust"));
}

output

This is a beginner course in Rust Programming.
Rust is a substring of Rust Programming: true.

Replace a Substring

To replace all occurrences of one substring within a String object with another String, use the replace() built-in function.

  • Syntax

The general syntax is :

str.replace(replace_from, replace_to)

Here str is the original string, replace_from is the value which is to be replaced in the string str and replace_to is the value the string is converted to.

fn main() {
  // define a growable string variable
  let str = String::from("Rust Programming"); 
  let replace_from = "Programming";
  let replace_to = "Language"; 
  // find if string contains a substring
  let result = str.replace(replace_from, replace_to);
  println!("{} now becomes {}.", str, result);
}

output


Rust Programming now becomes Rust Language.

Trim a String

To trim a string use the function trim(). It is used to remove leading and trailing whitespaces in a string.

  • Syntax The general syntax is :
string.trim()

Note: The trim function does not remove the space between the string.

fn main() {
   let string = "   Rust     Programming     ".to_string();
   let trim_string = string.trim(); 
   // get characters at 5,6,7,8,9,10 and 11 indexes
   println!("Trimmed_string : {}", trim_string);
}

output

Trimmed_string : Rust     Programming

Iterating Over Strings

The following methods describe three different ways of traversing a String:

  • Tokenizing a String Object A String object can be tokenized on a whitespace or a character token.

Tokenizing to Separate on Whitespaces

split_whitespace is used to split a String on the occurrence of whitespace. Loop through the String to split on whitespaces using a for a loop.

Syntax

The general syntax is:

for found in  str.split_whitespace(){
    println!("{}", found);
}

Here str is the original String which is to be traversed, split_whitespace() is a built-in keyword to split a string on whitespaces, for is used to traverse over the String and print it as soon as the whitespace is found and found is an iterator over the String.

fn main() {
  // define a String object
  let str = String::from("Rust Programming"); 
  // split on whitespace
  for token in str.split_whitespace(){
      println!("{}", token);
  }
}

output

Rust
Programming

Tokenizing to Split on a Custom Character

split method is used to split a sentence on some token. The token is specified in the split method. This would be useful to process comma-separated data, which is a common programming task.

  • Syntax

The general syntax is:

for found in str.split(","){
    println!("{}", found);
}

Here str is the original String which is to be traversed,str.split() is a built-in method which takes a parameter, i.e., any delimiter and split the sentence on that parameter, for is used to traverse over the String and print a word before the token.

fn main() {
  // define a String object
  let str = String::from("CloudNativeFolks, course on, Rust, Programming");  
  // split on token
  for token in str.split(","){
      println!("{}", token);
  }
}

output

 CloudNativeFolks 
 course on
 Rust
 Programming

Iterating Over the String Object

chars method allows iterating over each element in a String using a for a loop.

  • Syntax The general syntax is:
for found in  str.chars(){
    println!("{}", found);
}

Here str is the original String which is to be traversed, str.chars() is a built-in keyword to denote letters in a String, for is used to traverse over the String and print every literal, and found is an iterator over the String.

fn main() {
  // define a String object
  let str = String::from("Rust Programming");  
  // split on literal
  for token in str.chars(){
      println!("{}", token);
  }
}

output

R
u
s
t

P
r
o
g
r
a
m
m
i
n
g

Updating a String

An existing string can be updated by appending a character or a string.

💡 Why not make a new String rather than updating an existing one?

Updating an existing String is useful when you want to make changes to an existing String at run time rather than compile one like, in situations where changes are made to the String on a condition.

Push a Single Character

There are cases when it is required to update a string by pushing a single character. One example is to create a string which contains a single character repeated N times on a particular condition. Rust helps you do it by using the push method.

  • Steps to push a character to a String:

    • Make a mutable string variable.

    • To push a single Unicode character to a String object, pass a character within the push() built-in method.

    • The following code shows how to do it!

     fn main() {
 // define a String object
 let mut course = String::from("Rus");
 // push a character
 course.push('t');
 println!("This is a beginner course in {}.", course);
}

output

This is a beginner course in Rust.

There are cases when it is required to grow a String by concatenating a new String to an existing String. Rust helps you do it by using the push,+ operator and the format! macro method.

Push a String

Rust helps you to grow a String object using a push_str method.

  • Steps to push a String to a String:

    • Make a mutable String variable.

    • To push a string to a growable string variable, pass a character within the push_str() built-in method.

The following code shows how to do it

fn main() {
  // define a string object
  let mut course = String::from("Rust");
  // push a string
  course.push_str(" Programming");
  println!("This is a beginner course in {}.", course);
}

output

This is a beginner course in Rust Programming.

Concatenation Using + Operator

A String can be concatenated to another String using the + operator. Note: The right-hand-side operand is to borrowed while concatenating using + operator. The following code shows how to do it!

#[allow(unused_variables, unused_mut)]
fn main(){
   // define a String object 
   let course = "Rust".to_string();
   // define a String object
   let course_type = " beginner course".to_string();
   // concatenate using the + operator
   let result = course + &course_type;
   println!("{}", result);
}

output

Rust beginner course

Format Macro

To add two or more String objects together, there is a macro called format!. It takes variables or values and merges them in a single String.

Note: The format! macro allows concatenating in the desired order by passing a positive integer number within the placeholder. 
If the number is not mentioned it will concatenate in the order of the values written.

To display the result of format! macro, the result is to be saved in a variable.

The following code shows how to do it!

fn main(){

   let course = "Rust".to_string();
   let _course_type = "beginner course".to_string();
   // default format macro 
   let result = format!("{} {}", course, _course_type);
   // passing value in the placeholder in the format macro 
   let result = format!("{1} {0}", course,_course_type);
   println!("{}", result);
}

output

beginner course Rust

Slicing a String

What Is Slicing?

  • Slicing is used to get a portion of a string value.

  • Syntax The general syntax is:

let slice = &string[start_index..end_index]

Here,

  • start_index and end_index are the positions of starting and ending index of the original array respectively.

    • Note: The start_index is inclusive and end index is exclusive
  • & indicates that the variable slice borrows the string.

    • Note: The minimum value of start_index is 0 and the maximum value is the size of the string
   fn main() {
   let string = "Rust Programming".to_string();
   let slice = &string[5..12]; 
   // get characters at 5,6,7,8,9,10 and 11 indexes
   println!("Slice : {}", slice);
}

output

Slice : Program

functions and Strings

Passing Primitive String - String Literal (&str)

String literals are passed to the functions just like other variables. They can be reused after the function call.

fn main(){
   let course: &str = "Rust Programming";
   display_course_name(course); 
   println!("{}",course); // string literal is used after the function call
}
fn display_course_name(my_course: &str){
   println!("Course : {}", my_course);
}

output

Course : Rust Programming
Rust Programming

Passing Growable String - String Object (String)

While passing String Objects to functions, they cannot be reused again because the value once passed gets moved to that function’s scope and cannot be reused.

fn main(){
   let course:String = String::from("Rust Programming");
   display_course_name(course); 
   //cannot access course after display
}
fn display_course_name(my_course:String){
   println!("Course : {}", my_course);
}

output

Course : Rust Programming

Introduction to Vectors

  • What are Vectors?

Vectors are resizable arrays meaning(they can grow or shrink in size).

Create Vectors

  • There are two ways to create a vector:

  • Syntax

    • To create a vector write the vector macro (vec!) followed by the elements of the vector enclosed in square brackets

t is optional to define the type and size of the vector enclosed within angular brackets. Use the vector macro(vec!) before defining the elements of the vector.

fn main() {
   //define a vector of size 4
   let my_vec = vec![1, 2, 3, 4, 5];
   //print the vector
   println!("{:?}", my_vec);
}

output

[1, 2, 3, 4, 5]

Note: Like arrays can be displayed on the screen using the println!() macro.

Access an Element of a Vector

  • Any value of the vector can be accessed by writing the vector name followed by the index number enclosed within square brackets [ ].
fn main() {
   //define a vector of size 4
   let my_vec = vec![1, 2, 3, 4, 5];
   //access a particular value
   println!("{}", my_vec[0]);
}

output

1

Note: If you try to access an index that does not exist, the compiler will give out of bound access error, ❌.

This is illustrated in the code below:

fn main() {
   //define a vector of size 4
   let my_vec = vec![1, 2, 3, 4, 5];
   //access a particular value
   eprintln!("{}", my_vec[9]);
}

output

  thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 9', /rustc/73528e339aae0f17a15ffa49a8ac608f50c6cf14/src/libcore/slice/mod.rs:2796:10
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

To cater to out-of-bound exceptions, you can use a None keyword.

fn main() {
    let my_vec = vec![1, 2, 3,4,5];
    match my_vec.get(9) {
        Some(x) => println!("Value at given index:{}", x),
        None => println!("Sorry, you are accessing a value out of bound")
    }
}

output

Sorry, you are accessing a value out of bound

Print the Vector

fn main() {   
    println!("Print using debug trait");   
    let my_vec = vec![1, 2, 3,4,5];
    //using debug trait 
    println!("Vector : {:?}", my_vec);
    println!("Print using for loop"); 
    // using loop
    let mut index = 0;
    for i in my_vec {
        println!("Element at index {}:{} ", index, i);
        index = index+1;
    }
}

output

Print using debug trait
Vector : [1, 2, 3, 4, 5]
Print using for loop
Element at index 0:1 
Element at index 1:2 
Element at index 2:3 
Element at index 3:4 
Element at index 4:5

Methods of Vectors

The methods of vectors are summarized in the chart below:

#methodexplaination
1Vec::new()creates a new vector
2.push()push a value
3.pop()pop a value
4.contains()returns true if the vector
contains a particular value
5remove(i)remove value at given index
6.len()return len of the vector

The following code demonstrates each of the above methods:

fn main() {

   let mut my_vec = Vec::new();

   println!("Empty Vector : {:?}", my_vec);

   my_vec.push(1);

   my_vec.push(2);

   my_vec.push(3);

   println!("Pushed elements 1 , 2 , 3 : {:?}", my_vec);

   my_vec.pop();

   println!("Popped value: {}", 3);

   println!("Popped element at last index : {:?}", my_vec);

   my_vec.remove(1);

   println!("Removed value: {}", 2);

   println!("Removed element at index 1 : {:?}", my_vec);

   println!("Size of vector is :{}", my_vec.len());

   println!("Does my vector contains 1 : {}", my_vec.contains(&1));

}

output

Empty Vector : []
Pushed elements 1 , 2 , 3 : [1, 2, 3]
Popped value: 3
Popped element at last index : [1, 2]
Removed value: 2
Removed element at index 1 : [1]
Size of vector is :1
Does my vector contains 1 : true

Note: When using the .contains function, consider borrowing the value.

Iterating Over a Vector

If it is desired to access each element of a vector, then it is possible to iterate over the elements of a vector using iter() rather than using the indexes to access a particular element of a vector using the square bracket notation

Iterate Using .iter() Built-in Method

  • we learned to remove an element given an index. However, to remove a particular element, we first need to find the index of that element and then call the remove function passing that index.

  • For this we can use the .iter().position(|&e| e == element_name).unwrap().

Here,

  • iter() is the built-in function that iterates over the elements of the vector.

  • .position is a built-in function that takes the element name to get the position of that element in the vector, i.e., (|&e| e == element_name) defines a variable e with the value equal to the name of the element that we want to find.

  • .unwrap() is the built-in function.

fn main() {
    // defines a mutable vector
    let mut my_vec = vec![1, 2, 3, 4, 5];
    // define the value to be removed
    let value = 2; 
    // get the index of the value in the vector
    let index = my_vec.iter().position(|&r| r == value).unwrap();
    // call the built-in remove method
    my_vec.remove(index);
    // print the updated vector
    println!("Updated Vector: {:?}", my_vec);
}

output

Updated Vector: [1, 3, 4, 5]

As you can see the value 2 is removed from the vector. you’ll learn how the iterator function helps to loop through each element in the vector index-by-index.

Loop Through the Values

  • Define a vector variable.

  • The values of the vector within the loop can be traversed using .iter().

📝If you don’t write .iter() within the loop defination, a simple for loop will give you the same result.


fn main() {
  // define a vector of size 5   
  let my_vec = vec![1, 2, 3, 4, 5];
  // using loop
  let mut index = 0;
  for i in my_vec.iter(){ // it works even if .iter() is not written
      println!("Element at index {}:{} ", index, i);
      index = index + 1;
  }
}

output

Element at index 0:1 
Element at index 1:2 
Element at index 2:3 
Element at index 3:4 
Element at index 4:5

Loops and Mutate Values

  • Define a mutable vector variable

  • The values of the vector within the loop can be changed using .iter_mut().

fn main() {
 // define a vector of size 5
 let mut my_vec = vec![1, 2, 3, 4, 5];
 println!("Initial Vector : {:?}", my_vec);
 for x in my_vec.iter_mut(){
     *x *= 3;
 }
 // print the updated vector
 println!("Updated Vector : {:?}", my_vec);
}

The following illustration shows how the above code works:

Slicing a Vector

  • Get Slice Imagine a situation where you need to get a portion of a vector. Rust allows you to borrow the slice of the vector instead of using the whole vector.

  • Syntax Slice is a two-word object. The first word is a pointer to the data, and the second word is the length of the slice.

fn main() {
   // define a vector of size 5
   let my_vec = vec![1, 2, 3, 4, 5];
   let slice:&[i32] = &my_vec[2..4];
   // print the vector
   println!("Slice of the vector : {:?}",slice);
}

output

Slice of the vector : [3, 4]

Introduction to Structs

What Are Structs?

  • Structs consist of related items that potentially have different data types.

  • Structs are similar to tuples in this regard. However, unlike tuples, you must define the data type of the item within the struct

  • Structs help to create custom data types.

  • Let’s consider a real-life example. You know that a rectangle has two measurements, width and height. Suppose you have several rectangles which you can name. Once you have declared rectangle items within the struct, you can initialize the values according to the type of rectangle. Suppose the dimensions of a rectangle may vary according to the colour of the rectangle.

Declare a Struct

Structs are declared using a struct keyword followed by the name of the struct and then the body of the struct enclosed within curly braces. Within the body, the items of the struct are defined as a key: value pair where keys are the items of the struct and value is the data type of each item.

Note: The struct construct can be declared anywhere, above or below the function that initializes it.

  • Naming Convention:

    • The name of the struct should be in PascalCase, meaning, the first letter of each word in a compound word is capitalized.

    • If this case is not followed, a warning, ⚠️, is generated by the compiler.

Note: The order in which you assign values to items does not matter.

Access Values from a Struct

To access any value from the struct write the struct name followed by the . operator and then the name of the item to be accessed.

Note: A struct instance is immutable by default. Therefore it cannot be updated unless made mutable. However, the values can be accessed.

Update a Struct Instance

A struct instance can be made mutable by adding a mut keyword after the let followed by the instantiation of the struct. Now that the struct instance is mutable, any item can be accessed using the dot operator and the value of the item can be updated.

  • Example

The following example creates a struct named Course and defines three items of it: course name, course level, and course code.

//declare a struct
struct Course {
   code:i32,
   name:String,
   level:String, 
}

fn main() {
   //initialize
   let mut course1 = Course  {
      name:String::from("Rust"),
      level:String::from("beginner"),
      code:130,
   };
   let course2 = Course  {
      name:String::from("Javascript"),
      level:String::from("beginner"),
      code:122,
   };
   //access
   println!("Name:{}, Level:{}, code: {}", course1.name, course1.level, course1.code);
   println!("Name:{}, Level:{}, code: {}", course2.name, course2.level, course2.code); 
   //update
   course1.name = "Java".to_string();
   course1.code = 134;
   println!("Name:{}, Level:{} ,code: {}", course1.name, course1.level, course1.code);
}

output

Name:Rust, Level:beginner, code: 130
Name:Javascript, Level:beginner, code: 122
Name:Java, Level:beginner ,code: 134

Functions and Structs

  • Often, we need to pass a struct instance to a function. For example, in the previous lesson, every time we wanted to print a new struct instance we had to write a new print macro to print it. However, we can avoid multiple print statements by writing one print statement within a function and calling it when we need it.

Pass Structs to a Function

  • The structs can be passed to a function and the function can be invoked when required.

//declare a struct
struct Course {
   code:i32,
   name:String,
   level:String, 
}
fn display_mycourse_info(c:Course) {
println!("Name:{}, Level:{} ,code: {}", c .name, c .level, c.code);
}
fn main() {
   //initialize
   let course1 = Course  {
      name:String::from("Rust"),
      level:String::from("beginner"),
      code:130
   };
   display_mycourse_info(course1);
    let course2 = Course  {
      name:String::from("Java"),
      level:String::from("beginner"),
      code:130
   };
   display_mycourse_info(course2);
}

output

Name:Rust, Level:beginner ,code: 130
Name:Java, Level:beginner ,code: 130

Return Structs From a Function

Structs can also be returned from the functions.

//declare a struct
struct Course {
   code:i32,
   name:String,
   level:String, 
}
fn return_rust_course_info(c1:Course, c2:Course)-> Course{
   println!("I got into function and return values from there");
   if c1.name == "Rust" {
      return c1;
   }
   else{
      return c2;
   }
}

fn main() {
   //initialize
   let course1 = Course  {
      name:String::from("Rust"),
      level:String::from("beginner"),
      code:130
   };
    let course2 = Course  {
      name:String::from("Java"),
      level:String::from("beginner"),
      code:130
   };

  let choose_course = return_rust_course_info(course1, course2);
  println!("I choose to learn {} {} course with code:{}", choose_course.name, choose_course.level, choose_course.code);
}

output

I got into function and return values from there
I choose to learn Rust beginner course with code:130

What Are Static Methods?

  • Static methods are the ones that can be invoked without instantiating the struct.

Declare a Static Method

The following illustration explains how to declare a static method within the impl construct.

Note: If the construct is declared with an impl keyword, it must have one or both types of methods, static or non-static.

Invoke a Static Method

A static method can be invoked by following the struct name with the membership operator:: followed by the method name :

Example

The following example creates a static method my_static_method and invokes it from the main function.

// declare a struct

struct Course {

   name: String,

   level:String,

   code: i32,

}

impl Course {

   // static method

   fn my_static_method(n: String, l: String, c:i32) -> Course {

      Course { 

      name: n, 

      level:l,

      code:c

       }

   }

   //display

   fn display(&self){

      println!("name :{} code:{} of type: {}", self.name, self.code, self.level );

   }

}

fn main(){

   // call the static method

   let c1 = Course::my_static_method("Rust".to_string(), "beginner".to_string(), 132);

   c1.display();

}

output

name :Rust code:132 of type: beginner

Tuple Structs

What Are Tuple Structs?

  • Tuple Structs are a data type that is a hybrid between a tuple and a struct.

Why a tuple struct?

In the example above, when it is only a tuple we don’t know explicitly what each item in the tuple means. But when it is a tuple struct, we can assign a name to each item.

Define a Tuple Struct

  • Tuples can be of type struct by adding the struct keyword before the tuple name, followed by the data type of the variables enclosed within round brackets.

Initialize a Tuple Struct

A tuple struct can be initialized like a tuple.

Access a Tuple Struct

The tuple struct can be accessed using a . operator like a traditional struct.

Example

The following example declares a tuple struct named FruitQuantity.

//define a tuple struct
struct FruitQuantity(String, i32);
// main function
fn main() {
    // create an instance
    let r1 = FruitQuantity("oranges".to_string(), 12);
    // access values of a tuple struct
    println!("r1--name:{} quantity:{}", r1.0, r1.1);
    // create an instance
    let r2 = FruitQuantity("mangoes".to_string(), 13);
    // access values of a tuple struct
    println!("r2--name:{} quantity:{}", r2.0, r2.1);   
}

output

r1--name:oranges quantity:12
r2--name:mangoes quantity:13

main.rs

#[allow(unused_variables, unused_mut)]
use std::fs::File;
use std::io::{BufRead, BufReader};

fn main() {
   let filename = "sublist_input.txt";
   // Open the file in read-only mode (ignoring errors).
   let file = File::open(filename).unwrap();
   let reader = BufReader::new(file);
   let mut vec1 = vec![];
   let mut vec2 = vec![];
   // Read the file line by line using the lines() iterator from std::io::BufRead.
   for (index, line) in reader.lines().enumerate() {
       let line = line.unwrap(); // Ignore errors.
       // Show the line and its number.
       println!("{}", line.to_string());
       let mystr = line.to_string();
       let bar: Vec<&str> = mystr.split(", ").collect();
       let baz = bar.iter().map(|x| x.parse::<i64>());

       for x in baz {
           match x {
               Ok(i) => if index % 2 == 0 {vec1.push(i)} else {vec2.push(i)},
               Err(_) => println!("parse failed {:?}", x),
           }
       }
       if index % 2 == 1{
        println!("vec 1{:?}", vec1);  
        println!("vec 2{:?}", vec2);   

        println!("{}", Comparison::Superlist == sublist(&vec1, &vec1));
        println!("{}", Comparison::Equal == sublist(&vec1, &vec1));
        println!("{}", Comparison::Superlist == sublist(&vec2, &vec1));
        println!("{}", Comparison::Sublist == sublist(&vec1, &vec2));
        println!("{}", Comparison::Unequal == sublist(&[1, 2, 1, 2, 3], &[1, 2, 3, 1, 2, 3, 2, 3, 2, 1]));
       }

}
}


#[derive(Debug, PartialEq, Eq)]
enum Comparison {
    Equal,
    Sublist,
    Superlist,
    Unequal,
}


fn sublist(a : &[i64], b : &[i64]) -> Comparison {
    if a == b {
        Comparison::Equal
    } else if contains(a, b) {
        Comparison::Superlist
    } else if contains(b, a) {
        Comparison::Sublist
    } else {
        Comparison::Unequal
    }
}

fn contains(a: &[i64], b: &[i64]) -> bool {
    if a.len() < b.len() {
        return false;
    }

    if a.starts_with(b) {
        return true;
    }

    contains(&a[1..], b)
}

sublist_input.txt

3, 4, 5
1, 2, 3, 4, 5

output

3, 4, 5
1, 2, 3, 4, 5
vec 1[3, 4, 5]
vec 2[1, 2, 3, 4, 5]
false
true
true
true
true

Introduction to Enums

  • What Are Enums? Enum is a custom data type that is composed of variants.
 Variants are values which are definite.

The key is to enumerate all possible values and select one of the values from the list.

  • Let’s consider a real-life example to understand the concept of enums. The traffic signal can have
    only three possible states: red, yellow and green for stop, slow down, and go respectively.

Declare an Enum

Enums are declared using the enum keyword followed by the name of the enum and then the body of the enum enclosed within curly braces { }. Within the curly braces, the variants of the enum are defined.

Naming Convention

  • The name of the enum and it’s variants are written in CamelCase.

Initialize an Enum

Enums are initialized using the name of the enum followed by a double colon(::) and then specify the name of the variant of the enum.

Note: To print the values of an enum, write #[derive(Debug)] at the beginning of the program code. Use the debug trait{:?} for printing the variants.

  • Example The following example declares an enum named KnightMoves.

Note: To keep things simple, two variants are mentioned. However, a Knight in chess can move be in four directions. It moves to a square that is two squares away horizontally and one square vertically, or two squares vertically and one square horizontally

// make this `enum` printable with `fmt::Debug`.

#[derive(Debug)]

enum KnightMove{

   Horizontal, Vertical

}

fn main() {

   // use enum

   let horizontal_move = KnightMove::Horizontal;

   let vertical_move = KnightMove::Vertical;

   // print the enum values

   println!("Move 1: {:?}", horizontal_move);

   println!("Move 2: {:?}", vertical_move);

}

output

Move 1: Horizontal
Move 2: Vertical

Enums With Data Type

By default, the Rust compiler infers the data type for all variants of an enum. However, it is possible to use different data types for different variants of an enum.

Syntax

  • The data type can be added to each variant enclosed within round brackets ().

  • Example The following example makes an enum KnightMove having two variants Horizontal and Vertical both of type String.

// make this `enum` printable with `fmt::Debug`.

#[derive(Debug)]

enum KnightMove{

   Horizontal(String), Vertical(String)

}

fn main() {

   // invoke an enum

   let horizontal_move = KnightMove::Horizontal("Left".to_string());

   let vertical_move = KnightMove::Vertical("Down".to_string());

   // print enum

   println!("Move 1: {:?}", horizontal_move);

   println!("Movw 2: {:?}", vertical_move);

}

output

Move 1: Horizontal("Left")
Movw 2: Vertical("Down")

Methods of Enums

  • What Are Methods? Just like structs, methods are functions specific to enums.

  • Syntax

To define methods of enum write the functions within the impl followed by the enum name and then the functions within the impl block.

  • Move enum related logic in the impl construct

  • Example The example below declares an enum, named TrafficSignal and defines an enum method is_stop within the impl construct:

#![allow(dead_code)]
#[derive(Debug)]
// declare an enum
enum TrafficSignal{
  Red, Green, Yellow
}
//implement a Traffic Signal methods
impl TrafficSignal{
  // if the signal is red then return
   fn is_stop(&self)->bool{
     match self{
       TrafficSignal::Red=>return true,
       _=>return false
     }
   }
}
fn main(){
  // define an enum instance
  let action = TrafficSignal::Red;
  //print the value of action
  println!("What is the signal value? - {:?}", action);
  //invoke the enum method 'is_stop' and print the value
  println!("Do we have to stop at signal? - {}", action.is_stop());
}

Output

What is the signal value? - Red
Do we have to stop at signal? - true

Enums and Match Control Flow Operator

The match statement can be used to compare values within an enum. The match statement can be written within a main function or any other user-defined function.

Syntax

The match statement can be written within a function be it main or any other user-defined function.

Example

The example below makes use of a match statement within a print_direction function.

enum KnightMove{

   Horizontal,Vertical

}

// print function 

fn print_direction(direction:KnightMove) {

   // match statement

   match direction {

      //execute if knight move is horizontal

      KnightMove::Horizontal => {

         println!("Move in horizontal direction");

      },

       //execute if knight move is vertical

      KnightMove::Vertical => {

         println!("Move in vertical direction");

      }

   }

}

fn main() {

   // invoke function `print_direction`

   let knight1 = KnightMove::Horizontal;

   let knight2 = KnightMove::Vertical;

   print_direction(knight1);

   print_direction(knight2);

}

output

Move in horizontal direction
Move in vertical direction

Enums and Structures

Structures can have an item that is of type enum.

  • Syntax

The following illustration explains the syntax:

  • Example

The following example creates an enum KnightMove and a struct Player.

// make this `enum` printable with `fmt::Debug`.

#[derive(Debug)]

//define an enum

enum KnightMove{

   Horizontal, Vertical

}

#[derive(Debug)]

// make this `struct` print values of type `enum`  with `fmt::Debug`.

struct Player {

   color:String,

   knight:KnightMove

}

fn main() {

      // instance 1

      let p1 = Player{

      color:String::from("black"),

      knight:KnightMove::Horizontal

   };

      // instance 2

      let p2 = Player{

      color:String::from("white"),

      knight:KnightMove::Vertical

   };

   println!("{:?}", p1);

   println!("{:?}", p2);

}

output

Player { color: "black", knight: Horizontal }
Player { color: "white", knight: Vertical }

What Is Option?

Option is a built-in enum in the Rust standard library. It has two variants Some and None.

  • Variants:

    • Some(T), returns Some value T

    • None, returns no value

When to Use Option?

  • Options is a good choice when:

    • The return value is none

      • Rust avoids including nulls in the language, unlike other languages. For instance, the function that returns a value may actually return nothing. So, here the Option variant None comes in handy.
    • The value of the variable is optional

      • The value of any variable can be set to some value or set to none.
    • Out of bound exception is to be displayed

      • This is useful in the case of an array, string or a vector when an invalid index number tries to access it.

Example 1: Return Value Is None

The following example shows that if the else construct has no value then it can simply return None.

fn main() {

   println!("{:?}", learn_lang("Rust"));

   println!("{:?}", learn_lang("Python"));

}

fn learn_lang(my_lang:&str)-> Option<bool> {

   if my_lang == "Rust" {

      Some(true)

   } else {

      None

   }

}

output

Some(true)
None

Note: None does not take a parameter unlike Some.

Example 2: Optional Variable Value

The following example makes level variable of the struct Course as Option of type String. That means that it’s optional to set any value to it. It can be set to some value or it can be set to none.

//declare a struct
struct Course {
   code:i32,
   name:String,
   level: Option<String>, 
}
fn main() {
   //initialize
   let course1 = Course  {
      name:String::from("Rust"),
      level:Some(String::from("beginner")),
      code:130
   };
   let course2 = Course  {
      name:String::from("Javascript"),
      level:None,
      code:122
   };
   //access
   println!("Name:{}, Level:{} ,code: {}", course1.name, course1.level.unwrap_or("Level".to_string()), course1.code);
   println!("Name:{}, Level:{} ,code: {}", course2.name, course2.level.unwrap_or("No level defined!".to_string()), course2.code);
}

output

Name:Rust, Level:beginner ,code: 130
Name:Javascript, Level:No level defined! ,code: 12

Example 3: Index Out of Bound Exception

The example below uses a match statement that takes an index of string using match.str.chars().nth(index_no) and executes the Some block if index_no is in range and None block otherwise.

fn main() {

  // define a variable

  let str = String :: from("Educative");

  // define the index value to be found

  let index = 12;

  lookup(str, index);

}

fn lookup(str: String, index: usize) {

  let matched_index = match str.chars().nth(index){

    // execute if match found print the value at specified index 

     Some(c)=>c.to_string(),

     // execute if value not found

     None=>"No character at given index".to_string()

     };  

  println!("{}", matched_index);

}

output

No character at given index

is_some() , is_none() Functions

Rust provides is_some() and is_none() to identify the return type of variable of type Option, i.e., whether the value of type Option is set to Some or None.

Example 1

The following example checks whether the variable value of type Option is set to Some or None.

fn main() {
    let my_val: Option<&str> = Some("Rust Programming!");
    print(my_val); // invoke the function

}
fn print(my_val: Option<&str>){
     if my_val.is_some(){ // check if the value is equal to some value
        println!("my_val is equal to some value");
    }
    else{
        println!("my_val is equal to none");
    }
}

output

my_val is equal to some value

We need to do is to ensure that these functions return true or false. That’s where assert_eq and assert_ne functions come in handy.

  • Assert Macros

    • assert_eq!(left, right) - evaluates to true if left value is equal to that of right

    • assert_ne!(left, right) - evaluates to true if left value is not equal to that of right

The output of assert expression?

  • If the assertion passes no output is displayed, and if doesn’t the code gives an error saying that the assertion failed

Example 2

The following example uses the assert_eq! macro to check whether the variable value of type Option is set to Some or None.

Note: The assertion passes since the expression evaluates to true.

fn main() {
    let my_val: Option<&str> = Some("Rust Programming!");
    // pass since my_val is set to some value so left is true, and right is also true
    assert_eq!(my_val.is_some(), true); 
    // pass since my_val is set to some value so left is false, and right is also false
    assert_eq!(my_val.is_none(), false);
}

Result and Enum

  • What Is Result?

Result is a built-in enum in the Rust standard library. It has two variants Ok(T) and Err(E).

  • Variants:

    • Ok(T), returns the success statement of type T

    • Err, returns the error statement of type E.

  • When to Use Result?

    • Result should be used as a return type for a function that can encounter error situations. Such functions can return an Ok value in case of success or an Err value in case of an error.
  • Result and Function

    • Using Result as a function return type can be used to return various kinds of success and error codes to let the calling function decode the execution state of the called function.

Example 1

The following code has a function file_found which takes a number i and returns a Result of type i32, in case of variant Ok and bool, in case of Err.

fn main() {
   println!("{:?}",file_found(true)); // invoke function by passing true 
   println!("{:?}",file_found(false)); // invoke function by passing false
}
fn file_found(i:bool) -> Result<i32,bool> {
   if i { // if true
      Ok(200) // return Ok(200)
   } else { // if false
      Err(false) // return Err(false)
   }
}

output

Ok(200)
Err(false)

Example 2

The following code has a function divisible_by_3 which takes a number i and returns a Result of type String in case of both variants Ok and Err. If i is divisible by 3 Ok(Given number is divisible by 3) is returned and Err(Given number is not divisible by 3).

fn main() {
   println!("{:?}", divisible_by_3(6)); // invoke function by passing a number 6
   println!("{:?}", divisible_by_3(2)); // invoke function by passing a number 2
}
fn divisible_by_3(i:i32)->Result<String,String> {
   if i % 3 == 0 { // if number mod 3 equals 0
      Ok("Given number is divisible by 3".to_string()) // return this statement
   } else { // if if number mod 3 is not equals 0
      Err("Given number is not divisible by 3".to_string()) // return this statement
   }
}

output

Ok("Given number is divisible by 3")
Err("Given number is not divisible by 3")

is_ok(), is_err() Functions

Rust helps you to check whether the variable of type Result is set to Ok or Err.

fn main() {
   let check1 = divisible_by_3(6);
   if check1.is_ok(){ // check if the function returns ok
      println!("The number is divisible by 3");
   }
   else{
      println!("The number is not divisible by 3");

   }
   let check2 = divisible_by_3(2);
   if check2.is_err(){ // check if the function returns error
      println!("The number is not divisible by 3");
   }
   else{
      println!("The number is divisible by 3");

   }
}
fn divisible_by_3(i:i32)->Result<String,String> {
   if i % 3 == 0 { // check i modulus 3
      Ok("Given number is divisible by 3".to_string())
   } else {
      Err("Given number is not divisible by 3".to_string())
   }
}

output

The number is divisible by 3
The number is not divisible by 3

Example 2

The following example uses the assert_eq! macro to check whether the variable value of type Result is set to Ok or Err.

fn main() {
   let check1 = divisible_by_3(6);
   assert_eq!(check1.is_ok(), true);  // left is true and right is true so the assertion passes
   let check2 = divisible_by_3(2);
   assert_eq!(check2.is_err(), true); // left is true and right is true so the assertion passes
}
fn divisible_by_3(i:i32)->Result<String,String> {
   if i % 3 == 0 {
      Ok("Given number is divisible by 3".to_string())
   } else {
      Err("Given number is not divisible by 3".to_string())
   }
}

Note: The assertion passes since the expression evaluates to true.

Traits

When there are multiple different types behind a single interface, the interface can tell which concrete type to access. This is where the traits come in handy.

  • What Are Traits?

Traits are used to define a standard set of behaviour for multiple structs.

They are like interfaces in Java.

Suppose you want to calculate the area for different shapes. We know that the area is calculated differently for every shape. The best solution is to make a trait and define an abstract method in it and implement that method within every struct impl construct.

Types of Methods in Traits

There can be two types of methods for traits

  • Concrete Method

    • The method has a body meaning that implementation of the method is done within the method.
  • Abstract Method

    • The method that does not have a body means that implementation of the method is done by each struct in its own impl construct.

Declare a Trait

Traits are written with a trait keyword.

  • Naming Convention Name of the trait is written in CamelCase

Implement a trait

Traits can be implemented for any structure.

Example

The following example explains the concept of trait:

fn main(){

   //create an instance of the structure

   let c = Circle  {

      radius : 2.0,

   };

   let r = Rectangle  {

      width : 2.0,

      height : 2.0,

   };

   println!("Area of Circle: {}", c.shape_area());

   println!("Area of Rectangle:{}", r.shape_area());

}

//declare a structure

struct Circle {

   radius : f32,

}

struct Rectangle{

    width : f32,

    height : f32,

}

//declare a trait

trait Area {

   fn shape_area(&self)->f32;

}

//implement the trait

impl Area for Circle {

   fn shape_area(&self)->f32{

      3.13* self.radius *self.radius

   }

}

impl Area for Rectangle {

   fn shape_area(&self)->f32{

      self.width * self.height

   }

}

output

Area of Circle: 12.52
Area of Rectangle:4

Introduction to Modules

  • What Are Modules?

    • Modules are a collection of items that can contain structs, functions, enums, vectors, arrays, etc.

💡Why make a module?

  • As a result of making modules,

    • the program code becomes organized.

    • you can use the same name for things like a struct. For example, you can use the name Configuration in different modules and code it differently. Otherwise, you would have different clumsy names for the struct like EngineConfiguration, ConsoleConfiguration etc.

Declare a Module

To declare a module in Rust use the mod keyword followed by the name of the module and the body of the module within curly braces { }.

  • Naming Convention

    • Name of the module should be written in snake_case.

Invoking a Module

The module can be invoked from anywhere within the program code.

Keywords for Modules

  • The following keywords are used for declaring modules:

    • mod - declares a new module

    • pub - makes a public module

    • use - imports the module in the local scope

Note: Modules are declared by the mod keyword and are private by default.

Example

The following example makes use of the mod keyword to declare a module named r, and defines a function print_statement within the module:

// declare a module

mod r {

  fn print_statement(){

    println!("Hi, this a function of module r");

  }

}

// main function

fn main() {

  // invoke a module 'r'

   r::print_statement();

}

The above code generates an error, ❌, because the function print_statement is private

Controlling Visibility Within the Same File Using 'pub'

The pub keyword makes the item public and visible outside its scope.

Privacy Rules

The following are two privacy rules when declaring modules:

  • Rule No: 1

If an item is public it can be accessed from anywhere, i.e., within main function or any other module.

Example: Invoke a Public Function Directly

The following example declares a function public print_statement() within the mod r:

// declare a module

mod r {

  pub fn print_statement(){

    println!("Hi, this a function of module r");

  }

}

// main function

fn main() {

  println!("Let's go inside the module");

  // invoke a module 'r'

   r::print_statement();

}

output

Let's go inside the module
Hi, this a function of module r

Rule No: 2

  • If an item is private it can be accessed using its parent module meaning it can be accessed within the module but not outside it.

  • Example: Invoke a Private Function Indirectly through a Public Function

  • The example declares a module mod r which has two functions:

    • A public function my_public_function()

    • A private function my_private_function().

self can refer to a function or any item within the same module.

// declare a module

mod r{

  fn my_private_function(){

    println!("Hi, I'm a private function within the module");

  }

  pub fn my_public_function(){

    //! also works without writing self i.e.

    //! my_private_function();

    println!("Hi,I'm a public function within the module");

    println!("I'll invoke private function within the module");

    self::my_private_function(); 



  }

}

// main function

fn main() {

  println!("Let's go inside the module");

  // invoke a module 'r'

   r::my_public_function();

}

output

Let's go inside the module
Hi,I'm a public function within the module
I'll invoke private function within the module
Hi, I'm a private function within the module
If an item is private, it can be called from within the child module.

Example: Access a Private Function through a Child Module

📝If there is a module within the module, then the outer module is called the parent module and the module inside the parent module is called the child module. This is known as a nested module

The example declares a module mod outer_module which has:

  • A private functionmy_private_function().

  • inner_module - Inner module has one public function my_public_function()

The following example shows how the private function is accessed in the child module using the keyword super followed by :: and the function name in the parent module.

super keyword refers to the parent module.


// main function
fn main() {
 println!("Let's go inside the module");
 outer_module::inner_module::my_public_function();
}
// declare a module
mod outer_module {
 // function within outer module
 fn my_private_function() {
   println!("Hi, I got into the private function of outer module");
 }
 // declare a nested module
 pub mod inner_module {
   // function within nested module
   pub fn my_public_function() {
     println!("Hi, I got into the public function of inner module");
     println!("I'll invoke private function of outer module");
     super::my_private_function();
   }
 }
}

output

Let's go inside the module
Hi, I got into the public function of inner module
I'll invoke private function of outer module
Hi, I got into the private function of outer module

Even though the function my_private_function() is declared private, the main() function can invoke it indirectly because the function it calls is public.

Example: Access a Root Function

The example below shows how the root function (a function that exists outside the module) can be accessed within the function of a module. Write super:: followed by the root function name.

super can allow accessing a root function from within the module.


// main function
fn main() {
  println!("Let's go inside the module");
  my_module ::my_public_function();
}
fn my_function(){
  println!("Hi, you came inside the root function using super");
  }

// declare a module
mod my_module {
  // function within outer module
  pub fn my_public_function() {
    println!("Invoke root function");
    super::my_function();
  }
}

output

Let's go inside the module
Invoke root function
Hi, you came inside the root function using super

Control Visibility Within Different Files Using 'pub'

When modules get large and become cumbersome to store in a single file, it is possible to move their definitions to a separate file to make the code easier to navigate. It is possible to access a module even if it belongs to a different file. To use the module in a different file, write mod followed by the name of the file in which the module is declared.

  • Implicit Declaration A block of code put in a file without wrapping in a mod block implicitly declares a module.

  • Import the module
mod file_name
  • Call the module
file_name::x

Where x can be any construct within the module, i.e., function, array, trait, struct.

📝 Rust code is always put in files with .rs extension

Explicit Declaration

The code in a file is wrapped within the mod block. This explicitly declares a module.

  • Import the module
mod file_name
  • Call the module
 file_name::module_name::x

where x can be any construct within the module, i.e., function, array, trait, struct.

Privacy Rule

  • If the module belonging to some other file is to be made accessible then it should be made public by using the pub keyword before the mod.

📝Once the module is made public using pub, all privacy rules for defining modules within the same file apply.

Example

The following example shows how a module in another file can be accessed.

  • Implicit declaration The following example declares a module implicitly in a file my_mod.rs and calls it from main.rs. Note: In implicit declaration modules are public by default

main.rs

mod my_mod; 

fn main() {

  println!("Invoke function in my_mod.rs");  

  my_mod::my_public_function();

}

my_mod.rs

// declare a module
pub fn my_public_function() {
    println!("I am a public function in my_mod.rs");
}

output

Invoke function in my_mod.rs
I am a public function in my_mod.rs

Explicit declaration

  • The following example declares a module in a file my_mod.rs and call it from main.rs.

main.rs

mod my_mod; 

fn main() {

  println!("I am a public function in my_mod.rs");

  my_mod::module::my_public_function();

}

my_mod.rs

// declare a module
pub mod module{
pub fn my_public_function() {
    println!("I am a public function in my_mod.rs");
}
}

output

I am a public function in my_mod.rs
I am a public function in my_mod.rs

The 'use' Keyword

Why Use the use Keyword?

The benefit is greatest when items in the same module need to be referred to in the code again and again. Now, we don’t have to type the entire path over and over.

The following example shows how we can write a precise code using a use keyword:

pub mod chapter {
    pub mod lesson { // mod level 1
        pub mod heading { // mod level 2
            pub fn illustration() {  // mod level 3
              println!("Hi, I'm a 3rd level nested function");
            }
        }
    }
}
use chapter::lesson::heading; // make use of `use` keyword
fn main() {
    heading::illustration(); // call the function
}

output

Hi, I'm a 3rd level nested function

Glob Operator ( * )

The glob operator helps you to prevent writing EnumName::variant when assigning enum value to a variable.

the following example shows how we can avoid writing a lengthy code using the glob operator *.

// make this `enum` printable with `fmt::Debug`.

#[derive(Debug)]

enum KnightMove{

   Horizontal,Vertical

}

use KnightMove::*; // use of globe operator

fn main() {

   // use enum

   let horizontal_move = Horizontal; // Horizontal is shortcut for KnightMove::Horizontal

   let vertical_move = Vertical; // Vertical is shortcut for KnightMove::Vertical

   // print the enum values

   println!("{:?}", horizontal_move);

   println!("{:?}", vertical_move);

}

output

Horizontal
Vertical

Memory Management

In many programming languages, there is no need to bother about where the memory is allocated. But in system programming languages like Rust, a program’s behaviour depends upon the memory being used, i.e., stack or heap.

Stack

  • When the size of data is known at compile-time, a stack is used for storage.

What Is a Stack?

A stack is a Last in First Out(LIFO) data storage memory location meaning all values are stored in a last-in-first-out order. Let’s imagine a real-life analogy to understand stack. There is a huge pile of books with a wall around it. You want the one in the middle. You can’t just slip one from the middle. So, you’ll remove items until the desired location is reached. Inserting values onto the stack is a push operation and removing values from the stack is a pop operation.

  • Example All primitive data types that have a fixed size are stored on a stack.

  • Illustration The following example has a variable a and a variable b. Both are initialized to 1 and stored on the stack.

Heap

When the size of data is not known at compile time rather it is known at the run time, it goes in a portion of program memory called a heap.

What Is a Heap?

Heap is a big data storage and stores values whose size is unknown at compile time. The operating system allocates a space in the heap that is adequate, marks it as in use, and returns a pointer, which is the address of that location.

Let’s imagine a real-life analogy to understand heap. Suppose there are 4 students in a class and the classroom has the accommodations for 8. One new student gets enrolled and is directed to the classroom. Now the total students in the class becomes 5.

  • Example Since Vectors and String Objects are resizable, they are stored in a heap.

  • A String is made up of three parts:

    • A pointer to the memory that holds the contents of the string

    • Length

    • Capacity

  • This group of the information above is stored on the stack.

  • The memory on the heap holds the value assigned to the string. Below is the example of string object str which is initialized to Rust.

Here, ptr is a pointer to the base address of the string str. len is the total length of the string in bytes and capacity is the total amount of memory that the operating system has provided to the string.

Stack vs. Heap

- Pushing values onto the stack is much easier than heap since the operating system does not have to find a big space in memory and mark the pointer for the next allocation.

- Accessing data from the stack is much faster than heap since the processor will take less time to access the data that is closer to the other data.

Ownership

  • What Is Ownership?

    • Ownership in simple terms means to have possession of something.

    • Let’s look at a real-life analogy to explain this concept. If something belongs to you, you say “It’s mine”.

    • Similarly, in Rust, variable bindings can have ownership of what they are bound to.

Three Rules of Ownership

The following are three rules of ownership:

Rule 1

Each value has a variable binding called its owner.

Rule 2

There can only be one owner at a time.

Rule 3

  • When an owner goes out of scope, it does not remain accessible.

    • When the variable goes out of scope, Rust calls function drop automatically at the closing curly bracket (to deallocate the memory).

fn main() {

 let a = 1; // variable a is the owner of the value 1
 let b = 1; // variable b is the owner of the value 1
 let c = 3; // variable c is the owner of the value 3
 println!("a : {}", a);
 println!("b : {}", b);
 println!("c : {}", c);

}// value a, b, c are out of scope outside this block

output

a : 1
b : 1
c : 3
  • When using the assignment, two situations happen:

    • The value gets copied

    • The value gets moved

Copy Type and Moved Type

Copy Type

  • The ownership state of the original variable (whose value is assigned to another variable) is set to copied state. This means that the value of the assignee variable is copied to the assigned variable. A copy of the value is created so that the assignee variable gets ownership of the value but both variables have their copies.

    • Variable assignment in case of primitive data type is a copy type.

    • pass by value in a function call is an example of copy type

Why is a primitive type copied?

  • Primitive types are stored on the stack and it’s fast and cheap to copy them.

Example 1

The following example creates a variable a of type int and assigns the value of a to variable b. Note: The value of a is copied to b

fn main() {

    let a = 1;

    let b = a; // copy of 'a' is created

    println!("a:{} , b:{}", a, b); // print 'a' and 'b'

}

output


a:1 , b:1

Example 2

The following example creates a variable an of type array and assigns the value of a to variable b.

Note: The value of a is copied to b
fn main() {

    let a = [1,2,3];

    let b = a; // copy of 'a' is created 

    println!("a:{:?} , b:{:?}", a, b); // print 'a' and 'b'

}

output


a:[1, 2, 3] , b:[1, 2, 3]
Moved Type

The ownership state of the original variable (whose value is assigned to another variable) is set to moved.This means that the original variable binding cannot be accessed.

  • Variable assignment in case of Non-primitive types such as String Object and Vectors is a moved type.

Why is non-primitive type moved?

Non-primitive types are stored on the heap. When one variable is assigned to another variable,

two variables will point to the same value. This can’t happen since it violates ownership rule 2 - if one string/vector will

have two owners and one of them changes the value of string there is no way for the other to know. When there is time to clean up the

memory, both will try to find the string to clean it. This would lead to memory corruption. To avoid this, the Rust compiler moves the

owner to the assigned variables and makes the other one inaccessible.

  • Example 1

The following example creates a variable an of type String and assigns the value of a to variable b. Note: The following code gives an error, ❌, since the value of a is moved to b and a becomes inaccessible.

fn main() {

   let a = String::from("Rust");

   let b = a; // moves value of 'a' to 'b'

   eprintln!("a:{} , b:{}", a, b); // Error use of moved value 'a'

}
  • Example 2

The following example creates a variable a of type vector and assign the value of a to variable b.

Note: The following code gives an error, ❌, if the commented statement is uncommented since the value of a is moved to b and a becomes inaccessible
fn main() {

   let a = vec![2, 4, 8];

   let b = a; // move value of 'a' to 'b'

   println!("b : {:?} ", b); // prints 'b'

   //println!("{:?} {:?}", a, b); // Error; use of moved value: 'a'

}

output

b : [2, 4, 8]

The clone Keyword

If you still want both variables to have the same value and be able to use both variables, it is possible to copy the value of one variable to the other using the clone function.

fn main() {

    let mut a = String::from("Rust"); // define a String and save in 'a'

    let b = a.clone(); // b clones a

    a.push('y');

    println!("a:{} , b:{}", a, b);  // print 'a' and 'b'

}

output

a:Rusty , b:Rust

Ownership and Functions

the assignment of a variable to another variable will copy or move it. In case of passing variables to the functions, similar can happen.

When a variable whose memory is allocated on heap goes out of scope, the value will be cleaned up by drop unless the data has been moved such that it is now being owned by another variable.

Passing Values to a Function

  • The ownership of the variable is

    • Copied if the value is a primitive data type so the variable can be reused after the function call

    • Moved if the value is a non-primitive data type so the value becomes inaccessible after the function call

   fn main() {
    let str = String::from("Rust"); // str comes into scope
                                    // str is a move type

    pass_string_object(str);        // str's value moves into the function...
                                    // ... and becomes in accessible here
    //println!("{}" , str);         // This line will give an error

    let my_int = 10;                // my_int comes into scope

    pass_integer(my_int);          // my_int value is a copy into the function,
                                    // but i32 is a copy type, so can my used
                                    // use my_int if desired

} // Here, my_int and then str goes out of scope

fn pass_string_object(my_string: String) { // my_string comes into scope
    println!("{}", my_string);
} // Here, my_string goes out of scope and `drop` frees the memory

fn pass_integer(my_integer: i32) { // my_integer comes into scope
    println!("{}", my_integer);
} // Here, my_integer goes out of scope

output

Rust
10

In this example, value str of type String is moved when passed to the function as an argument and my_int of type i32 is copied.

Return Values from a Function

Returning values from a function transfers the ownership to the caller function.


#[allow(dead_code)]
fn main() {

    let str_1 = move_return_value_str_1();  // gives_ownership to str_1                                  

    println!("The function gives ownership to string by returning a value \nstring 1 :{}",str_1); // print value of str_1

    let str_2 = String::from("Rust Language");     // assigns a string object to str_2

    println!("This is a string declared \nstring 2 :{}",str_2); // print value of str_2   

    let str_3 = moves_str_2_return_str_2(str_2);  // str_2 is moved into the function argument
                                            // return value moves to str_3 
    println!("string 2 passes to the function and returns its value to string 3 \nstring 3 :{}",str_3); // print value of str_3                         

} // Here, str_3,str_2,str_1 goes out of scope respectively
 // str_3 dropped
 // str_2 moved
 // str_1 dropped
fn move_return_value_str_1() -> String {     // gives ownership 
                                             // value goes to that calls the function
    let my_string = String::from("Rust"); // my_string comes into scope

    my_string                              // my_string is returned 
}

fn moves_str_2_return_str_2(my_string: String) -> String { // my_string comes into 
                                                      // scope
    my_string  // my_string is returned 
}

output

The function gives ownership to string by returning a value 
string 1 :Rust
This is a string declared 
string 2 :Rust Language
string 2 passes to the function and returns its value to string 3 
string 3 :Rust Language

Here, in this example, variable str_1 gains the ownership of a String when the value is returned from the function move_return_value_str_1. Variable str_2 is declared and its value is passed to the function moves_str_2_return_str_2. Upon being returned from the function the value is saved in str_3.

Note:str_2 becomes inaccessible since its value is moved in the function

Borrowing

  • What is Borrowing? Borrowing, in simple terms, means to share. Let’s look at a real-life analogy to explain this concept. You are the owner of a book, but you allow other people to use it.

Types of Borrowing

In Rust, borrowing can be of two types:

Shared Borrows

The ownership belongs to the assignee variable but the assigned variable can only read the value.

  • Multiple variables can borrow the value of the variable at the same time.

Mutable Borrows

The ownership belongs to the assignee variable but the assigned variable can share as well as mutate the value of owner variable.

  • Only one variable in the scope can borrow mutably. After the mutable borrow operation, the value of the mutably borrowed variable is moved to become inaccessible.

Rules of Borrowing

There are two rules to referencing or borrowing variables.

Rule 1

There can be either one mutable borrow or any number of immutable borrows within the same scope.

  • It is not possible to do a shared borrow as well as a mutable borrow operation simultaneously in the same scope. If you want to do this in the same program code, then enclose a block of code within {}. In the inner block perform the shared borrow, and in the outer block perform the mutable borrow. Vice versa, outer block perform the shared borrow, and in the inner block perform the mutable borrow.

  • Example #

The following example explains rule 1.

/// cannot mutable borrow b since its already a shared borrow

/// mutable borrow a in outer scope and shared borrow in inner scope

fn main() {
  let mut a = 1; // mutable variable a is defined
  println!("variable `a` :{}", a);
  let b = 1;
  println!("variable `b` :{}", b);
  {
      let r1 = &a; // no problem
      println!("variable `r1` references `a` in inner scope(SHARED BORROW(a)) :{}",r1);
      let r2 = &a; // no problem
      println!("variable `r1` references `a` in inner scope(SHARED BORROW(a) :{}",r2);
      println!("r1:{}\nr2:{}", r1, r2);
     // r1 and r2 scope end here
  }
  let r3 = &mut a; // no problem
  *r3 = 3;
  println!("variable `r1` references `a` in outer scope(MUTABLE BORROW(a) and derefernced it and changed value to 3) :{}",r3);
  let r4 = &b;
  println!("variable `r3` references `b` in outer scope(SHARED BORROW(b)) :{}",r4);

  let r5 = &b;

  println!("variable `r3` references `b` in outer scope(SHARED BORROW(b)) :{}",r5);

  println!("r3:{}\nr4:{}\nr5:{}", r3 , r4 , r5);

}

output

variable `a` :1
variable `b` :1
variable `r1` references `a` in inner scope(SHARED BORROW(a)) :1
variable `r1` references `a` in inner scope(SHARED BORROW(a) :1
r1:1
r2:1
variable `r1` references `a` in outer scope(MUTABLE BORROW(a) and derefernced it and changed value to 3) :3
variable `r3` references `b` in outer scope(SHARED BORROW(b)) :1
variable `r3` references `b` in outer scope(SHARED BORROW(b)) :1
r3:3
r4:1
r5:1

Rule 2

References must always be valid.

  • Cannot reference a value that is moved, i.e., a non-primitive data type.

  • Example The following example explains rule 2.

fn main() {
  let a = String::from("Rust"); //variable a
  println!("This is a variable a: {}", a);
  let b = a; // moves value of a to b
  println!("Value of variable a is moved to b.\n b : {}", b);
  println!("Now a becomes invalid.Accessing a will give error");
  //let c = &a;

  //println!("This is a variable c trying to access value a: {}", c);

}

output

This is a variable a: Rust
Value of variable a is moved to b.
 b : Rust
Now a becomes invalid.Accessing a will give error

Functions and Borrowing

Recall pass-by reference in which & mut was used as a function parameter when mutating values inside the function.

  • Example

The following example declares a function example which takes an owner variable, shared borrow and a mutable borrow as arguments to the function.

// 'a' an owner variable
// 'b' a shared borrow
// 'c' a mutable borrow
fn example(a: i32, b:& i32,c : &mut i32){
  println!("a: {}, b: {}, c: {}", a , b , c);
   *c=9;
}
fn main(){
   let a = 1;
   let b = 2;
   let mut c = 3;
   example( a, &b , &mut c);
   println!("a: {}, b: {}, c: {}", a , b , c);
}

output

a: 1, b: 2, c: 3
a: 1, b: 2, c: 9

Borrowing and Slicing

It is possible to borrow a slice of an array, vector or string. Recall the syntax of slicing. It used an & before the name of the variable to be borrowed.

let arr:[i32;4] = [1, 2, 3, 4]; 
let borrow_a = &arr[0..2];

let str=String::from("Rust Programming");
let borrow_str = &str[0..2];

let my_vec = vec![1, 2, 3, 4, 5];
let borrow_vec = &my_vec[0..2];

Here & indicates a shared borrow.

Example

The following examples revise your concept of slicing in arrays, strings, and vectors respectively.

fn main() {

  let arr:[i32;4] = [1, 2, 3, 4]; // define an array
  let borrow_arr = &arr[0..2]; // slice an array
  println!("arr : {:?}", arr); // print the array
  println!("sliced_arr : {:?}", borrow_arr); // print the sliced array
  let str = String::from("Rust Programming"); // define a String object
  let borrow_str = &str[0..2]; // slice the String object
  println!("str : {:?}", str); // print the String Object 
  println!("sliced_str : {:?}", borrow_str); // print the sliced String
  let my_vec = vec![1, 2, 3, 4, 5]; // define a vector
  let borrow_vec = &my_vec[0..2]; // slice the vector
  println!("vec: {:?}", my_vec);  // print the vector
  println!("sliced_vec : {:?}", borrow_vec); // print the sliced vector

}

output

arr : [1, 2, 3, 4]
sliced_arr : [1, 2]
str : "Rust Programming"
sliced_str : "Ru"
vec: [1, 2, 3, 4, 5]
sliced_vec : [1, 2]

Lifetimes

  • What Is a Lifetime?

Lifetimes describes the scope that a reference is valid for. Let’s look at a real-life analogy to explain this concept. You are the owner of a book, but you allow other people to use it for some time.

  • Let’s understand this concept in terms of memory management. Having a reference to a resource that someone else has the ownership of can be complicated. For example, imagine this set of operations:

    • I have some kind of resource.

    • I allow you to borrow the resource.

    • I decide my task is completed with the resource, and I want to deallocate it, while you still have the reference of my resource. You still want to use the resource.

  • Now you are referencing a resource that is deallocated, an invalid resource. This is called a dangling pointer or use after free, when the resource is memory.

  • To make sure that step four never happens, the concept of a lifetime comes into play!

There are two cases to consider.

Case 1: When we know the lifetime of a variable by just looking at the program code

#![allow(dead_code)] 
struct Course{
   name: String,
   id : i32,
}
fn main(){
  let c1:&Course;
    {
    let c2: Course = Course {
      name : String::from("Rust"),
      id:101,
    };
    }
    c1 = &c2; // allocated reference to a memory location that is dropped
}

The above example gives an error, ❌, since c1 references c2 which is deallocated when the scope of c2 finishes.

It is implicit that the scope of c2 finishes and it cannot be used after line 13. But when there is a function that takes an argument by reference, we can be either implicit or explicit about the lifetime of the reference

Case 2: When we can’t say anything about the lifetime of a variable

#![allow(dead_code)] 

struct Course{

   name: String,

   id : i32,

}

fn get_course(c1: &Course, c2: &Course)->&Course{

  if c1.name == "Rust" {

     c1

  }

  else {

     c2

  }

}

fn main(){

  let c1: Course = Course {

      name : String::from("Rust"),

      id:101,

    };



   let c2: Course = Course {

      name : String::from("C++"),

      id:101,

    };

    get_course(c1, c2);   

}

The above code gives an error, ❌, which says missing lifetime specifier. In the function definition, c1 is referencing Course and c2 is referencing Course. What if this particular piece of memory is invalid? And for how long does it remains valid? How can we ensure that the function is going to return a valid reference? The compiler is confused what the lifetime of the variable is. Solution: use the lifetime annotation.

How does the compiler know the lifetime of a variable?

  • The Rust compiler has a borrow checker. It compares the scope of the reference variable and the variable
    being referred. It ensures that the variable being referred to lives as long as the variable referencing it.

Lifetime Annotation

  • It describes the lifetime of a reference.

    • Starts with an apostrophe

    • Followed by a small case name, generally a single letter

Function With Reference Variable Having a Lifetime

Function With Mutable Reference Variable Having a Lifetime

  • Example

Now, coming back to the example in Case 2. The following code annotates the lifetime of variable c1, c2, and the return value.

#![allow(dead_code)] 

struct Course{

   name: String,

   id : i32,

}

fn get_course<'a> (c1: &'a Course, c2: &'a Course) -> &'a Course {

  if c1.name == "Rust" {

     c1

  }

  else {

     c2

  }

}

fn main(){

  let c1: Course = Course {

      name : String::from("Rust"),

      id:101,

    };

   let c2: Course = Course {
      name : String::from("C++"),
      id:101,
    };
    get_course(&c1, &c2);   
}

Here, lifetime annotation is used for parameters c1 and c2 meaning both parameters have the same lifetime as that of the function. It is trying to tell the compiler that there is a lifetime for which both of the references are valid and for the same lifetime the reference for the return type is also valid. Earlier the issue was that the time for which the return type was valid was unknown. Now, the borrow checker can check whether the reference is valid or not and gives us a compile-time error if it is invalid. Here, in this case, the reference is valid for `a.

The lifetime annotation <'a> is a relationship. It is not used standalone. If you don’t write this, the code will be correct syntactically, but the compiler will generate a compile-time error, ❌.

  • How is a calculated?

    • a refers to the lifetime in which all the references are valid, which is the overlap of the related lifetimes, or the common lifetime of n number of instances. As a result, the lifetime having the smallest reference of all references gets assigned to a.

Multiple Lifetimes

There are two possibilities when the reference variables are used as function parameters:

Multiple references have the same lifetime

fn fun_name <'a>(x: & 'a i32 , y: & 'a i32) -> & 'a i32 
  // statements

Here, the variable x and y have the same lifetime a.

Multiple references have different lifetimes

fn fun_name<'a , 'b>(x: & 'a i32 , y: & 'b i32) 
 // statements

Here, both references x and y have different lifetimes. x has lifetime 'a and y has lifetime 'b.

lifetime Elision

  • What Is Lifetime Elision?

Some common coding patterns were identified and annotating them was an extra effort. In order to avoid that overhead, Rust allows lifetimes to be elided or omitted. This is known as lifetime elision. Rust does so by designing rules for omitting lifetime annotation. They are tested at compile time and are used to determine a lifetime.

  • Lifetime Ellison can appear in two ways:

    • An input lifetime is a lifetime associated with a parameter of a function.
```rust
 fn fun_name<'a>( x : & 'a i32);  
```

Here, the input lifetime of x is 'a.
  • An output lifetime is a lifetime associated with the return value of a function.
 fn fun_name<'a>() -> & 'a i32;

Here, the output lifetime of return value is 'a.

Note: A function can have both input and output lifetimes.

Note: A function can have both input and output lifetimes.

Here, the x has the input lifetime and the return value has the output lifetime.

Rules for Elision

Rule 1

Each input parameter gets its own lifetime. If the lifetime is not specified, then the lifetime of each parameter is different.

  • Elided lifetime:
fn fun_name( x: &i32, y: &i32){ 
}
  • Expanded form:
fn fun_name<'a,'b>( x :& 'a i32, y : & 'b i32){
}

Rule 2

If there is only one input parameter, its lifetime is assigned to all the elided output lifetimes.

  • Elided lifetime:
fn fun_name(x: i32) -> &i32{
}
  • Expanded form:
fn fun<'a>(x: i32) -> & 'a i32 {
}

Rule 3

If there are multiple input lifetimes, one of them is &self or &mut self, the lifetime of self is assigned to all elided output lifetimes.

  • Elided lifetime:
fn fun_name(&self, x : &str) -> & str {
}
  • Expanded form:
fn fun_name<'a,'b>(& 'a self, x : & 'b str) -> & 'a str {
}

Note: If the compiler can’t infer the lifetime, it throws an error.

Did you find this article valuable?

Support CloudNativeFolks Community by becoming a sponsor. Any amount is appreciated!

Learn more about Hashnode Sponsors
 
Share this