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
Top comments (19)
thank you for sharing ...simple and easy to understand
That was the goal of the article: simplifying the rust syntax. Of course, it's not exhaustive, but it was more about lowering the complexity barrier. Thank you
:)
I was going to learn rust, this is a good starting point. Especially the ownership and borrowing part, I can assure you that I have missed up a lot of C code... mostly skill issues as always.
glad it helped
Beautiful, thank you . It feels like rust is Somewhat similar to scala and golang ...
Gonna implememt life times To my language, thanks
lifetimes is a concept specific to the rust programming language
Im sure they wont mind me using as well
gl
What a great article.. Rusts pattern matching is really clever. I'm bored to death with C#, and Rust seems to inspire me again
Very inconsistent, irregular, weird and illogical systax. 🤦
It really is, no wonder Rust programmers make $30k/year more than us "lowly" C++ programmers at my company, no one can tell if what they're doing is right or not🤦🏼♀️ But here I am learning it since it's the only way I'll make over $200k without scoring a senior role...
interesting take, it seems the opposite to me. Can you elaborate on each point?
variables
, they areconstants
by default! And one has to usemut
to define a variable variable! While most of the programming languages require to defineconstants
by some other specific way.6..=10
. Is..
one operator or..=
? If..
is an operator then other match/switch cases should also be prefixed with=
, for example:=1 => println("One!")
. Otherwise=
in..=
seems extra.Plus, I have some questions/doubts:
_
statement has a comma too??
operator examples, it looks like the code is returningOk
orErr
. What does returningOk
orErr
means?It's fine if you don't make any changes to the post to make me understand or explain anything here. I don't know Python language but I have read its documentation for beginners and I liked its syntax. Python seemed most logical than most of the programming languages.
Immutable variables are not constants, since their values are not known at compile time, constants are values written to the binary during compilation; immutable variables can be assigned during program execution and immutable from there. There are several compression errors like this in all your criticisms, but I think the main one is that you think Rust has to look like the languages you know and there is absolutely no reason for that. All languages are different!
@nikunjbhatt I think you are complaining that Rust does not look like some languages you know.
Many of those weirdness you cited come from SML. If you are curious, see how to implement binary tree in SML. It's super easy. The pattern matching and algebraic datatypes came straight from there (or some other language of the SML family)
Why? I am curious now
Indeed!