Sans IO in Rust
Introduction to Rust
Getting Started with Rust
Rust is a modern programming language focused on three things: safety, speed, and concurrency. Think of it as having the power of languages like C++ but with a friendly compiler that acts as a safety net, preventing common bugs before your code even runs.
Variables, Functions, and Flow
Like other languages, Rust uses variables to store data. You declare a variable with the let keyword. By default, variables in Rust are immutable, meaning their values can't be changed once set. This is a deliberate safety feature. If you need a variable you can change, you must explicitly say so with mut.
fn main() {
// This variable is immutable. Its value can't be changed.
let x = 5;
println!("The value of x is: {}", x);
// This variable is mutable. We can change its value.
let mut y = 10;
println!("The initial value of y is: {}", y);
y = 20;
println!("The new value of y is: {}", y);
}
Code is organized into functions. The main function is the entry point of every Rust program. You define a function using the fn keyword, followed by the function name, parentheses for parameters, and curly braces for the function body.
Controlling the flow of your program is done with familiar structures. An if expression lets you branch your code depending on a condition.
fn check_number(n: i32) {
if n < 0 {
println!("{} is negative", n);
} else if n > 0 {
println!("{} is positive", n);
} else {
println!("{} is zero", n);
}
}
For repetition, Rust provides a few kinds of loops. A for loop is excellent for iterating over a collection of items, like a range of numbers.
fn count_to_three() {
// The `1..4` creates a range that includes 1, 2, and 3.
for number in 1..4 {
println!("{}", number);
}
println!("LIFTOFF!!!");
}
The Ownership Model
Here's where Rust gets really interesting. Many languages use a garbage collector to clean up memory that's no longer in use, which can add overhead. Others, like C++, require the programmer to manage memory manually, which is powerful but can lead to errors. Rust has a third approach: ownership.
The rules are simple but have profound implications:
- Each value in Rust has a variable that’s called its owner.
- There can only be one owner at a time.
- When the owner goes out of scope, the value is dropped (i.e., its memory is freed).
When you assign a variable to another, ownership can be moved. For simple types like integers, the value is copied. But for more complex data that lives on the heap, like a String, ownership is transferred. The old variable is no longer valid.
fn main() {
let s1 = String::from("hello");
let s2 = s1; // Ownership of the string is moved from s1 to s2.
// This next line would cause a compile error because s1 is no longer valid!
// println!("s1 is: {}", s1);
}
This prevents a "double free" error, where two variables might try to free the same memory when they go out of scope. The compiler enforces this at compile time, guaranteeing memory safety.
Borrowing and Lifetimes
What if you want to let a function use a value without giving it ownership? This is where borrowing comes in. You can create a reference to a value, which allows you to access it without taking ownership. A reference is like a library card: it lets you use the book, but you don't own it, and you have to give it back.
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // We pass a reference to s1.
println!("The length of '{}' is {}.", s1, len);
}
// This function 'borrows' a String.
fn calculate_length(s: &String) -> usize {
s.len()
}
The ampersand & creates a reference. Because calculate_length only borrows s1, the main function retains ownership and can still use s1 after the call.
References are also immutable by default. If you need to change the borrowed value, you can create a mutable reference using &mut. However, Rust enforces a crucial rule:
You can have either one mutable reference or any number of immutable references to a particular piece of data in a particular scope, but not both at the same time.
This rule prevents data races—a type of bug that happens when multiple parts of a program try to modify the same data concurrently. Rust stops these bugs from ever happening by checking the rules at compile time.
Finally, the compiler needs to ensure that any reference will not outlive the data it refers to. This concept is called lifetimes. For now, just know that Rust is smart enough to figure this out in most cases, ensuring you never have a dangling pointer—a reference that points to invalid memory.
Ready to check your understanding? Let's see what you've learned.
What is the default behavior of variables declared with the let keyword in Rust?
In Rust's ownership system, how many owners can a value have at any given time?
These concepts—ownership, borrowing, and lifetimes—form the foundation of Rust's safety guarantees. They might feel different at first, but they quickly become powerful tools for writing correct and efficient code.
