Variables and Data Types
This page explains Rust variables, their mutability, and data types.
Variables
In Rust, a variable is a named binding to a value stored in memory.
Mutability
In Rust, by default, Variables are immutable. That is, once a value is bound to a variable name, it's value can't be changed. You define variables using the let keyword.
let x = 5;
// The following line throws a compilation error, since the variable is immutable
x = 6;You can, however, have the option to make your variables mutable. You can do so by using mut keyword as shown below:
let mut x = 5;
// The following line works fine now
x = 6;Constants
Like immutable variables, Constants are values that are bound to a name and are not allowed to change. But there are some differences:
- Constants are defined with
constkeyword, instead of theletkeyword. - Constants aren't just immutable by default - they're always immutable. That is, you're not allowed to use
mutwith constants. - The type of the constant value must be annotated.
- Constants may be set only to a constant expression, not the result of a value that could only be computed at runtime.
const THREE_HOURS_IN_SECONDS: u32 = 3 * 60 * 60;The Rust compiler evaluates the above expression at compile time, which lets us choose to write out this value in a way that’s easier to understand and verify, rather than setting this constant to the value 10800.
Naming hardcoded values used throughout your program as constants is useful in conveying the meaning of that value to future maintainers of the code. It also helps to have only one place in your code you would need to change if the hardcoded value needed to be updated in the future.
Static Variables
In Rust, a static variable is a value that has a single, fixed memory location for the entire duration of the program. In other words, it’s a global variable with a static lifetime, meaning it lives for as long as the program runs.
By default, static variables are immutable. To make one mutable, you must use mut keyword but accessing or modifying it requires an unsafe block. This is because mutable statics can cause data races if used concurrently.
Static variables should be used for truly global data that never changes (Ex: version strings, configuration constants).
Shadowing
In Rust, shadowing is when you declare a new variable with the same name as a previous one, effectively “shadowing” the original variable. The new variable hides the old one within its scope, even though both exist in memory at different times.
fn main() {
let x = 5;
let x = x + 1; // shadowing: creates a new `x`, not mutating the old one
{
let x = x * 2; // shadowing: creates a new `x`, not mutating the old one
println!("The value of x in the inner scope is: {x}"); // Prints: 12
}
println!("The value of x is: {x}"); // Prints: 6
}A few points to note:
- Shadowing is not the same as mutation.
- When you shadow, you create a new variable; When you mutate, you change an existing one.
- Type can change when shadowing.
- Since it’s a new variable, you can even change its type.
let spaces = " "; // `spaces` is a string type
let spaces = spaces.len(); // Now `spaces` is a number typeShadowing is commonly used to perform a few transformations on a value without needing a new name, but still have the variable be immutable after those transformations.
Naming Conventions
- Use snake case for variable and function names (lower cased).
- Names should be descriptive but concise.
- Avoid abbreviations unless they're widely understood (Ex:
id,url,api). - Avoid using keywords for variable or function names.
- Use upper cased names for constants and static variables.
let user_name = "Alice";
let account_balance = 250.75;
const MAX_POINTS: u32 = 100;
static APP_VERSION: &str = "1.0.0";Data Types
Every Value in Rust is of a certain data type. Rust is a statically typed language, which means all variable types must be known at compile time. The compiler can usually infer the type based on the value assigned. In cases when the compiler doesn’t have enough information to guess the type, you must add a type annotation, like this:
let guess: i32 = "42".parse().unwrap();Rust’s data types are grouped into two subsets:
- Scalar types – represent a single value
- Compound types – group multiple values together
Scalar Types
Rust has four primary scalar types: integers, floating-point numbers, Booleans and characters.
Integers
An integer is a number without a fractional component.
Integers can be signed (types start with i) or unsigned (types start with u). Unsigned integers can only store non-negative numbers, whereas signed integers can store both positive and negative numbers. The first bit of a signed integer is used as a sign bit - 0 for positive, 1 for negative.
The following variants can be used to declare the type of an integer value:
| Type | Size (bits) | Range (approx.) |
|---|---|---|
i8 / u8 | 8 | −128 → 127 / 0 → 255 |
i16 / u16 | 16 | −32,768 → 32,767 / 0 → 65,535 |
i32 / u32 | 32 | −2.1B → 2.1B / 0 → 4.2B |
i64 / u64 | 64 | very large range |
i128 / u128 | 128 | extremely large range |
isize / usize | depends on system architecture (32-bit or 64-bit) |
You can write integer literals in any of the forms shown below:
| Number Literals | Example |
|---|---|
| Decimal | 98_222 |
| Hexadecimal | 0xff |
| Octal | 0o77 |
| Binary | 0b1111_0000 |
Byte (u8 only) | b'A' |
A few points to note:
- Some numeric literals can represent multiple integer types, can specify their type explicitly using a type suffix. For example,
57u8indicates an unsigned 8-bit integer. - Number literals can also use
_as a visual separator to make the number easier to read, such as1_000, which will have the same value as if you had specified1000.
NOTE
In Rust, integer types default to i32.
Integer Overflow
If a value goes outside this range, integer overflow occurs. For example, a u8 can store values from 0 to 255 and if the value goes beyond 255, an integer overflow occurs.
In debug mode, Rust checks for overflow and panics at runtime if it happens. panics are discussed in detail later.
In release mode (--release), Rust does not panic. Instead, it uses two’s complement wrapping. This means values “wrap around” to the start of the range. For example, 256 becomes 0, 257 becomes 1, and so on.
To handle overflow safely, you can use these standard library methods on numeric types:
wrapping_*— always wraps aroundchecked_*— returnsNoneon overflowoverflowing_*— returns the value and a flagsaturating_*— clamps to the min or max value
Example:
let a: u8 = 250;
// overflowing_* → returns (value, overflowed)
let (val, overflowed) = a.overflowing_add(10); // 250 + 10 = 4 (overflows and wraps to 0–255 range)
println!("overflowing_add: value={val}, overflowed={overflowed}"); // prints overflowing_add: value=4, overflowed=trueFloating-Point Types
Rust also has two primitive types for floating-point numbers, which are numbers with decimal points. Rust’s floating-point types are f32 and f64, which are 32 bits and 64 bits in size, respectively. The default type is f64 because on modern CPUs, it’s roughly the same speed as f32 but is capable of more precision.
NOTE
In Rust, all floating-point types are signed.
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}Numeric Operations
Rust supports the basic mathematical operations you’d expect for all the number types: addition, subtraction, multiplication, division, and remainder. Integer division truncates toward zero to the nearest integer.
fn main() {
// division
let quotient = 56.7 / 32.2; // 1.7608695652173911
let truncated = -5 / 3; // -1
let error = 5.0 / 3; // Compilation Error: Cannot divide float by integer
}Boolean Type
Boolean type in Rust has two possible values: true and false. Booleans are one byte in size.
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}Character Type
Rust’s char type is the language’s most primitive alphabetic type. Here are some examples of declaring char values:
fn main() {
let c = 'z';
let z: char = 'ℤ'; // with explicit type annotation
let heart_eyed_cat = '😻';
}Note that we specify char literals with single quotes, as opposed to string literals, which use double quotes. Rust’s char type is four bytes in size and represents a Unicode scalar value, which means it can represent a lot more than just ASCII. Unicode characters are discussed in detail later.
Compound Types
Compound types can group multiple values into one type. Rust has two primitive compound types: tuples and arrays.
Tuple Type
A tuple is a general way of grouping together a number of values with a variety of types into one compound type. Tuples have a fixed length: once declared, they cannot grow or shrink in size.
We create a tuple by writing a comma-separated list of values inside parentheses. Each position in the tuple has a type, and the types of the different values in the tuple don’t have to be the same. We’ve added optional type annotations in this example:
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}The tuple without any values has a special name, unit. This value and its corresponding type are both written () and represent an empty value or an empty return type.
Accessing Tuple elements
You can access Tuple elements using their respective indices (index starts with 0) and dot notation, as shown below:
fn main() {
let tup = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
}You can also use Tuple destructuring to get the individual values out of a tuple, as shown below:
fn main() {
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
println!("The value of y is: {y}");
}Array Type
Array type is similar to Tuple type, but every element of an array must have the same type. Also, unlike arrays in some other languages, arrays in Rust have a fixed length.
We write the values in an array as a comma-separated list inside square brackets:
fn main() {
let a = [1, 2, 3, 4, 5];
}Arrays are useful when you want your data allocated on the stack (LIFO), rather than heap. Also, Array isn't as flexible as a vector type. A vector is a similar collection type provided by the standard library that is allowed to grow or shrink in size because its contents live on the heap. Stack, heap and vectors are discussed in detail later.
If you’re unsure whether to use an array or a vector, chances are you should use a vector. However, arrays are more useful when you know the number of elements will not need to change. For example, storing all the 12 month names in an array.
Array's type is declared using square brackets with the type of each element, a semicolon, and then the number of elements in the array, like so:
let a: [i32; 5] = [1, 2, 3, 4, 5];You can also initialize an array to contain the same value for each element by specifying the initial value, followed by a semicolon, and then the length of the array in square brackets, as shown here:
let a = [3; 5];
// Same as let a = [3, 3, 3, 3, 3];Accessing Array Elements
You can access elements of an array using indexing, like this:
fn main() {
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
}If you try to access an element outside the index bounds, you'll get a compilation error. In case the index value is derived at runtime (such as when it's passed from user input), the code panics with index out of bounds.
