Getting familiar with Rust's Syntax
So, you've decided to learn Rust.
Good choice! Rust is an awesome language that combines the power of systems programming with modern language features, and it can be used for Web Development and blockchain.
However, when learning Rust, one of the blockers is getting familiar with its Syntax.
In this article, I'll do my best to provide examples that will make you feel comfortable with them.
Getting Started: Variables and Types
Let's start with the basics: variables.
By default, Rust variables are immutable. This might sound weird if you're used to languages like Python or JavaScript, which allow variables to change.
fn main() {
let x = 5; // x is immutable by default
// x = 6; // Uncommenting this will throw a compiler error
let mut y = 5; // y is mutable
y = 6; // No problem here
}
Notice the let
keyword? That's how you declare variables in Rust. If you want to change a variable, make it mutable with the mut keyword.
Type Annotations
Rust has great type inference: the compiler usually knows your variables' type.
But sometimes, you'll need to specify the type yourself:
// Here, we're explicitly saying that z is a 32-bit integer
let z: i32 = 10;
Rust's type system is one of its great advantages, so it’s worth getting comfortable with it early on.
Functions
Functions in Rust look pretty familiar if you've worked with other languages. But there are some syntax quirks to watch out for.
fn add(a: i32, b: i32) -> i32 {
a + b // No semicolon means this is the return value
}
Notice that we’re using -> to define the function's return type. Also, there's no return keyword here; Rust returns the last expression by default if you omit the semicolon.
It’s nice once you get used to it.
Ownership and Borrowing
Alright, here’s where things get interesting. Rust’s ownership model makes it stand out but can be tricky at first.
Let’s see another example
Ownership
In Rust, each value has a variable, which is its owner.
When the owner goes out of scope, the value is dropped. This is how Rust avoids memory leaks.
fn main() {
let s1 = String::from("hello");
let s2 = s1; // Ownership of the String is moved to s2, s1 is now invalid
// println!("{}", s1); // This would cause a compile-time error
}
Here, s1 no longer owns the String after it’s moved to s2.
If you try to use s1 after that, Rust won’t let you. It’s like Rust says: "Hey, that’s not yours anymore."
Borrowing
But what if you want to use a value without taking ownership of it?
That’s where borrowing comes in. You can borrow a value by using references:
fn main() {
let s1 = String::from("hello");
let len = calculate_length(&s1); // We're borrowing s1 here
println!("The length of '{}' is {}.", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
In this example, &s1 is a reference
to s1. The calculate_length function temporarily borrows s1 without taking ownership. After the function is done, s1 is still valid. That's pretty cool.
Lifetimes
Lifetimes are how Rust keeps track of how long references are valid.
They can be confusing initially, but they’re crucial for safe memory management.
Let's see a very basic example, to get familiar with it.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
Here, 'a is a lifetime parameter. It means the references x and y must live at least as long as the return value. This ensures that we don’t return a reference to something that’s already been dropped.
Pattern Matching
Rust's match statement is like a switch on steroids. It’s one of my favorite parts of the language because it’s so powerful and expressive.
fn main() {
let number = 7;
match number {
1 => println!("One!"),
2 => println!("Two!"),
3 | 4 | 5 => println!("Three, Four, or Five!"),
6..=10 => println!("Between Six and Ten!"),
_ => println!("Anything else!"),
}
}
The match statement checks a value against multiple patterns and runs the code for the first matching pattern. The _ is a catch-all pattern, which is useful when you want to handle anything you haven’t explicitly matched.
Destructuring with Pattern Matching
You can also use match
to destructure complex data types like tuples or enums.
fn main() {
let pair = (2, 5);
match pair {
(0, y) => println!("First is zero and y is {}", y),
(x, 0) => println!("x is {} and second is zero", x),
_ => println!("No zeroes here!"),
}
}
This is just scratching the surface.
Match can do much more, but this should give you a solid foundation.
Error Handling
Rust doesn’t have exceptions. Instead, it uses the Result and Option types for error handling. It might feel a bit verbose initially, but it’s much safer than unchecked exceptions.
fn main() {
let result = divide(10, 2);
match result {
Ok(v) => println!("Result is {}", v),
Err(e) => println!("Error: {}", e),
}
}
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}
Here, Result
is a type that can be either Ok (success) or Err (error). This forces you to handle success and failure cases, which is great for writing robust code.
The ?
Operator
To make error handling a bit more ergonomic, Rust provides the ?
Operator. It’s a shorthand for propagating errors.
fn main() -> Result<(), String> {
let result = divide(10, 0)?; // If divide returns Err, it returns from the function immediately
println!("Result is {}", result);
Ok(())
}
This is Rust's saying, “If there’s an error, just return it.”
Advanced Syntax: Traits, Generics, and More
Now that we've got the basics down, let's dive into more advanced topics.
Traits: Interfaces (Kinda)
Traits are kind of like interfaces in other languages. They define shared behavior that different types can implement.
trait Summary {
fn summarize(&self) -> String;
}
struct Article {
title: String,
content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}", self.title, self.content)
}
}
Here, we define and implement a Summary trait for the Article struct. Now, any Article can be summarized. Traits are super powerful for writing generic and reusable code.
Generics: Writing Flexible Code
Generics let you write functions and types that work with any data type.
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
This function works with any type T that can be compared. The PartialOrd part is trait-bound, meaning that T must implement the PartialOrd trait, which allows for ordering comparisons.
Practical Tips: Writing Idiomatic Rust
Use rustfmt: Rust has a built-in formatter that keeps your code looking sharp. Just run cargo fmt in your project directory.
Use Rust Analyzer: This powerful IDE extension provides code completion, refactoring, and more. It’s like having an assistant that knows Rust inside and out.
Clippy: This is a linter for Rust that catches common mistakes and suggests improvements. Run cargo clippy to see what it finds.
Conclusion
This quick article is to get a bit more familiar with Rust.
I have a series of free videos about these specific topics.
You can check it out here
Subscribe to my newsletter
Read articles from Francesco Ciulla directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Francesco Ciulla
Francesco Ciulla
👋 Hi, I Am Francesco I am a Computer Scientist interested in Web3 and DevRel. I worked from 2017 to 2020 on the Copernicus project for the ESA European Space Agency as a Fullstack Developer. Docker Captain I have interviewed 195+ Developers on my YouTube Channel I am a Developer Advocate at daily.dev I have founded 4C, a community focused on Content Creation.