Hello, fellow Rustaceans (or aspiring ones)!Welcome back to Week 2 of my deep dive into the Rust programming language, and this week I explored compound types and control flow. Here's what I learned, where I got stuck, and how I'm making it make sense.
Here's what I tackled:
- A. Compound Types:
-
String
(and its relationship with&str
) - Arrays
- Slices
- Tuples
- Structs
- Enums
- B. Control Flow:
if/else if/else
loop
while
-
for
loops with iterators
Let's dive into how these building blocks are starting to click for me, and how AI is continuing to be a valuable co-pilot.
A. Compound Types
Last week, we dealt with simple numbers and booleans. This week was all about grouping related data and defining my own custom types.
1. String
vs. &str
This was probably the most nuanced part of learning about text.
- String
: Owned, mutable, grows on the heap. Think of it as a dynamic, editable document.
- &str
(string slice):
An immutable reference to a String
(or a string literal that lives in the binary). Think of it as a view or a borrow of part of a document.
Understanding when to use which and how they interact is fundamental for efficient and safe string
manipulation in Rust.
fn main() {
let mut s1 = String::from("Hello"); // Owned, mutable String
s1.push_str(", world!");
println!("{}", s1);
let s_slice: &str = &s1[0..5]; // Immutable slice from s1
println!("Slice: {}", s_slice);
let literal_slice: &str = "This is a literal"; // String literal is also &str
println!("Literal: {}", literal_slice);
// AI helped clarify conversion:
// Prompt: "How do I convert &str to String in Rust?"
let from_slice = String::from(literal_slice);
println!("Converted to String: {}", from_slice);
}
My AI Assistant Insight: I found myself frequently asking my AI assistant for clarification on String vs. &str
scenarios. It quickly provided examples for conversions (.to_string(), String::from())
and explained the performance implications of each, helping me choose the right type for the job.
2. Arrays and Slices: Fixed vs. Dynamic Collections
- Arrays: Fixed-size collections of elements of the same type, allocated on the stack. Great for compile-time known sizes.
let arr: [i32; 5] = [1, 2, 3, 4, 5];
println!("First element: {}", arr[0]);
// arr[5] would cause a panic at runtime!
-
Slices: A dynamic view into a part of a collection (like an array or
Vec
). They don't take ownership and are essentially a pointer to the start and a length.
let a = [1, 2, 3, 4, 5];
let slice = &a[1..4]; // slice is &[2, 3, 4]
println!("Slice: {:?}", slice); // Needs Debug trait for printing arrays/slices
Slices are incredibly powerful for passing parts of collections around efficiently without copying.
3. Tuples: Grouping Different Types Together
Tuples are simple, fixed-size collections that can hold values of different types. Useful for returning multiple values from a function.
fn get_user_info() -> (&str, i32, bool) {
("Alice", 30, true)
}
fn main() {
let person: (&str, i32, bool) = ("Bob", 25, false);
println!("Name: {}, Age: {}", person.0, person.1); // Access by index
let (name, age, _is_active) = get_user_info(); // Destructuring
println!("User from function: {}, {}", name, age);
}
4. Structs
Structs are like classes in other languages (without methods, initially), they let you create custom data types with named fields. This is fundamental for organizing related data.
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
username: String::from("alice123"),
email: String::from("alice@example.com"),
active: true,
sign_in_count: 1,
};
println!("User: {}, Email: {}", user1.username, user1.email);
// AI's help with update syntax:
// Prompt: "How do I create a new struct instance from an existing one in Rust?"
let user2 = User {
email: String::from("bob@example.com"),
..user1 // Fills in remaining fields from user1
};
println!("User2: {}", user2.username); // User2's username is also alice123
}
5. Enums
Enums
are fantastic for representing a set of distinct, related possibilities. Coupled with the match control flow, they become incredibly powerful for handling different states or data variations.
enum Message {
Quit,
Move { x: i32, y: i32 }, // Enum variant with anonymous struct
Write(String), // Enum variant with a String
ChangeColor(i32, i32, i32), // Enum variant with a tuple
}
fn process_message(msg: Message) {
match msg {
Message::Quit => println!("The Quit message was received."),
Message::Move { x, y } => println!("Move to {} {}", x, y),
Message::Write(text) => println!("Text message: {}", text),
Message::ChangeColor(r, g, b) => println!("Change color to R:{} G:{} B:{}", r, g, b),
}
}
fn main() {
let m1 = Message::Quit;
let m2 = Message::Move { x: 10, y: 20 };
let m3 = Message::Write(String::from("hello Rust"));
let m4 = Message::ChangeColor(255, 0, 100);
process_message(m1);
process_message(m2);
process_message(m3);
process_message(m4);
}
B. Control Flow
After defining my data, the next step was to control what my program does and when. Rust's control flow constructs are powerful and familiar, but with some Rust-specific nuances.
1. if/else if/else
: Conditional Execution
Pretty standard, but remember that if
conditions must evaluate to a bool
. Also, if
is an expression, which is a neat feature for assigning values conditionally.
fn main() {
let number = 7;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
let result = if number % 2 == 0 { "even" } else { "odd" };
println!("The number is {}", result);
}
2. loop
: Infinite Loops (with break
and continue
)
The loop
keyword creates an infinite loop, perfect for retrying operations or listening for events. break
exits, continue
skips to the next iteration. You can even return values from a loop
.
fn main() {
let mut counter = 0;
let result = loop {
counter += 1;
if counter == 10 {
break counter * 2; // Returns a value from the loop!
}
};
println!("The result is {}", result); // result will be 20
}
3. while
: Conditional Loops
For loops that run as long as a condition is true.
fn main() {
let mut number = 3;
while number != 0 {
println!("{}!", number);
number -= 1;
}
println!("LIFTOFF!!!");
}
5. for
Loops
for
loops Iterates Over Collections. This is the most common loop for iterating over collections. Rust's for loop works with iterators, which is a very efficient and safe way to process data.
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() { // Iterate over array elements
println!("The value is: {}", element);
}
// Looping with a range
for number in 1..4 { // Range is exclusive of the end (1, 2, 3)
println!("{}!", number);
}
// AI helped with reverse iteration:
// Prompt: "How do I loop backwards over a range in Rust?"
for number in (1..4).rev() { // Reverse range
println!("{}!", number);
}
}
Week 2 Reflections
This week felt like I gained a lot of power in organizing and controlling my programs. Understanding String
vs. &str
and the different compound types like struct
and enum
is crucial for writing robust Rust. The control flow structures felt familiar from other languages, but seeing them used as expressions (like if
statements) was a neat Rust-specific twist.
My AI assistant continues to be a fantastic resource for quick syntax lookups, clarifying nuances (especially with String
vs. &str
), and discovering idiomatic ways to do things (like .rev()
on iterators). It's accelerating my learning without doing the thinking for me, which is exactly the balance I'm aiming for in 2025.
In case you missed week 1, check it out here.
See y'all in week 3.
Top comments (0)