Sajiron
📖 If you haven't read it yet, check out the previous blog: Rust Programming: A Beginner’s Guide to Getting Started
Understanding Rust’s basic syntax and data types is essential for writing efficient and safe code. In this post, we’ll explore how Rust handles variables, mutability, data types, and naming conventions.
In Rust, variables are immutable by default. This means once a variable is assigned a value, it cannot be changed.
fn main() {
let x = 10;
println!("Value of x: {}", x);
// x = 20; // This will cause a compile-time error
}
To make a variable mutable, use the mut
keyword:
fn main() {
let mut y = 10;
println!("Before mutation: {}", y);
y = 20;
println!("After mutation: {}", y);
}
const
) vs Immutable Variables (let
)Rust provides two ways to declare immutable values: const
and let
. While both prevent modification after assignment, they have fundamental differences.
A const
variable is always immutable, requires explicit type annotation, and is evaluated at compile time. const
variables are stored directly in the program’s binary, making them available globally across the entire program. They cannot be shadowed or reassigned.
A let
variable, on the other hand, is also immutable by default but allows shadowing. If mutability is needed, you can explicitly declare the variable as mut
, allowing it to be reassigned instead of shadowed. Shadowing enables transformations of the same variable name while keeping it immutable. However, if you need to update a variable frequently, using mut
is a better option than shadowing. Unlike const
, let
variables are evaluated at runtime and do not require explicit type annotations. They exist within the scope they are defined and are removed once the scope ends.
Constants are always immutable and must have their types explicitly specified:
const MAX_USERS: u32 = 1000;
Rust provides type safety via static typing, meaning that every variable has a known type at compile time. Variables can be explicitly type annotated, but in most cases, the Rust compiler can infer the type from the context, reducing the need for explicit annotations.
fn main() {
let number = 42; // Compiler infers `i32`
let float_number: f64 = 3.14; // Explicit type annotation
println!("{} and {}", number, float_number);
}
Scope: Variables in Rust are limited to the block {}
in which they are declared. Once a variable goes out of scope, it is no longer accessible.
fn main() {
let outer_var = 10; // Outer scope variable
{
let inner_var = 20; // Inner scope variable
println!("Inner variable: {}", inner_var);
println!("Outer variable from inner scope: {}", outer_var);
}
// println!("Inner variable: {}", inner_var); // ❌ Error: inner_var is out of scope
println!("Outer variable: {}", outer_var); // ✅ Allowed
}
Shadowing: Shadowing in Rust allows a new variable to be declared with the same name, effectively hiding the old variable within a certain scope. Unlike mut
, which modifies a variable, shadowing creates a new variable binding, allowing transformations or type changes.
fn main() {
let x = 5; // First x
let x = x + 1; // Shadows the first x
let x = x * 2; // Shadows the second x
println!("Final x: {}", x); // Output: 12
let x = "hello"; // x is now a string
println!("x = {}", x);
}
Freezing: When a mutable variable is shadowed by an immutable one, the original mutable variable becomes frozen and cannot be modified in that scope.
fn main() {
let mut y = 10;
{
let y = y; // Shadows the mutable `y` with an immutable one
// y = 20; // ❌ Compile-time error: `y` is frozen
println!("Frozen y: {}", y);
}
y = 30; // ✅ Allowed: Outer `y` is still mutable
println!("Updated y: {}", y);
}
Rust categorizes its data types into scalar types and compound types based on their structure and complexity.
A scalar type represents a single value. Rust has four primary scalar types:
Integer types (i8
, i16
, i32
, i64
, i128
, u8
, u16
, u32
, u64
, u128
)
Floating-point types (f32
, f64
)
Boolean type (bool
)
Character type (char
)
A compound type groups multiple values together into a single unit. Rust has two primary compound types:
Tuples (Fixed-size collection of values of different types)
fn main() {
let data = (true, 42, "Rust", 3.14);
println!("Boolean: {}", data.0);
println!("Integer: {}", data.1);
println!("String: {}", data.2);
println!("Float: {}", data.3);
}
Arrays (Fixed-size collection of values of the same type)
fn main() {
let numbers = [10, 20, 30, 40, 50]; // An array of integers
println!("Array: {:?}", numbers);
println!("First element: {}", numbers[0]);
println!("Third element: {}", numbers[2]);
// Accessing an element dynamically
let index = 4;
println!("Element at index {}: {}", index, numbers[index]);
}
Feature | Tuple | Array |
Data Types | Can hold multiple different types (e.g., | Must hold elements of the same type (e.g., |
Size | Fixed-size, defined at compile time | Fixed-size, defined at compile time |
Access | Access via dot notation ( | Access via indexing ( |
Mutability | Immutable by default, but can be mutable ( | Immutable by default, but can be mutable ( |
Memory Layout | Stored in memory as a single unit | Stored sequentially in memory |
Use Case | Best for grouping related but different data (e.g., | Best for storing multiple values of the same type (e.g., |
Iteration | Cannot iterate directly | Can iterate using |
Rust does not allow implicit type conversion, so explicit casting using the as
keyword is required.
Example: Casting an Integer to a Floating Point
fn main() {
let x: i32 = 10;
let y: f64 = x as f64 + 5.5;
println!("y: {}", y);
}
Example: Casting a Float to an Integer
fn main() {
let num: f64 = 9.8;
let int_num: i32 = num as i32; // Truncates the decimal part
println!("Converted: {}", int_num);
}
Type aliasing allows you to create custom names for existing types, improving readability.
Defining a Type Alias
type Kilometers = i32;
fn main() {
let distance: Kilometers = 100;
println!("Distance: {} km", distance);
}
Using Aliases with Complex Types
type Point = (i32, i32);
fn main() {
let origin: Point = (0, 0);
println!("Origin: ({}, {})", origin.0, origin.1);
}
Consistent naming conventions improve code readability and maintainability. Rust follows specific naming rules for different elements, including variables, functions, constants, structs, and modules. Some of the concepts mentioned here, such as lifetimes, generics, and modules, will be discussed in detail in later blogs.
Consistent naming conventions improve code readability and maintainability. Rust follows specific naming rules for different elements, including variables, functions, constants, structs, and modules.
Use snake_case (lowercase words separated by underscores) for variable and function names.
This makes them easy to read and aligns with Rust's standard conventions.
let user_name = "Alice";
fn get_user() {}
Use SCREAMING_SNAKE_CASE for constants and static variables.
Constants are declared using const
, while static variables use static
.
const MAX_CONNECTIONS: u32 = 100;
static SERVER_PORT: u16 = 8080;
Use PascalCase (capitalizing each word) for struct, enum, and trait names.
struct UserProfile {
name: String,
age: u8,
}
enum Status {
Active,
Inactive,
}
Use snake_case for module names.
mod user_profile;
Use short, lowercase names like 'a
, 'b
for lifetimes.
fn lifetime_example<'a>(input: &'a str) -> &'a str {
input
}
Use single uppercase letters for generic types.
Common conventions:
T
for a generic type.
E
for an error type.
K
and V
for key-value pair types.
struct Container<T> {
item: T,
}
Following these naming conventions ensures consistency and improves collaboration within Rust projects.
Would you like to explore more Rust concepts? Stay tuned for the next tutorial! Now that you understand Rust’s syntax and data types, the next tutorial will cover Control Flow & Functions in Rust.