DEV Community

Anshuman Sathua
Anshuman Sathua

Posted on

Programming fundamentals in Rust - Part 1 : Variables and Mutability

By default, Rust embraces immutability for variables as a deliberate choice. This serves as one of Rust's guiding principles, encouraging developers to write code that capitalizes on the safety and seamless concurrency that the language offers. Nevertheless, Rust provides the flexibility to make variables mutable, offering developers the choice to opt out of this default behavior. Let's delve into how and why Rust encourages immutability, and when you might consider opting for mutability.

In Rust, when a variable is immutable, once a value is bound to a name, you cannot alter that value. To illustrate this concept, let's create a new project named "variables" in our projects directory using cargo new variables.

Now, navigate to the newly created "variables" directory, open the src/main.rs file, and replace its existing code with the following snippet, which won't compile just yet:

fn main() {
    let x = 5;
    println!("The value of x is: {x}");
    x = 6; // This line will cause a compilation error
    println!("The value of x is: {x}");
}
Enter fullscreen mode Exit fullscreen mode

Save your changes and execute the program using cargo run. Expect to encounter an error message indicating an immutability violation, as illustrated in the following output:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
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

For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` due to previous error
Enter fullscreen mode Exit fullscreen mode

This example highlights the compiler's role in pinpointing errors within your programs. While compiler errors may seem vexing, they essentially signify that your program is not yet securely achieving its intended functionality.

The error message cannot assign twice to immutable variable x was triggered because an attempt was made to assign a second value to the immutable variable x.

This compile-time error is crucial as it prevents scenarios where unintentional value changes can lead to bugs. When one part of your code assumes that a value remains constant, and another part modifies that value, it introduces the potential for unexpected behavior. Identifying the source of such bugs after the fact can be challenging, particularly when the second piece of code alters the value selectively.

Rust's compiler provides assurance that if you declare a value as immutable, it genuinely remains unchanged. This eliminates the need for manual tracking, making your code more predictable and easier to comprehend.

However, embracing mutability can prove highly beneficial, enhancing the convenience of code composition. Despite the default immutability of variables, you have the flexibility to render them mutable by prefixing the variable name with mut. This not only alters the variable's behavior but also serves as a clear indication to future code readers that other segments of the codebase will modify the value associated with this variable.

For example, let’s change src/main.rs to the following:

fn main() {
    let mut x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}
Enter fullscreen mode Exit fullscreen mode

When we run the program now, we get this:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/variables`
The value of x is: 5
The value of x is: 6
Enter fullscreen mode Exit fullscreen mode

The introduction of mut grants us the liberty to alter the value assigned to x from 5 to 6. The choice of whether to embrace mutability or not rests entirely on your discretion and hinges on what you deem most lucid in that specific scenario.

Constants

Similar to immutable variables, constants represent values tied to a name and are strictly off-limits when it comes to alterations. However, there are a few key differences between constants and variables.

Firstly, constants are inherently immutable — you can't use mut with them. Unlike variables declared with the let keyword, constants use const, and you must annotate the type of the value. Don't worry too much about the details of types and annotations for now, we'll dive into that in the next section, called "Data Types," where we'll get a more comprehensive understanding. Just keep in mind that type annotations are always required for constants.

Another thing to note is that constants can be declared in any scope, including the global scope. This makes them useful for values that many parts of the code need to know about.

The last difference is that constants may only be set to a constant expression, not the result of a value that could only be computed at runtime.

We'll explore these concepts further as we progress. For now, let's look at an example of a constant declaration:

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
Enter fullscreen mode Exit fullscreen mode

The constant is named THREE_HOURS_IN_SECONDS, and its value is determined by multiplying 60 (the number of seconds in a minute) by 60 (the number of minutes in an hour) by 3 (representing the desired count of hours in this program). Rust follows a naming convention for constants, utilizing all uppercase letters with underscores between words. The compiler can evaluate a restricted set of operations during compile time, allowing us to express this value in a more comprehensible and verifiable manner, as opposed to directly setting it to the numeric result, such as 10,800.

Constants remain valid throughout the entire program runtime, confined to the scope in which they were declared. This enduring nature makes constants particularly advantageous for values within your application domain that multiple program sections might need to reference, like the maximum points a player can earn in a game or the speed of light.

