Variables in Rust

Like every programming language, you will have to use variables in rust to save data. Today we will learn how the variables work in Rust.

Variables and Mutability

Variables in Rust have an immutable default state. This is only one of several nudges Rust provides to help you build code that makes the most of the safety and simple concurrency it provides. You can still choose to make your variables changeable, though. We'll look at how and why Rust promotes immutability, and why you might want to reject it.

When a variable is immutable, you can't change its value after it has been bound to a name. Let's create a new project called variables in your project directory as an example by using the

cargo new --bin variables

Next, open src/main.rs in the variables directory and substitute the following code there:

Filename: src/main.rs

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

Save and run the program using:

cargo run

You should receive an error message, as shown in this output:

error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         - first assignment to `x`
3 |     println!("The value of x is: {}", x);
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable

This illustration demonstrates how the compiler can be used to discover programming errors. Although compiler errors can be annoying, they do not indicate that you are a bad programmer; they simply indicate that your software is not yet capable of accomplishing what you want it to securely. Rust programmers with experience still encounter compiler issues.

The error indicates that the reason for the error is that we cannot assign the immutable variable x more than once because we attempted to do so.

Because trying to alter a value that was previously marked as immutable can result in defects, it's crucial that we get compile-time errors. It's possible that the first section of our code won't function as intended if it runs under the premise that a value won't ever change and another component of our code modifies that value. Finding this bug's root cause can be challenging, particularly if the second piece of code only seldom modifies the value.

When we assert in Rust that a value won't change, the compiler ensures that it actually won't change. As a result, you don't have to remember how and where a value could change when reading and creating code, which might make it simpler to reason about.

However, mutability has its uses. By default, variables are immutable, but we can change that by adding the word "mut" before the variable name. In addition to allowing this value to change, by stating that other parts of the code will also be changing this variable value, it also communicates intent to potential code readers.

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

Filename: src/main.rs

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

When we run this program, we get the following:

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

We are permitted to change the value that x binds to from 5 to 6 using mut. Making a variable mutable is a good idea in some circumstances because it makes writing code easier than using only immutable variables.

Along with pest avoidance, there are other trade-offs to take into account. For instance, changing an instance while it's already running might be quicker than copying and returning newly allocated instances when using huge data structures. Smaller data structures may make it simpler to reason about creating new instances and writing in a more functional programming style, therefore the slower performance may be an acceptable trade-off for getting that clarity.

Differences Between Variables and Constants

You may have been reminded of another programming idea that is present in the majority of other programming languages, by the inability to alter the value of a variable. Constants are values that are bound to a name and are not allowed to change, similar to immutable variables, but with a few key distinctions.

First off, we are not permitted to use mut with constants because they are always immutable and are not only immutable by default.

Instead of the let keyword, we use the const keyword to declare constants, and the type of the variable must be annotated. Don't worry about the specifics at this time; just know that we must always annotate the type. We'll address types and type annotations in the section below on data types.

Constants are helpful for values that many parts of the code need to be aware of because they can be declared in any scope, including the global scope.

The final distinction is that constants cannot be set to the outcome of a function call or any other value that could only be determined at runtime, only to a constant expression.

Here is an illustration of how to declare a constant with the name MAX POINTS and a value of 100,000. (Rust constant naming convention calls for using all uppercase letters and underscores to separate words)

const MAX_POINTS: u32 = 100_000;

Constants are a useful choice for values in your application domain that multiple parts of the programme might need to know about, such as the maximum number of points any player in a game is allowed to earn or the speed of light, because they are valid for the duration of a program's execution, within the scope they were declared in.

It's helpful to give constant names to hardcoded values that are used throughout your programme so that future code maintainers will know what such values signify. Additionally, it is advantageous to have only one place in your code that would require modification should the hardcoded value ever need to be changed.

Shadowing

In the Rust programming language, we can declare a new variable that has the same name as an existing variable and shadows the existing variable. When we utilize the variable, we will see the value of the second variable, according to the rustacean principle that the first variable is shadowed by the second. Using the same variable name and the let keyword twice, we can shadow a variable as seen below:

Filename: src/main.rs
fn main() {
    let x = 5;
    let x = x + 1;
    let x = x * 2;
    println!("The value of x is: {}", x);
}

This programme first assigns the value 5 to the variable x. Then it shadows x by iterating let x =, adding 1 to the starting value to make x equal 6. The third let statement also shadows x, multiplying the previous value by two to give x a final value of twelve. When you run this programme, the following results will be produced:

