Core Concepts: Variables, Data Types, and Operators
Now that your Rust environment is set up, let’s dive into the fundamental building blocks of any programming language: variables, data types, and operators. Understanding these concepts is crucial for writing any meaningful Rust program.
Variables and Mutability
In Rust, variables are used to store data. By default, variables are immutable, meaning once a value is bound to a variable, it cannot be changed. This promotes safer code by making it harder to introduce unexpected side effects.
Declaring Variables
You declare a variable using the let keyword:
fn main() {
let x = 5; // x is immutable
println!("The value of x is: {}", x);
// x = 6; // This would result in a compile-time error!
}
If you try to reassign x, the Rust compiler will give you an error:
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
3 | let x = 5;
| -
| |
| first assignment to `x`
| help: consider making this binding mutable: `mut x`
4 | x = 6;
| ^^^^ cannot assign twice to immutable variable
The error message is helpful! It even suggests a solution if you do intend to change the value.
Mutability with mut
To make a variable mutable (changeable), you add the mut keyword before the variable name:
fn main() {
let mut x = 5; // x is now mutable
println!("The value of x is: {}", x);
x = 6; // This is perfectly fine
println!("The new value of x is: {}", x);
}
Shadowing
Rust allows you to declare a new variable with the same name as a previous variable. This is called shadowing. When you shadow a variable, the new variable “shadows” or “hides” the old one, and it can even have a different data type.
fn main() {
let x = 5; // x is an integer
let x = x + 1; // x is shadowed by a new x, calculated from the old x
{
let x = x * 2; // Inner x shadows the outer x
println!("The value of x in the inner scope is: {}", x); // Output: 12
}
println!("The value of x in the outer scope is: {}", x); // Output: 6
}
Shadowing is different from mut because we’re creating a new variable each time. This is useful when you want to transform a value but keep the original variable name for conceptual clarity, while ensuring the original value is no longer accessible.
Data Types
Every value in Rust has a data type. Rust is a statically typed language, meaning it must know the types of all variables at compile time. However, the compiler can often infer the type based on the value you assign, so you don’t always have to write it explicitly.
Rust provides two main categories of data types:
- Scalar Types: Represent a single value.
- Compound Types: Group multiple values into one type.
Scalar Types
Scalar types represent a single value. Rust has four primary scalar types: integers, floating-point numbers, Booleans, and characters.
Integers
Integers are whole numbers. Rust’s integer types are signed (can be positive or negative) and unsigned (only positive) and come in various sizes:
| Length | Signed | Unsigned |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| arch | isize | usize |
isizeandusizedepend on the architecture of the computer your program is running on (e.g., 64 bits on a 64-bit system). They are primarily used for indexing collections.- The default integer type is
i32.
Integer Literals:
fn main() {
let decimal = 98_222; // underscores improve readability
let hex = 0xff;
let octal = 0o77;
let binary = 0b1111_0000;
let byte = b'A'; // u8 only
println!("Decimal: {}", decimal);
println!("Hex: {}", hex);
println!("Octal: {}", octal);
println!("Binary: {}", binary);
println!("Byte: {}", byte); // Prints the ASCII value
}
Integer Overflow:
When an integer type exceeds its maximum value (or goes below its minimum), it “overflows.” In debug builds, Rust will panic (crash) on integer overflow. In release builds (with optimizations), Rust performs “two’s complement wrapping,” meaning the value wraps around to the minimum value (e.g., u8::MAX + 1 becomes 0).
For this reason, it’s best to handle potential overflows explicitly using methods like checked_add, saturating_add, overflowing_add, or wrapping_add if wrapping behavior is desired.
fn main() {
let a: u8 = 255;
let b: u8 = 1;
// debug build: will panic!
// release build: wraps to 0
// let c = a + b;
// println!("a + b = {}", c);
// Explicitly handle overflow
if let Some(sum) = a.checked_add(b) {
println!("Checked sum: {}", sum);
} else {
println!("Addition overflowed!"); // This will be printed
}
let saturating_sum = a.saturating_add(b);
println!("Saturating sum: {}", saturating_sum); // Output: 255 (clamps to max)
let (wrapping_sum, did_overflow) = a.overflowing_add(b);
println!("Wrapping sum: {}, did overflow: {}", wrapping_sum, did_overflow); // Output: 0, true
let wrapping_sum_simple = a.wrapping_add(b);
println!("Simple wrapping sum: {}", wrapping_sum_simple); // Output: 0
}
Floating-Point Numbers
Rust has two floating-point types:
f32: Single-precision floatf64: Double-precision float (default)
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
println!("Float x: {}", x);
println!("Float y: {}", y);
}
Booleans
Booleans represent truth values and can be either true or false.
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
println!("Boolean t: {}", t);
println!("Boolean f: {}", f);
}
Characters
Rust’s char type represents a single Unicode scalar value. This means it can represent a lot more than just ASCII characters.
fn main() {
let c = 'z';
let z = 'ℤ';
let heart_eyed_cat = '😻';
println!("Char c: {}", c);
println!("Char z: {}", z);
println!("Char heart_eyed_cat: {}", heart_eyed_cat);
}
Note that char literals use single quotes, while string literals use double quotes.
Compound Types
Compound types can group multiple values into one type. Rust has two primitive compound types: tuples and arrays.
Tuples
A tuple is a general-purpose 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.
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
// Accessing tuple elements:
let (x, y, z) = tup; // Destructuring
println!("The value of y is: {}", y); // Output: 6.4
let five_hundred = tup.0; // Access by index
let six_point_four = tup.1;
println!("The first element is: {}", five_hundred); // Output: 500
}
An empty tuple, (), is known as the “unit type” and its value is the “unit value.” It is often used when a function doesn’t return any meaningful value.
Arrays
An array is a collection of values of the same type. Arrays also have a fixed length.
fn main() {
let a = [1, 2, 3, 4, 5]; // Array of 5 i32 integers
let b: [i32; 5] = [1, 2, 3, 4, 5]; // Explicit type: array of 5 i32
let c = [3; 5]; // An array of 5 elements, all initialized to 3. Equivalent to [3, 3, 3, 3, 3]
// Accessing array elements:
println!("First element of a: {}", a[0]); // Output: 1
println!("Third element of c: {}", c[2]); // Output: 3
// Trying to access an out-of-bounds element will cause a runtime panic
// let index = 10;
// let element = a[index]; // This will panic at runtime!
// println!("Element at index {}: {}", index, element);
}
While arrays are useful, in many cases, you might want to use a Vec (vector), which is a growable list type provided by the standard library. We’ll cover vectors later.
Operators
Rust supports common arithmetic, comparison, logical, and bitwise operators.
Arithmetic Operators
Standard mathematical operations:
+: addition-: subtraction*: multiplication/: division%: remainder (modulo)
fn main() {
// Addition
let sum = 5 + 10;
println!("Sum: {}", sum); // 15
// Subtraction
let difference = 95.5 - 4.3;
println!("Difference: {}", difference); // 91.2
// Multiplication
let product = 4 * 30;
println!("Product: {}", product); // 120
// Division
let quotient = 56.0 / 3.0;
println!("Quotient: {}", quotient); // 18.666...
let truncated = -5 / 3; // Integer division truncates towards zero
println!("Truncated: {}", truncated); // -1
// Remainder
let remainder = 43 % 5;
println!("Remainder: {}", remainder); // 3
}
Comparison Operators
Used to compare values, resulting in a bool:
==: equals!=: not equals>: greater than<: less than>=: greater than or equal to<=: less than or equal to
fn main() {
let a = 10;
let b = 5;
println!("a == b: {}", a == b); // false
println!("a != b: {}", a != b); // true
println!("a > b: {}", a > b); // true
}
Logical Operators
Combine boolean expressions:
&&: logical AND||: logical OR!: logical NOT
fn main() {
let is_sunny = true;
let is_warm = false;
println!("Is it good weather? {}", is_sunny && !is_warm); // false (true AND true is true, but !is_warm is true)
println!("Can we go out? {}", is_sunny || is_warm); // true
}
Bitwise Operators (for integers)
Operate on individual bits of integer types:
&: bitwise AND|: bitwise OR^: bitwise XOR<<: left shift>>: right shift
fn main() {
let x = 0b1010; // 10 in decimal
let y = 0b1100; // 12 in decimal
println!("x & y: {:04b}", x & y); // 0b1000 (8)
println!("x | y: {:04b}", x | y); // 0b1110 (14)
println!("x ^ y: {:04b}", x ^ y); // 0b0110 (6)
println!("x << 1: {:04b}", x << 1); // 0b10100 (20)
println!("y >> 2: {:04b}", y >> 2); // 0b0011 (3)
}
The {:04b} format specifier prints the number in binary, padded with leading zeros to a width of 4.
Exercises / Mini-Challenges
Reinforce your understanding with these hands-on challenges!
Exercise 2.1: Calculate Area and Perimeter
Write a Rust program that calculates the area and perimeter of a rectangle. Define two mutable integer variables, width and height, assign them values, and then print the calculated area and perimeter.
Instructions:
- Initialize
widthto 10 andheightto 5. - Calculate
area = width * height. - Calculate
perimeter = 2 * (width + height). - Print both results in a user-friendly format.
- After printing, change
widthto 12 andheightto 6, then recalculate and print the new area and perimeter.
// Solution Hint:
/*
fn main() {
let mut width = 10;
let mut height = 5;
// Calculate and print initial values
// Reassign and calculate again
width = 12;
height = 6;
// Calculate and print new values
}
*/
Exercise 2.2: Type Conversion and Shadowing
You are given a value representing the number of seconds, but you need to display it in hours, minutes, and remaining seconds. Use shadowing and type conversion where necessary.
Instructions:
- Start with a variable
total_secondsof typeu32initialized to3675. - Shadow
total_secondsto calculate the number of hours and store it in a new variablehours. - Shadow
total_secondsagain to find the remaining seconds after removing hours. - Shadow
total_secondsone more time to calculate the number of minutes from the remaining seconds. - The final
total_secondswill be the remaining seconds after removing minutes. - Print the result in the format:
HH hours, MM minutes, SS seconds.
Example Output (for 3675 seconds):
1 hours, 1 minutes, 15 seconds
// Solution Hint:
/*
fn main() {
let total_seconds: u32 = 3675;
let hours = total_seconds / 3600; // 3600 seconds in an hour
let total_seconds = total_seconds % 3600; // Remaining seconds after extracting hours
let minutes = total_seconds / 60;
let total_seconds = total_seconds % 60; // Remaining seconds after extracting minutes
// Print the results
}
*/
Exercise 2.3: Array Manipulation and Access
Create an array of your top 3 favorite fruits. Then, try to swap the first and last elements.
Instructions:
- Declare an array named
fruitsof type[&str; 3](an array of 3 string slices) and initialize it with your favorite fruits (e.g.,["Apple", "Banana", "Cherry"]). - Print the initial array.
- Implement logic to swap the first and last elements of the array. Hint: You’ll need a temporary variable.
- Print the array after the swap.
- Attempt to access an element at an index that is clearly out of bounds (e.g.,
fruits[5]) and observe the compiler’s behavior or runtime panic. Comment out the problematic line after observing.
// Solution Hint:
/*
fn main() {
let mut fruits = ["Apple", "Banana", "Cherry"];
println!("Initial fruits: {:?}", fruits);
let temp = fruits[0];
fruits[0] = fruits[2];
fruits[2] = temp;
println!("Fruits after swap: {:?}", fruits);
// This line will cause a runtime panic if uncommented:
// let out_of_bounds_element = fruits[5];
// println!("Out of bounds element: {}", out_of_bounds_element);
}
*/