Employing constants for hardcoded values across your program enhances code clarity for future maintainers and streamlines future updates. By centralizing such values as constants, you establish a single, easily locatable point for any necessary modifications in the future.

Shadowing

In Rust, we have the flexibility to declare a new variable with the same name as a previous one. Rust developers often refer to this as "shadowing," where the first variable is overshadowed by the second. This implies that, when we use the variable name, the compiler recognizes the second variable. Essentially, the second variable supersedes the first, capturing any references to the variable name until either it gets shadowed itself or the current scope concludes. To shadow a variable, we can simply reuse the same variable name and employ the let keyword again, as illustrated below:

fn main() {
    let x = 5;

    let x = x + 1;

    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }

    println!("The value of x is: {x}");
}
Enter fullscreen mode Exit fullscreen mode

This program initially binds the variable x to a value of 5. Subsequently, it introduces a new variable x by reusing the let x = construct, taking the original value and incrementing it by 1, resulting in x being assigned a new value of 6. Then, within an inner scope defined by curly brackets, the third let statement further shadows x, creating yet another variable. This time, the value is obtained by multiplying the previous value by 2, making x assume a value of 12. Upon the conclusion of this inner scope, the inner shadowing ceases, and x reverts to being 6. Upon executing this program, the output will be as follows:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/variables`
The value of x in the inner scope is: 12
The value of x is: 6
Enter fullscreen mode Exit fullscreen mode

Shadowing differs from marking a variable as mutable (mut) because attempting to reassign a value without using the let keyword results in a compile-time error. The use of let allows us to apply various transformations to a value while maintaining immutability after those transformations are completed.

Another distinction between mut and shadowing lies in the fact that, with shadowing, a new variable is effectively created when the let keyword is employed again. This provides the flexibility to change the type of the value while reusing the same variable name. For instance, consider a scenario where our program prompts a user to specify the desired number of spaces between some text using space characters, and subsequently, we aim to store that input as a number:

    let spaces = "   ";
    let spaces = spaces.len();
Enter fullscreen mode Exit fullscreen mode

The initial spaces variable is of type string, while the subsequent spaces variable is of type number. Shadowing offers the advantage of avoiding the need for distinct names, such as spaces_str and spaces_num, instead, we can reuse the more straightforward spaces name. However, if we attempt to use mut for this purpose, as illustrated here, a compile-time error will be triggered:

    let mut spaces = "   ";
    spaces = spaces.len();
Enter fullscreen mode Exit fullscreen mode

The error says we’re not allowed to mutate a variable’s type:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
2 |     let mut spaces = "   ";
  |                      ----- expected due to this value
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected `&str`, found `usize`

For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` due to previous error
Enter fullscreen mode Exit fullscreen mode

Summary

  • Rust defaults to immutability for variables, encouraging safer code and easier concurrency handling.

  • Immutable variables cannot be altered once a value is assigned to them.

  • Attempting to modify an immutable variable results in a compile-time error.

  • Immutability ensures predictability in code behavior and prevents unintentional value changes.

  • Despite the default immutability, Rust allows variables to be declared mutable using the mut keyword.

  • Mutability offers flexibility in code composition but should be used judiciously.

  • Constants in Rust are similar to immutable variables but have a few key differences.

  • Constants are declared using the const keyword and must have a type annotation.

  • Constants are useful for values that remain constant throughout the program and can be declared in any scope.

  • Rust supports shadowing, allowing variables to be redeclared within the same scope.

  • Shadowing creates a new variable with the same name as an existing one, effectively hiding the original variable.

  • Shadowing differs from mutability in that it allows for the reuse of variable names and changing variable types.

  • Constants, immutability, mutability, and shadowing are fundamental concepts in Rust that contribute to code safety and readability.

As we wrap up this chapter, we've laid a solid foundation by comprehending the intricacies of variables and their behavior, including concepts like immutability, mutability, and shadowing. Our journey into Rust programming is far from over, though. In the upcoming chapters, we will dive into the realm of data types, unlocking a new layer of knowledge that will pave the way for crafting even more sophisticated and efficient Rust programs. Stay tuned for the next chapter, where we explore the diverse world of Rust data types and further elevate our programming skills.

Top comments (0)