$ cargo run
   Compiling variables v0.1.0 (file:///projects/variables)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running `target/debug/variables`
The value of x is: 12

This is distinct from designating a variable as mut because, unless we use the let keyword once again, if we unintentionally try to reassign to this variable, a compile-time error will occur. A value can undergo a few alterations, but once those transformations are finished, the variable must be immutable.

The let keyword again effectively creates a new variable, so when using mut instead of shadowing, we can change the type of the value while retaining the same name. Let's take an example where our programme asks the user to input space characters to indicate how many spaces they want between some text, but we actually want to store that input as a number:

let spaces = "   ";
let spaces = spaces.len();

Because the first spaces variable is a string type and the second spaces variable—a brand-new variable that just so happens to have the same name as the first one—is a number type, this construction is legal. Thus, shadowing saves us from having to think of new names, such as spaces str and spaces num; instead, we can reuse the more straightforward spaces name. However, if we try to use mut for this, as shown here, we’ll get a compile-time error:

let mut spaces = "   ";
spaces = spaces.len();

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

error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected &str, found usize
  |
  = note: expected type `&str`
             found type `usize`

Now that we’ve explored how variables work, let’s look at more data types they can have.

Data Types

Every value in Rust has a specific type, which lets Rust know what sort of data is being supplied and how to handle it. We'll examine a number of the language's built-in kinds in this section. We divided the types into the scalar and compound subsets.

Keep in mind that Rust is a statically typed language throughout this section because it needs to know the types of all variables at compile time. The type we intend to use is typically inferred by the compiler from the value and our usage of it. When many types are allowed, as when we used parse to convert a String to a numeric type, we must provide a type annotation, such as this:

let guess: u32 = "42".parse().expect("Not a number!");

The following error will appear in Rust if we don't add the type annotation, meaning the compiler requires additional information from us to determine which potential type we wish to use:

error[E0282]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^
  |         |
  |         cannot infer type for `_`
  |         consider giving `guess` a type

You’ll see different type annotations as we discuss the various data types.

Scalar Types

One value is represented by a scalar type. Integers, floating-point numbers, Booleans, and characters are Rust's four main scalar types. These are probably familiar to you from other programming languages, but let's get started on how they function in Rust.

Integer Types

A number without a fractional component is called an integer. One integer type, the u32 type, was utilised earlier in this blog. This type declaration specifies that the value it refers to must be a 32-bit unsigned integer (signed integer types begin with I rather than u). The integer types that come with Rust are listed in Table 1. Declaring the type of an integer value can be done using any variation in the Signed and Unsigned columns (for instance, i16).

Table 1: Integer Types in Rust

Length Signed Unsigned
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
arch isize usize

Each version has an explicit size and can be signed or unsigned. In other words, whether the number has to have a sign with it (signed) or whether it will always be positive and may therefore be represented without a sign, depends on whether it is feasible for the number to be either positive or negative (unsigned). Similar to how numbers are written on paper, when it's important to indicate whether a number is positive or negative, a plus sign or a minus sign is used; otherwise, no sign is used. Two's complement representation is used to store signed numbers in memory.

Each signed version, where n is the amount of bits used, can store numbers in the range of -(2n - 1) to 2n - 1 - 1 inclusive. As a result, an i8 can hold numbers from -(27) to 27 - 1, or -128 to 127. A u8 can store numbers from 0 to 28 - 1, or 0 to 255, since unsigned variations may store numbers from 0 to 2n - 1.

The isize and usize types are also based on the type of computer your programme is running on: 64-bits if you're on a 64-bit architecture and 32-bits if you're on a 32-bit architecture.

Any of the forms listed in Table 2 may be used to write integer literals. It should be noted that all number literals, with the exception of the byte literal, support type suffixes like 57u8 and as a visual separator like 1 000.

Table 2: Integer Literals in Rust

Number literals Example
Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b1111_0000
Byte (u8 only) b'A'

So how do you decide which integer type to use? If you're unsure, you should stick with Rust's defaults. For example, integer types default to i32 because it's typically the quickest, even on 64-bit computers. When indexing a collection, the main circumstance in which you'd use isize or usize is.

Floating-Point Types

Two primitive types for floating-point numbers, or numbers with decimal points, are also available in Rust. The 32-bit and 64-bit floating-point types in Rust are called f32 and f64, respectively. Because it runs about as fast as f32 on contemporary CPUs but offers greater precision, f64 is the default type.

Here’s an example that shows floating-point numbers in action:

Filename: src/main.rs

fn main() {
    let x = 2.0; // f64
    let y: f32 = 3.0; // f32
}

The IEEE-754 standard is used to represent floating-point numbers. The f64 type has double precision, while the f32 type is a single-precision float.

Numeric Operations

For all number types, Rust offers addition, subtraction, multiplication, division, and remainder—the standard fundamental operations. Each one is demonstrated in the following code for use in a let statement:

Filename: src/main.rs

fn main() {
    // addition
    let sum = 5 + 10;
    // subtraction
    let difference = 95.5 - 4.3;
    // multiplication
    let product = 4 * 30;
    // division
    let quotient = 56.7 / 32.2;
    // remainder
    let remainder = 43 % 5;
}

These statements each contain an expression that employs a mathematical operator and evaluates to a single value that is then assigned to a variable.

The Boolean Type

A Boolean type in Rust has two possible values, true and false, just like in the majority of other programming languages. In Rust, bool is used to specify the Boolean type. For example:

Filename: src/main.rs

fn main() {
    let t = true;
    let f: bool = false; // with explicit type annotation
}

Boolean values are mostly used in conditionals, such an if expression. In the "Control Flow" section, we'll discuss how if expressions function in Rust.

The Character Type

We've just used numbers up until this point, but Rust also allows letters. The following code demonstrates one approach to use the most basic alphanumeric type in the Rust language, the char type. In contrast to strings, which employ double quotes, the char type is given with single quotations:

Filename: src/main.rs

fn main() {
   let c = 'z';
   let z = 'ℤ';
   let heart_eyed_cat = '😻';
}

Rust's char type may express much more than just ASCII because it is a Unicode Scalar Value. Emoji, zero width spaces, accented characters, and Chinese/Japanese/Korean ideographs are all recognised char types in Rust. The inclusive range of Unicode Scalar Values is U+0000 to U+D7FF and U+E000 to U+10FFFF. Your human understanding of what a "character" is may differ from what a char in Rust is because a "character" isn't really a concept in Unicode.

Compound Types

Multiple values of other kinds can be combined into one type using compound types. Tuples and arrays are the two basic compound types in Rust.

Grouping Values into Tuples

A tuple is a generic term for a means to combine several other values of different types into a single composite type.

A tuple is made by enclosing a list of values in parentheses and separating them with commas. The types of the various values in the tuple don't have to match for any given position because each position in the tuple has its own type. We’ve added optional type annotations in this example:

Filename: src/main.rs

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}

