Table of contents
- Introduction to Functions
- What Is a Function?
- main Function
- User-defined Functions
- Define a Function
- Call a Function
- Example
- Explanation
- Functions With Parameters
- What Are the Parameters?
- What Are Arguments?
- Example
- Explanation
- Types of Arguments
- Pass by Value
- Example
- Explanation
- Arguments Pass by Reference
- Example
- Explanation
- User-defined function
- Driver function
- Returning a Value From a Function
- Example 1
- Example 2
- Explanation
- Function With Multiple Return Values
- Example
- Explanation
- Functions With Arrays as Arguments
- Example 2
- Pass by Reference
- Return an Array
- Example
- What Is Recursion?
- Parts of Recursion
- Example
- Explanation
- What Is Recursion?
- Parts of Recursion
- Example
- Explanation
- Introduction to Strings
- Types of Strings
- Create a String Literal
- String Object (String)
- Create a String Object
- Creating an Initialized String Object
- Core Methods of String Objects
- Capacity in Bytes
- Finding a Substring
- Replace a Substring
- Trim a String
- Iterating Over Strings
- Tokenizing to Separate on Whitespaces
- Tokenizing to Split on a Custom Character
- Iterating Over the String Object
- Updating a String
- Push a Single Character
- Push a String
- Concatenation Using + Operator
- Format Macro
- Slicing a String
- functions and Strings
- Passing Primitive String - String Literal (&str)
- Passing Growable String - String Object (String)
- Introduction to Vectors
- Create Vectors
- Access an Element of a Vector
- Print the Vector
- Methods of Vectors
- Iterating Over a Vector
- Iterate Using .iter() Built-in Method
- Loop Through the Values
- Loops and Mutate Values
- Slicing a Vector
- Introduction to Structs
- What Are Structs?
- Declare a Struct
- Access Values from a Struct
- Update a Struct Instance
- Functions and Structs
- Pass Structs to a Function
- Return Structs From a Function
- What Are Static Methods?
- Declare a Static Method
- Invoke a Static Method
- Example
- Tuple Structs
- Define a Tuple Struct
- Initialize a Tuple Struct
- Access a Tuple Struct
- Example
- Introduction to Enums
- Declare an Enum
- Initialize an Enum
- Enums With Data Type
- Syntax
- Methods of Enums
- Enums and Match Control Flow Operator
- Syntax
- Example
- Enums and Structures
- When to Use Option?
- Example 1: Return Value Is None
- Example 2: Optional Variable Value
- Example 3: Index Out of Bound Exception
- is_some() , is_none() Functions
- Example 1
- The output of assert expression?
- Example 2
- Result and Enum
- Example 1
- Example 2
- Traits
- Types of Methods in Traits
- Declare a Trait
- Implement a trait
- Introduction to Modules
- Declare a Module
- Invoking a Module
- Keywords for Modules
- Example
- Controlling Visibility Within the Same File Using 'pub'
- Privacy Rules
- Example: Invoke a Public Function Directly
- Rule No: 2
- Example: Access a Private Function through a Child Module
- Example: Access a Root Function
- Control Visibility Within Different Files Using 'pub'
- Explicit Declaration
- Privacy Rule
- Example
- Explicit declaration
- The 'use' Keyword
- Why Use the use Keyword?
- Glob Operator ( * )
- Memory Management
- Stack
- What Is a Stack?
- Heap
- What Is a Heap?
- Stack vs. Heap
- Ownership
- Three Rules of Ownership
- Rule 1
- Rule 2
- Rule 3
- Copy Type
- Why is a primitive type copied?
- Example 1
- Example 2
- Why is non-primitive type moved?
- The clone Keyword
- Ownership and Functions
- Passing Values to a Function
- Return Values from a Function
- Borrowing
- Types of Borrowing
- Shared Borrows
- Mutable Borrows
- Rules of Borrowing
- Rule 1
- Rule 2
- Functions and Borrowing
- Borrowing and Slicing
- Example
- Lifetimes
- How does the compiler know the lifetime of a variable?
- Lifetime Annotation
- Function With Reference Variable Having a Lifetime
- Function With Mutable Reference Variable Having a Lifetime
- Multiple Lifetimes
- Multiple references have the same lifetime
- Multiple references have different lifetimes
- lifetime Elision
- Rules for Elision
- Rule 1
- Rule 2
- Rule 3
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
andparam_2
are passed to the function.The values of passed parameters are printed online
3
and line4
.Driver function
The driver
function main()
is defined from the line6
to line12
.On line
7
and line8
, twovariables value_1
andvalue_2
are defined.On line
10
, the function is invoked while passing the value of thevariable value_1
as the first argument and that ofvalue_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 line1
to line4
.On line
2
n is multiplied by itself and the value is saved inn
. The square of the argument thus calculated is displayed on the screen online3
.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 fromline 1
toline 4
which takes a mutable reference(&mut)
to the parameter n of typei32
.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 andmut
indicates that n can be changed inside the functionsquare()
. - 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 theprintln!()
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 parametersx
andy
and the result is saved in the area.On
line 14
, the perimeter of the rectangle is calculated by adding parametersx
andy
and then multiplying the result with2
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 line2
to line9
On
line 3
, a variable length is initialized with the value4
.On
line 4
, a variable width is initialized with the value 3.On
line 5
and6
, the value of length and width is displayed respectively.On
line 7
, the functioncalculate_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 line7
.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
toline 16
.
- The factorial function is defined from
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 from1∗factorial(0)
. factorial(2) is calculated from2∗factorial(1)
This processn∗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 line7
.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
toline 16
.
- The factorial function is defined from
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 from1∗factorial(0)
. factorial(2) is calculated from2∗factorial(1)
This processn∗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)
- String Literal
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
is0
and the maximum value is the size of the string
- Note: The minimum value of
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
- To create a vector write the vector macro
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:
# | method | explaination |
1 | Vec::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 | ||
5 | remove(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
#[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
namedKnightMoves
.
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 theimpl
constructExample The example below declares an
enum
, namedTrafficSignal
and defines an enum methodis_stop
within theimpl
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 rightassert_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 typeT
Err
, returns the error statement of typeE
.
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.
- The method that does not have a body means that implementation of the method is done by each struct in its own
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 functionmy_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 frommain.rs
. Note: In implicit declaration modules are public by default
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 frommain.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 toa
.
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!