In the previous chapter, we built a simple guessing game. Now, we will learn more about variables, data types, mutability, and control flow in Rust
Variables and Mutability
In Rust, we define variables with let
keyword. However, variables by default are not mutable in Rust.
This means that once a variable is defined, it cannot be changed. To make a variable mutable, we use the
mut
keyword after the let
keyword. Below is an example of mutable and immutable variables.
fn main() {
let x = 5; // immutable variable
let mut y = 6; // mutable variable
If we try to reassign value to the variable x
above, the Rust
compiler will NOT
like it! Let’s try.
fn main() {
let x = 5; //immutable variable
x = 6;
}
Try running the above code, and we should get the following:
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| help: consider making this binding mutable: `mut x`
3 | println!("The value of x is {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
Even if the compiler does not like what we did, it does not hate us!(unlike the C
compiler, wha whaaaat!!!!) We can see that the issue is
very nicely printed out for us.
Similarly, we can reassign any other value to the variable y
from the above example as follows:
fn main() {
let mut y = 6;
y = 7
println!("The value of y is {y}")
}
If you build and run the above code, you should see that the value of y
is 7
.
Constants
In rust, constants are always immutable by default. We define constants using the const
keyword,
instead of the let
keyword. Trying to make a constant defined with const
mutable, as seen in below
code, would result in the following error:
fn main() {
const mut MAX_POINTS: u32 = 100_000;
MAX_POINTS = 100_001;
}
error: const globals cannot be mutable
--> src/main.rs:4:11
|
4 | const mut MAX_POINTS: u32 = 100_000;
| ----- ^^^ cannot be mutable
| |
| help: you might want to declare a static instead: `static`
So, in conclusion, we cannot make constants mutable in Rust.
Shadowing
We can shadow a variable by using the same variable name as the previous variable. This is different from mutability. Let’s see an example:
fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("The value of x is {x}");
}
Also, shadowing is different that using mut
variables, since for mut variables, we can ignore the
let
keyword, but for shadowing, we have to use the let
keyword. Also, if we plan to change the
datatype of the variable we are shadowing, we need to explicitly mention the new datatype.
Key Points
- Variables are immutable by default in Rust
- If we want immutable variables, we use the
mut
keyword after thelet
keyword - By strict nature,
const
variables, i.e., constants, are immutable in Rust - Shadowing is different that using a
mut
variable, since if we shadow a variable again, we need to use the -let
keyword again. - If we change the datatype of the variable we are shadowing, we need to explicitly mention the new datatype.
Data Types
In rust, every value has a data type
. Data types are the typical data types that we see in other programming
languages, and they are as follows:
- Scalar Types
- Integers
- Floating-Point Numbers
- Booleans
- Characters
Integer Types
Integer types as signed and unsigned, i.e., can range from negative to positive values, and only positive values,
respectively. They are represented as either i32
for signed integers, or u32
for unsigned integers.
Full reference can be found here.
Floating-Point Types
There are only two floating point types in Rust, f32
and f64
. The default type is f64
, since it is faster
than f32
on modern CPUs.
Important Note
: When performing mathematical operations, we need to be careful about the data types we are using.
By default, mathematical operations in rust
do not convert floating point numbers to return floats. For example,
an operation 2/3
would return 0
, since both 2
and 3
are integers. To get the correct result, we need to
explicitly convert one of the numbers to a float, as follows: 2.0/3
or 2/3.0
or 2.0/3.0
.
Boolean Type
Boolean types in Rust are represented as bool
, and can have two values, true
or false
.
Character Type
Most primitive alphabetic type. Characters are always specified using single quotes, as follows: let c = 'z'
.
Compound Types
These data types are used to combine multiple values into a one type. There are only two compound types in Rust:
tuples
and arrays
.
Tuples
Can have multiple data types clubbed into one tuple. Type annotation is optional when creating a tuple, but useful.
To get the values out from a tuple in rust
, we can either unzip the tuple, or use .
operator with the index.
Arrays
Arrays must have all elements from the same data type. Arrays in rust
have a fixed size, so we cannot append
to arrays, instead we append to vectors
, which we will see in a later chapter.
Also, arrays are good when we want to store the data on stack
rather than heap
, since they always have
a fixed size.
In rust
, arrays should be defined as follows:
let a: [i32; 5] = [1, 2, 3, 4, 5];
Elements from array in rust
are accessed as they are in Python
, via the [index]
operator.
If we try to access an invalid index, the compiler panics
and we usually get a helpful error message.
Functions
In rust
, functions are defined with fn
keyword, followed by the name of the function, and then the
parameters. The return type of the function is specified after the ->
operator. If the function does
not return anything, we can omit the return type.
In rust
, the function signatures must
have a data type. This is because the compiler needs to know
the size of the return type at compile time. This can be seen in this example:
fn main() {
print_labeled_measurement(5, 'h');
}
fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}
Expressions and Statements
In rust
, expressions and statements are different.
- Statements are instructions that perform some action and do not return a value.
- Expressions evaluate to a resultant value.
let y = 6;
is a statement, as it is only assigning a value to the variable y
.
{
let x = 3;
x + 1
} // This is an expression, since it returns the value 'x + 1'
We can return early from a function using the return
keyword in rust
. However, even without the return
keyword, the last expression in a function is returned implicitly. If we have a return type, its type is to
given in the function signature as follows:
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {x}");
}
Control Flow
Most common constructs are the if
, while
, and for
loops. They are all expressions, as they typically
return a value at the end of their execution.
If Expressions
Only requirement is that the condition that is being checked must be a bool
type. So, we cannot have a conditional
block like this in rust
:
fn main() {
let number = 3;
if number {
println!("number was three");
}
}
The above code block would raise an error
in rust
, as the condition if number
is not a bool
type.
Using if
in let
statements
In rust, we can use if
in let
statements as follows:
fn main() {
let condition = true;
let number = if condition { 5 } else { 6 };
println!("The value of number is: {number}");
}
This is a valid assignment (statement) in rust, as we assign a value based on a condition. However, we need to
make sure that the return value of the conditional is of the same data type, otherwise the compiler will throw
an error. For example, this is not a valid expression using if
blocks:
fn main() {
let condition = true;
let number = if condition { 5 } else { "six" };
println!("The value of number is: {number}");
}
The above would throw an error, as one type is of integer and another is of string type.
Looping your code
In rust, we can make loops using the loop
, while
, and for
keywords.
loop
keyword
Executes forever, until user stops the program manually. We can also use break
keyword to break out of the loop.
We can also assign loop labels
in rust to know which loop is being executed. When breaking out of a loop,
we can then use the break
keyword with the loop label
to stop execution. It is mandatory to have the
loop labels starting with the '
character to be able to use them in break
statements.
fn main() {
let mut count = 0;
'counting_up: loop {
println!("count = {count}");
let mut remaining = 10;
loop {
println!("remaining = {remaining}");
if remaining == 9 {
break;
}
if count == 2 {
break 'counting_up;
}
remaining -= 1;
}
count += 1;
}
println!("End count = {count}");
}
while
keyword
The while
keyword works just like the if
keyword, wherein the code is executed until it reaches a false
condition.
for
keyword
The for
keyword is used to iterate over a collection of items. For example, we can iterate over a range of numbers.
Important Note
: In for
conditions, the range over which we an element to loop over is specified with the syntax
(start..end)
, where start
is inclusive and end
is exclusive. For example, for n in (1..4)
will iterate over
the numbers 1, 2, 3
.
Conclusion
In this chapter, we learned about the basic data types in rust
, and how to use them. We also learned about the
if
expressions, and how to use them in let
statements. We also learned about the loop
, while
, and for
keywords, and how to use them to iterate over a collection of items.