Rust: Rust Variables and Data Types
Last updated: 2026-08-26
In Rust, variables are immutable by default—this is not a limitation, but a safety guarantee.
Rust's variable and type systems are the cornerstone of its commitment to memory safety. Understanding immutability, type inference, and scalar types is the first step toward mastering Rust.
1. What You'll Learn
- Declare a variable using
letto understand immutability - Create a mutable variable using
let mut - Use
constto define a compile-time constant - Rust's four scalar types: integers, floating-point numbers, booleans, and characters
- Type Inference and Explicit Type Annotation
- Variable shadowing mechanism
2. A True Story of a Cashier
(1) The Struggle: Correcting Errors in Numbers in Excel
Lisa is a cashier at a small grocery store. Every day after work, she uses Excel to tally the day's sales:
- The price is constantly changing (due to promotions, discounts, and member prices), so she has to manually update the cells.
- Once, she accidentally overrode a row of formulas with numbers, and the entire spreadsheet was ruined.
- It took 3 hours to reconcile the accounts again
After she learned Rust, she remarked, "If Excel cells were uneditable by default, I wouldn't have to worry about overwriting formulas."
(2) Rust's Variable Model
fn main() {
let total_sales: f64 = 1280.50; // Immutable by default -- Today's Sales
// total_sales = 1500.00; // ❌ Compilation Error: You cannot modify immutable variables!
let mut daily_changes = 0.0; // Use mut to declare a mutable variable
daily_changes = 150.0; // ✅ Can be modified
daily_changes = daily_changes + 200.0;
const TAX_RATE: f64 = 0.08; // Compile-time constant, Global Fixed
println!("Tax Rate: {}, Today's Accounts Receivable: {}", TAX_RATE, total_sales * (1.0 + TAX_RATE));
}
Rust's philosophy is: If it can stay unchanged, don't change it. Default immutability makes the code easier to reason about and reduces bugs caused by unintended modifications.
3. Variable Declaration
(1) let: immutable by default
let x = 5; // Immutable Variable
// x = 10; // ❌ Compilation Error: cannot assign twice to immutable variable
(2) let mut: mutable variables
let mut y = 5; // Mutable Variable
y = 10; // ✅ Can be reassigned
(3) const: Compile-time constant
const MAX_POINTS: u32 = 100_000; // The type must be specified.
const PI: f64 = 3.1415926535;
| Characteristics | let |
let mut |
const |
|---|---|---|---|
| Variability | Immutable | Mutable | Immutable |
| Exists at runtime | Yes | Yes | No (inlined at compile time) |
| Type annotation | Optional | Optional | Required |
| Global scope | No | No | Yes |
| Expression Evaluation | Runtime | Runtime | Compile-time constant |
4. Scalar Types
Rust has four basic scalar types: integers, floating-point numbers, booleans, and characters.
graph TB
A[Scalar Types] --> B[Integer: i8/u8/i16/u16/i32/u32/i64/u64/i128/u128]
A --> C[Floating-point: f32/f64]
A --> D[Boolean: bool]
A --> E[Character: char]
(1) Integer Types
| Length | Signed | Unsigned | Range |
|---|---|---|---|
| 8-bit | i8 |
u8 |
-128 ~ 127 / 0 ~ 255 |
| 16-bit | i16 |
u16 |
-32768 ~ 32767 / 0 ~ 65535 |
| 32-bit | i32 |
u32 |
±2.1 billion / 0 ~ 4.2 billion |
| 64-bit | i64 |
u64 |
±9.2 × 10¹⁸ / 0 ~ 1.8 × 10¹⁹ |
| 128-bit | i128 |
u128 |
Maximum range |
| arch | isize |
usize |
Same as the system's bit width (64-bit system = i64/u64) |
The default integer type is
i32—it offers the best performance and is sufficient for most needs.
(2) Floating-Point Types
let a: f32 = 3.14; // 32-bit precision floating-point (Single Precision)
let b: f64 = 3.141592653589793; // 64-bit precision floating-point (Double Precision, Default Type)
The default floating-point type is
f64—on modern CPUs, f64 and f32 have nearly the same speed, but f64 offers higher precision.
(3) Boolean Type
let is_ok: bool = true;
let is_not = false;
Boolean values are commonly used in conditional statements, such as the conditional expressions if and while.
(4) Character Types
let c: char = 'A'; // ASCII Character
let emoji: char = '🦀'; // Unicode Character(4 Byte)
let han: char = 'Rust'; // Unicode character
In Rust,
charis a Unicode scalar value that occupies 4 bytes. It is not ASCII! This gives Rust native support for internationalization.
| Type | Size | Value Range | Typical Uses |
|---|---|---|---|
bool |
1 byte | true / false |
Conditional checks, flags |
char |
4 bytes | Unicode scalar value | Single-character processing |
f32 |
4 bytes | ±3.4×10³⁸ (approximately 7-digit precision) | Graphics computing, GPU shaders |
f64 |
8 bytes | ±1.8×10³⁰⁸ (approximately 15-digit precision) | Scientific computing (default floating-point) |
i32 |
4 bytes | ±2.1 billion | General-purpose integer (default) |
u8 |
1 byte | 0–255 | Byte data, RGB values |
usize |
arch | Same as the system's bit width | Array indices, container size |
5. Type Inference and Annotation
▶ Example 1: Type Inference (Difficulty ⭐)
Output:
The value of x is: 42
The value of y is: 3.14
// ============================================
// Rust compiler infers types based on assignments
// ============================================
fn main() {
let x = 42; // Inferred as i32 (Default integer type)
let y = 3.14; // Inferred as f64 (Default floating-point type)
let z = true; // Inferred as bool
let c = 'R'; // Inferred as char
// Can use :type to view (Example in This Section, Not a standard method)
println!("The value of x is: {}", x);
println!("The value of y is: {}", y);
}
Output:
x The value is: 42
y The value is: 3.14
Output:
small: 9223372036854775807, big: 3.141592653589793, precise: 255, flag: true
The compiler automatically infers types based on context, so developers don't need to specify type annotations everywhere.
▶ Example 2: Explicit Type Annotation (Difficulty ⭐)
Output:
small: 9223372036854775807, big: 3.141592653589793, precise: 255, flag: true
// ============================================
// Explicitly Specify the Type -- When a specific size or precision is required
// ============================================
fn main() {
let small: u8 = 255; // u8 Maximum value
let big: i64 = 9_223_372_036_854_775_807; // An underscore can be used as a digit separator
let precise: f32 = 3.141592653589793; // f32 May result in loss of precision
let flag: bool = 5 > 3; // The result of a Boolean expression
println!("small: {}, big: {}, precise: {:.10}, flag: {}", small, big, precise, flag);
}
Output:
small: 255, big: 9223372036854775807, precise: 3.1415927410, flag: true
Output:
small: 255, big: 9223372036854775807, precise: 3.1415927410, flag: true
Note that
f32has a precision of only about 7 significant digits, and3.141592653589793is truncated to3.1415927410.
▶ Example 3: Variable Shadowing (Difficulty ⭐⭐)
Output:
x value: 12
name length: 4
// ============================================
// Shadowing: Overwrite a previous variable with the same name
// ============================================
fn main() {
let x = 5; // First x
let x = x + 1; // Shadowing: Second x, value becomes 6
let x = x * 2; // Shadowing: Third x, value becomes 12
// Shadowing allows for a change in type!
let name = "Rust"; // &str Type
let name = name.len(); // became usize Type!
println!("x value: {}", x);
println!("name length: {}", name);
}
Output:
x value: 12
name length: 4
Output:
=== Inventory Management System ===
Shadowing ≠ mutability. Shadowing creates a new variable (new memory), while
mutmodifies the same block of memory. Shadowing can change a variable's type within the same scope, butmutcannot.
▶ Example 4: Comprehensive Exercise—Inventory Management Widget (Difficulty ⭐⭐⭐)
Output:
=== Inventory Management System ===
Products: Wireless Mouse
Unit Price: 42.49 yuan yuan
Inventory: 2500 items
--- Shipped Today 300 items ---
Remaining Inventory: 2500 items
--- Member Discount 15% off ---
Member Price: 42.49 yuan yuan
Price Tag: 42.49 yuan
Total Inventory Value: <total_value> yuan
Ample inventory (> 500), No need to restock
Out of Stock! Suggested Reorder 7500 items
// ============================================
// Comprehensive Example: Using Variables, Shadowing and Constants in Inventory Management
// Demo let/mut/const/shadowing Practical Applications
// ============================================
const MAX_STOCK: u32 = 10_000;
const DISCOUNT_THRESHOLD: u32 = 500;
fn main() {
let item_name = "Wireless Mouse";
let price: f64 = 49.99;
let mut stock: u32 = 2500;
println!("=== Inventory Management System ===");
println!("Products: {}", item_name);
println!("Unit Price: {:.2} yuan", price);
println!("Inventory: {} items", stock);
stock = stock - 300;
println!("\n--- Shipped Today 300 items ---");
println!("Remaining Inventory: {} items", stock);
let price = price * 0.85;
println!("\n--- Member Discount 15% off ---");
println!("Member Price: {:.2} yuan", price);
let price = format!("{:.2} yuan", price);
println!("Price Tag: {}", price);
let total_value = (stock as f64) * 49.99;
println!("\nTotal Inventory Value: {:.2} yuan", total_value);
if stock > DISCOUNT_THRESHOLD {
println!("Ample inventory (> {}), No need to restock", DISCOUNT_THRESHOLD);
} else {
let needed = MAX_STOCK - stock;
println!("Out of Stock! Suggested Reorder {} items", needed);
}
println!("Maximum Capacity: {} items", MAX_STOCK);
}
Output:
=== Inventory Management System ===
Products: Wireless Mouse
Unit Price: 49.99 yuan
Inventory: 2500 items
--- Shipped Today 300 items ---
Remaining Inventory: 2200 items
--- Member Discount 15% off ---
Member Price: 42.49 yuan
Price Tag: 42.49 yuan
Total Inventory Value: 109978.00 yuan
Ample inventory (> 500), No need to restock
Maximum Capacity: 10000 items
This example combines the use of the
constconstant, theletimmutable variable, thelet mutmutable variable, and variable shadowing (where the shadowed type changes fromf64toString), demonstrating how these different variable declaration methods work together in a real-world scenario.
▶ Example 5: Integer Overflow and Safe Arithmetic (Difficulty ⭐⭐)
Output:
u8 Maximum value: 255
255 + 1 (overflowing_add): <result>, Did overflow?: <did_overflow>
255 + 1 (checked_add): <checked>
255 + 100 (saturating_add): <saturating>
255 + 1 (wrapping_add): <wrapping>
i8 absolute value of minimum: <abs_result>, Did overflow?: <overflow>
// ============================================
// Integer Overflow Behavior: debug mode panic vs release mode wrapping
// ============================================
fn main() {
let max_u8: u8 = 255;
println!("u8 Maximum value: {}", max_u8);
// Safe Computation Method (No panic, Returns overflow result)
let (result, did_overflow) = max_u8.overflowing_add(1);
println!("255 + 1 (overflowing_add): {}, Did overflow?: {}", result, did_overflow);
let checked = max_u8.checked_add(1);
println!("255 + 1 (checked_add): {:?}", checked);
let saturating = max_u8.saturating_add(100);
println!("255 + 100 (saturating_add): {}", saturating);
let wrapping: u8 = max_u8.wrapping_add(1);
println!("255 + 1 (wrapping_add): {}", wrapping);
let neg_i8: i8 = -128;
let (abs_result, overflow) = neg_i8.overflowing_abs();
println!("i8 absolute value of minimum: {}, Did overflow?: {}", abs_result, overflow);
}
Output:
Original Input: Result: 84 (Type: &str)
Analysis Results: Result: 84
After unpacking: Result: 84 (Type: f64)
Round to an integer: Result: 84 (Type: i32, Truncation of Decimals)
After doubling: Result: 84 (Type: i32)
Final Output: Result: 84 (Type: String)
Alice: Score=95, Level=<alice_grade>
In Rust, integer overflow causes a panic in debug mode, but results in a wrap-around in release mode. It is recommended to use the
checked_*,saturating_*, andwrapping_*methods to explicitly control overflow behavior.
▶ Example 6: Practical Application of Variable Shadowing and Type Conversion (Difficulty: ⭐⭐⭐)
Output:
Original Input: Result: 84 (Type: &str)
Analysis Results: Result: 84
After unpacking: Result: 84 (Type: f64)
Round to an integer: Result: 84 (Type: i32, Truncation of Decimals)
After doubling: Result: 84 (Type: i32)
Final Output: Result: 84 (Type: String)
Alice: Score=95, Level=<alice_grade>
// ============================================
// Practical Examples of Variable Shadowing: String Parsing Chain
// ============================================
fn main() {
let input = "42.5";
println!("Original Input: {} (Type: &str)", input);
let input = input.parse::<f64>();
println!("Analysis Results: {:?}", input);
let input = match input {
Ok(value) => value,
Err(_) => 0.0,
};
println!("After unpacking: {} (Type: f64)", input);
let input = input as i32;
println!("Round to an integer: {} (Type: i32, Truncation of Decimals)", input);
let input = input * 2;
println!("After doubling: {} (Type: i32)", input);
let input = format!("Result: {}", input);
println!("Final Output: {} (Type: String)", input);
let alice_score = "95";
let alice_score = alice_score.parse::<u32>().unwrap_or(0);
let alice_grade = if alice_score >= 90 { "A" } else { "B" };
println!("Alice: Score={}, Level={}", alice_score, alice_grade);
}
Output:
Original Input: 42.5 (Type: &str)
Analysis Results: Ok(42.5)
After unpacking: 42.5 (Type: f64)
Round to an integer: 42 (Type: i32, Truncation of Decimals)
After doubling: 84 (Type: i32)
Final Output: Result: 84 (Type: String)
Alice: Score=95, Level=A
Variable shadowing allows for a gradual conversion of data types within the same scope—from
&str→Result<f64>→f64→i32→String—using the same variable name at each step, resulting in clear, redundancy-free code.
❓ FAQ
i32 instead of i64?let mut?mut modifies the same variable (without changing its type).char takes up 4 bytes, so aren't strings a waste of space?char.const and let?const is inlined at compile time, while let is allocated at runtime.📖 Summary
- Rust variables are immutable by default; use
mutto declare mutable variables constCompile-time constants must be type-annotated and follow the all-caps naming convention- Integer types are classified as signed (i) and unsigned (u); by default
i32 - The default floating-point format is
f64;charoccupies 4 bytes and supports Unicode. - The Rust compiler has powerful type inference, but you can explicitly specify types when needed.
- Variable shadowing allows a new variable with the same name to override an old variable; it can even change the type.
📝 Exercises
- Difficulty ⭐: Declare a variable of type
u16and assign it the value 65535, then try to change it to 65536 and observe the compiler's error message. - Difficulty ⭐⭐: Write a program that uses
let mutto declare a variable that accumulates the sum of numbers from 1 to 5, and output the result. - Difficulty ⭐⭐⭐: Try declaring a
constconstant outside offn main()and using it inside the function. Then try declaring aletvariable outside the function and observe the compiler error—this will help you understand the global scope ofconst.