Due to the fact that a tuple is regarded as a single compound element, the variable tup binds to the entire tuple. We can use pattern matching to destructure a tuple value so that we can obtain the individual values out of it, as in the following example:

Filename: src/main.rs

fn main() {
    let tup = (500, 6.4, 1);
    let (x, y, z) = tup;
    println!("The value of y is: {}", y);
}

First, a tuple is created and bound to the variable tup in this programme. Then, tup is split into three distinct variables, x, y, and z, using a pattern using let. Because it divides the one tuple into three parts, this process is known as destructuring. The programme then displays the value of y, which is 6.4.

A period (.) followed by the index of the desired value can be used to access a tuple element directly in addition to destructuring through pattern matching. For instance:

Filename: src/main.rs

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);
    let five_hundred = x.0;
    let six_point_four = x.1;
    let one = x.2;
}

The tuple x is first created by this programme, which then generates new variables for each element based on their index. A tuple's first index, as in most programming languages, is 0.

Arrays

Using an array is another technique to have a collection of various values. Every element of an array, unlike a tuple, must be the same type. Because Rust arrays have a set length and cannot increase or decrease in size after being stated, they vary from arrays in some other programming languages.

The values entering an array in Rust are written as a list separated by commas enclosed in square brackets:

Filename: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];
}

When you wish to allocate data on the stack rather than the heap or guarantee that you always have a set number of elements, arrays come in handy. However, they lack the vector type's flexibility. The standard library's vector type is a comparable collection type that supports size expansion and contraction. You should probably use a vector if you're not sure whether to use an array or not:

In a software that needs to know the names of the months in the year, for instance, you might wish to use an array rather than a vector. You may use an array because you know it will always have 12 elements because it's extremely rare that such a software will need to add or remove months:

let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];

Accessing Array Elements

A single memory allocation on the stack is known as an array. Indexing allows us to access items of an array in the following way:

Filename: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];
    let first = a[0];
    let second = a[1];
}

As 1 is the value at index [0] in the array in this example, the variable named first will receive that value. The array's index [1] will be used to determine the value 2 for the second variable.

Invalid Array Element Access

What occurs if you attempt to access an element of an array after it has reached its end? If you alter the example to the following code, the programme will build but execute incorrectly:

Filename: src/main.rs

fn main() {
    let a = [1, 2, 3, 4, 5];
    let index = 10;
    let element = a[index];
    println!("The value of element is: {}", element);
}

Running this code using cargo run produces the following result:

$ cargo run
   Compiling arrays v0.1.0 (file:///projects/arrays)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31 secs
     Running `target/debug/arrays`
thread '<main>' panicked at 'index out of bounds: the len is 5 but the index is
 10', src/main.rs:6
note: Run with `RUST_BACKTRACE=1` for a backtrace.

Although there were no compilation faults, the application encounters a runtime error and does not successfully exit. Rust will verify that the supplied index is smaller than the array length before attempting to access an element using indexing. Rust will panic, which is the phrase used by Rust when a programme ends with an error, if the index is bigger than the length.

The safety concepts of Rust are now being used for the first time. This type of check is frequently skipped in low-level languages, which allows access to faulty memory when an incorrect index is provided. Rust safeguards you from this kind of error by quitting right away rather than allowing the memory access and continuing.

Post a Comment

0 Comments