Rust Basics Series #2: Using Variables and Constants in Rust Programs
Move ahead with your Rust learning and familiarize yourself with Rust programs' variables and constants.
In the first chapter of the series, I shared my thoughts on why Rust is an increasingly popular programming language. I also showed how to write Hello World program in Rust.
Let's continue this Rust journey. In this article, I shall introduce you to variables and constants in the Rust programming language.
On top of that, I will also cover a new programming concept called "shadowing".
The uniqueness of Rust's variables
A variable in the context of a programming language (like Rust) is known as an alias to the memory address in which some data is stored.
This is true for the Rust programming language too. But Rust has one unique "feature" compared to most other popular programming languages. Every variable that you declare is immutable by default. This means that once a value is assigned to the variable, it can not be changed.
This decision was made to ensure that, by default, you don't have to make special provisions like spin locks or mutexes to introduce multi-threading. Rust guarantees safe concurrency. Since all variables (by default) are immutable, you do not need to worry about a thread changing a value unknowingly.
This is not to say that variables in Rust are like constants because they are not. Variables can be explicitly defined to allow mutation. Such a variable is called a mutable variable.
Following is the syntax to declare a variable in Rust:
// immutability by default
// the initialized value is the **only** value
let variable_name = value;
// mutable variable defined by the use of 'mut' keyword
// the initial value can be changed to something else
let mut variable_name = value;
Meaning, if you have a mutable variable of type float, you can not assign a character to it down the road.
High-level overview of Rust's data types
In the previous article, you might have noticed that I mentioned that Rust is a strongly typed language. But to define a variable, you don't specify the data type, instead, you use a generic keyword let
.
The Rust compiler can infer the data type of a variable based on the value assigned to it. But it can be done if you still wish to be explicit with data types and want to annotate the type. Following is the syntax:
let variable_name: data_type = value;
Some of the common data types in the Rust programming language are as follows:
- Integer type:
i32
andu32
for signed and unsigned, 32-bit integers, respectively - Floating point type:
f32
andf64
, 32-bit and 64-bit floating point numbers - Boolean type:
bool
- Character type:
char
I will cover Rust's data types in more detail in the next article. For now, this should be sufficient.
8
to a variable with a floating point data type, you will face a compile time error. What you should assign instead is the value 8.
or 8.0
.Rust also enforces that a variable be initialized before the value stored in it is read.
{ // this block won't compile
let a;
println!("{}", a); // error on this line
// reading the value of an **uninitialized** variable is a compile-time error
}
{ // this block will compile
let a;
a = 128;
println!("{}", a); // no error here
// variable 'a' has an initial value
}
If you declare a variable without an initial value and use it before assigning it some initial value, the Rust compiler will throw a compile time error.
Though errors are annoying. In this case, the Rust compiler is forcing you not to make one of the very common mistakes one makes when writing code: un-initialized variables.
Rust compiler's error messages
Let's write a few programs where you
- Understand Rust's design by performing "normal" tasks, which are actually a major cause of memory-related issues
- Read and understand the Rust compiler's error/warning messages
Testing variable immutability
Let us deliberately write a program that tries to modify a mutable variable and see what happens next.
fn main() {
let mut a = 172;
let b = 273;
println!("a: {a}, b: {b}");
a = 380;
b = 420;
println!("a: {}, b: {}", a, b);
}
Looks like a simple program so far until line 4. But on line 7, the variable b
--an immutable variable--gets its value modified.
Notice the two methods of printing the values of variables in Rust. On line 4, I enclosed the variables between curly brackets so that their values will be printed. On line 8, I keep the brackets empty and provide the variables as arguments, C style. Both approaches are valid. (Except for modifying the immutable variable's value, everyting in this program is correct.)
Let's compile! You already know how to do that if you followed the previous chapter.
$ rustc main.rs
error[E0384]: cannot assign twice to immutable variable `b`
--> main.rs:7:5
|
3 | let b = 273;
| -
| |
| first assignment to `b`
| help: consider making this binding mutable: `mut b`
...
7 | b = 420;
| ^^^^^^^ cannot assign twice to immutable variable
error: aborting due to previous error
For more information about this error, try `rustc --explain E0384`.
This perfectly demonstrates Rust's robust error checking and informative error messages. The first line reads out the error message that prevents the compilation of the above code:
error[E0384]: cannot assign twice to immutable variable b
It means that the Rust compiler noticed that I was trying to re-assign a new value to the variable b
but the variable b
is an immutable variable. So that is causing this error.
The compiler even identifies the exact line and column numbers where this error is found.
Under the line that says first assignment to `b`
is the line that provides help. Since I am mutating the value of the immutable variable b
, I am told to declare the variable b
as a mutable variable using the mut
keyword.
Playing with uninitialized variables
Now, let's look at what the Rust compiler does when an uninitialized variable's value is read.
fn main() {
let a: i32;
a = 123;
println!("a: {a}");
let b: i32;
println!("b: {b}");
b = 123;
}
Here, I have two immutable variables a
and b
and both are uninitialized at the time of declaration. The variable a
gets a value assigned before its value is read. But the variable b
's value is read before it is assigned an initial value.
Let's compile and see the result.
$ rustc main.rs
warning: value assigned to `b` is never read
--> main.rs:8:5
|
8 | b = 123;
| ^
|
= help: maybe it is overwritten before being read?
= note: `#[warn(unused_assignments)]` on by default
error[E0381]: used binding `b` is possibly-uninitialized
--> main.rs:7:19
|
6 | let b: i32;
| - binding declared here but left uninitialized
7 | println!("b: {b}");
| ^ `b` used here but it is possibly-uninitialized
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
error: aborting due to previous error; 1 warning emitted
For more information about this error, try `rustc --explain E0381`.
Here, the Rust compiler throws a compile time error and a warning. The warning says that the variable b
's value is never being read.
But that's preposterous! The value of variable b
is being accessed on line 7. But look closely; the warning is regarding line 8. This is confusing; let's temporarily skip this warning and move on to the error.
The error message reads that used binding `b` is possibly-uninitialized
. Like in the previous example, the Rust compiler is pointing out that the error is caused by reading the value of the variable b
on line 7. The reason why reading the value of the variable b
is an error is that its value is uninitialized. In the Rust programming language, that is illegal. Hence the compile time error.
Example program: Swap numbers
Now that you are familiar with the common variable-related issues, let's look at a program that swaps the values of two variables.
fn main() {
let mut a = 7186932;
let mut b = 1276561;
println!("a: {a}, b: {b}");
// swap the values
let temp = a;
a = b;
b = temp;
println!("a: {}, b: {}", a, b);
}
Here, I have declared two variables, a
and b
. Both variables are mutable because I wish to change their values down the road. I assigned some random values. Initially, I print the values of these variables.
Then, on line 8, I create an immutable variable called temp
and assign it the value stored in a
. The reason why this variable is immutable is because temp
's value will not be changed.
To swap values, I assign the value of variable b
to variable a
and on the next line I assign the value of temp
(which contains value of a
) to variable b
. Now that the values are swapped, I print values of variables a
and b
.
When the above code is compiled and executed, I get the following output:
a: 7186932, b: 1276561
a: 1276561, b: 7186932
As you can see, the values are swapped. Perfect.
Using Unused variables
When you have declared some variables you intend to use down the line but have not used them yet, and compile your Rust code to check something, the Rust compiler will warn you about it.
The reason for this is obvious. Variables that will not be used take up unnecessary initialization time (CPU cycle) and memory space. If it will not be used, why have it in your program in the first place? Though, the compiler does optimize this away. But it still remains an issue in terms of readability in form of excess code.
But sometimes, you might be in a situation where creating a variable might not be in your hands. Say when a function returns more than one value and you only need a few values. In that case, you can't tell the library maintainer to adjust their function according to your needs.
So, in times like that, you can have a variable that begins with an underscore and the Rust compiler will no longer give you such warnings. And if you really do not need to even use the value stored in said unused variable, you can simply name it _
(underscore) and the Rust compiler will ignore it too!
The following program will not only not generate any output, but it will also not generate any warnings and/or error messages:
fn main() {
let _unnecessary_var = 0; // no warnings
let _ = 0.0; // ignored completely
}
Arithmetic operations
Since math is math, Rust doesn't innovate on it. You can use all of the arithmetic operators you might have used in other programming languages like C, C++ and/or Java.
A complete list of all the operations in the Rust programming language, along with their meaning, can be found here.
Example Program: A Rusty thermometer
Following is a typical program that converts Fahrenheit to Celsius and vice a versa.
fn main() {
let boiling_water_f: f64 = 212.0;
let frozen_water_c: f64 = 0.0;
let boiling_water_c = (boiling_water_f - 32.0) * (5.0 / 9.0);
let frozen_water_f = (frozen_water_c * (9.0 / 5.0)) + 32.0;
println!(
"Water starts boiling at {}Β°C (or {}Β°F).",
boiling_water_c, boiling_water_f
);
println!(
"Water starts freezing at {}Β°C (or {}Β°F).",
frozen_water_c, frozen_water_f
);
}
Not much is going on here... The Fahrenheit temperature is converted to Celsius and vice a versa for the temperature in Celsius.
As you can see here, since Rust does not allow automatic type casting, I had to introduce a decimal point to the whole numbers 32, 9 and 5. Other than that, this is similar to what you would do in C, C++ and/or Java.
As a learning exercise, try writing a program that finds out how many digits are in a given number.
Constants
With some programming knowledge, you might know what this means. A constant is a special type of variable whose value never changes. It stays constant.
In the Rust programming language, a constant is declared using the following syntax:
const CONSTANT_NAME: data_type = value;
As you can see, the syntax to declare a constant is very similar to what we saw in declaring a variable in Rust. There are two differences though:
- A constant name should be in
SCREAMING_SNAKE_CASE
. All uppercase characters and words separated by an undercase. - Annotating the data type of the constant is necessary.
Variables vs Constants
You might be wondering, since the variables are immutable by default, why would the language also include constants?
The following table should help alleviate your doubts. (If you are curious and want to better understand these differences, you can look at my blog which shows these differences in detail.)
Example program using constants: Calculate area of circle
Following is a straightforward program about constants in Rust. It calculates the area and the perimeter of a circle.
fn main() {
const PI: f64 = 3.14;
let radius: f64 = 50.0;
let circle_area = PI * (radius * radius);
let circle_perimeter = 2.0 * PI * radius;
println!("There is a circle with the radius of {radius} centimetres.");
println!("Its area is {} centimetre square.", circle_area);
println!(
"And it has circumference of {} centimetres.",
circle_perimeter
);
}
And upon running the code, the following output is produced:
There is a circle with the radius of 50 centimetres.
Its area is 7850 centimetre square.
And it has circumference of 314 centimetres.
Variable shadowing in Rust
If you are a C++ programmer, you already sort of know what I am referring to. When the programmer declares a new variable with the same name as an already declared variable, it is known as variable shadowing.
Unlike C++, Rust allows you to perform variable shadowing in the same scope too!
Let us take a look at how it works in Rust.
fn main() {
let a = 108;
println!("addr of a: {:p}, value of a: {a}", &a);
let a = 56;
println!("addr of a: {:p}, value of a: {a} // post shadowing", &a);
let mut b = 82;
println!("\naddr of b: {:p}, value of b: {b}", &b);
let mut b = 120;
println!("addr of b: {:p}, value of b: {b} // post shadowing", &b);
let mut c = 18;
println!("\naddr of c: {:p}, value of c: {c}", &c);
c = 29;
println!("addr of c: {:p}, value of c: {c} // post shadowing", &c);
}
The :p
inside curly brackets in the println
statement is similar to using %p
in C. It specifies that the value is in the format of a memory address (pointer).
I take 3 variables here. Variable a
is immutable and is shadowed on line 4. Variable b
is mutable and is also shadowed on line 9. Variable c
is mutable but on line 14, only it's value is mutated. It is not shadowed.
Now, let's look at the output.
addr of a: 0x7ffe954bf614, value of a: 108
addr of a: 0x7ffe954bf674, value of a: 56 // post shadowing
addr of b: 0x7ffe954bf6d4, value of b: 82
addr of b: 0x7ffe954bf734, value of b: 120 // post shadowing
addr of c: 0x7ffcfcd16b54, value of c: 18
addr of c: 0x7ffcfcd16b54, value of c: 29 // post shadowing
Looking at the output, you can see that not only the values of all three variables have changed, but the addresses of variables that were shadowed are are also different (check the last few hex characters).
The memory address for the variables a
and b
changed. This means that mutability, or lack thereof, of a variable is not a restriction when shadowing a variable.
Conclusion
This article covers variables and constants in the Rust programming language. Arithmetic operations are also covered.
As a recap:
- Variables in Rust are immutable by default but mutability can be introduced.
- Programmer needs to explicitly specify variable mutability.
- Constants are always immutable no matter what and require type annotation.
- Variable shadowing is declaring a new variable with the same name as an existing variable.
Awesome! Good going with Rust, I believe. In the next chapter, I discuss data types in Rust. Stay Tuned.
Meanwhile, if you have any questions, please let me know.