iTranslated by AI
Summary of Rust's Special Syntax (Regularly Updated)
Introduction
This is a summary I put together to look back on whenever I get stuck with Rust.
I welcome corrections or additions via comments or GitHub pull requests.
Type Declaration Rules
Type declarations in Rust are written as follows.
let x: i32 = 5;
let y: &str = "Hello";
let z: Vec<i32> = vec![1, 2, 3];
The left side of the : is the variable name, and the right side is the type name.
The type name can be omitted if it can be clearly inferred.
let x = 5; // i32
let y = "Hello"; // &str
let z = vec![1, 2, 3]; // Vec<i32>
What is the "!"?
Hello World in Rust is written as follows.
fn main(){
println!("HelloWorld");
}
There is a ! at the end of println!.
This is what is known as a macro.
Macros are called by appending a !.
In Rust, you cannot declare functions that take a variable number of arguments, but since println! and vec! are macros, they can accept a variable number of arguments.
Also, unlike C macros, Rust macros are not mere text substitutions.
The following looks like it would result in 8 at first glance.
#include <stdio.h>
#define x() 2 + 2
int main(){
printf("%d\n", x() * 2); // 6
return 0;
}
However, when executed, since C uses simple substitution, it becomes 2 + 2 * 2.
It is evaluated as follows:
In the case of Rust, macros are also evaluated as syntax.
macro_rules! x {
() => {
2 + 2
};
}
fn main(){
println!("{}", x!() * 2);// 8
}
It is evaluated as follows:
Omitting return
In Rust, the last evaluated expression becomes the return value. However, this rule only applies to the end of a block. You cannot omit return anywhere else. The "end" refers to the position immediately before a }. Additionally, if you omit return but include a ;, the return value becomes ().
fn average(numbers: &[f64]) -> f64 {
let mut sum = 0.0;
for &n in numbers {
sum += n;
}
sum / numbers.len() // This is the return value
}
fn main() {
println!("Average value: {}", average(&[1.0, 2.0, 3.0]));
}
Meaning of {}
What do you think the following code will return?
fn main() {
let x = {
let y = 5;
y + 1
};
println!("{}", x);
}
This is a syntax not commonly seen in other languages, although JavaScript has something similar.
const x = ()=>{
let y = 5;
return y + 1;
}
console.log(x());
That is a function declaration. In Rust, {} represents an expression. The entire content within the {} block is the value itself. However, as mentioned earlier, if the last expression in a block where return is omitted ends with a ;, the return value becomes (). You can think of it as declaring a local, throwaway function.
True Meaning of Variable Declaration
Variables in Rust are a bit special. A variable declaration in Rust is called a "variable binding," where a value is bound to an owner (the variable). Also, when declaring a variable, the result of the expression on the right-hand side is assigned to the left-hand side. At this time, the left-hand side and the right-hand side must have the same type and pattern. In other words, the true meaning of let is "if the left-hand side and the right-hand side are the same type and pattern, bind the value to the variable."
Look at the following code.
fn main(){
let (x, y) = (1,2,3); //error
}
The right-hand side returns three values 1, 2, 3, but since the left-hand side only accepts two values, it results in an error. The following code also results in an error.
fn main(){
let Some(x) = Some(1); //error
}
At first glance, it looks fine, but since Some might return None, it fails because that pattern hasn't been accounted for.
Meaning of if let
I explained earlier that a variable declaration in Rust means "if the left-hand side and the right-hand side are the same type and pattern, bind the value to the variable."
Look at the following code.
The syntax if let is being used.
fn main() {
if let Some(x) = Some(3) {
println!("{:?}",assert_eq!(x, 3));
};
}
if let is not a simple combination of if and let keywords.
However, it's not entirely separate either; it's a syntax that blends the meanings of both.
As mentioned repeatedly, let means "if the left-hand side and the right-hand side have the same type and pattern, bind the value to the variable."
if means "if the condition is true."
In other words, if let means "if the binding with let is possible, then execute the {} block."
A match statement can sometimes become redundant because it requires accounting for all possible patterns. In those cases, use if let.
Meaning of _
The underscore _ has various meanings in Rust.
Unused variables
fn main() {
let _x = 5;
}
Wildcard in match
fn main() {
let _x = 5;
match _x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}
Wildcard in patterns
fn main() {
let _x = (1,2,3);
let (_, _, n) = _x;
println!("{}", n); // 3
}
Wildcard in types
fn main() {
let _x:<Vec<_>> = vec![1,2,3];
}
Numeric separator
fn main() {
let _n = 1_000_000;
}
Wildcard for type inference
fn main() {
let x = 0.1;
let mut y = 0;
y = x as _;
println!("{}", y); // 0.1
}
Ownership
Many programming languages handle memory management by either leaving it entirely up to the programmer (like C) or by automatically releasing memory with a Garbage Collector (GC). Rust manages it using a third concept called "Ownership."
For example, the following code will result in an error:
fn main() {
let name1 = String::from("Sato");
let name2 = name1;
println!("{}", name1); // Error
}
error: borrow of moved value: `name1`
label: move occurs because `name1` has type `String`, which does not implement the `Copy` trait
Ownership has moved from name1 to name2, so name1 is deallocated, resulting in an error.
However, the following does not result in an error:
fn main() {
let age1 = 18;
let age2 = age1;
println!("{}", age1); // 18
}
Since it implements the Copy trait, age1 is not deallocated.
Without Copy trait
- String
- Vec
- etc.
With Copy trait
- Floating-point types
- char type
- bool type
- etc.
Through the concept of ownership, it is possible to prevent memory leaks and the accumulation of wasted memory. Additionally, by clarifying who has access permissions, it prevents issues like concurrent access during parallel processing. Since parallelization would make this explanation quite long, I will omit it here.
Converting Strings to Numbers
In Rust, you can convert a string to a number as follows:
fn main() {
let x = "1";
let y = x.parse::<i32>().unwrap();
println!("{}", y); // 1
}
To convert a string received from standard input into a number, you can do it like this:
fn main() {
let mut x = String::new();
std::io::stdin().read_line(&mut x).unwrap();
let y = x.trim().parse::<i32>().unwrap();
println!("{}", y); // 1
}
trim() is a method that removes whitespace from both ends of a string. While languages like Python or Ruby can convert to an integer even if a newline character is present, Rust will result in an error if there is a newline character.
Array Access
In Rust, array access is treated as a slice. You can do the same with vec! (vectors).
fn main() {
let x = [1,2,3,4,5];
println!("{}", x[0]); // 1
println!("{:?}", &x[1..3]); // [2, 3]
}
[start..end] retrieves elements from the start index up to end-1.
Numeric-Only Type Declarations
In Rust, you can append the type suffix to numeric literals.
fn main() {
// All of these have the same meaning
let w = 1;
let x = 1i32;
let y:i32 = 1;
let z:i32 = 1i32;
println!("{}", w); // 1
println!("{}", x); // 1
println!("{}", y); // 1
println!("{}", z); // 1
}
Conditional Assignment
Rust supports conditional assignment. However, it results in an error if the return types of each branch differ.
fn main() {
let x = 1;
let y = if x == 1 {
1
} else {
2
};
println!("{}", y); // 1
}
Loops
While common languages typically have for and while, Rust provides loop, while, and for.
loop
loop creates an infinite loop. Use break to exit the loop.
fn main() {
let mut count = 0;
loop {
count += 1;
if count == 10 {
break;
}
}
println!("{}", count); // 10
}
while
while loops as long as a condition is true.
fn main() {
let mut count = 0;
while count < 10 {
count += 1;
}
println!("{}", count); // 10
}
for
for iterates over a specified range or collection.
fn main() {
for i in 0..10 {
println!("{}", i); // Outputs 0 to 9
}
}
Pattern Matching with @
In Rust, you can use @ to perform pattern matching and binding simultaneously. This is useful when you want to perform pattern matching while also wanting to use the matched value in subsequent processing.
For example, the following code binds to id_variable if id is between 3 and 7.
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello {
id: id_variable @ 3..=7,
} => println!("Found an id in range: {}", id_variable),
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
}
Message::Hello { id } => println!("Found some other id: {}", id),
}
Since the matched value is stored in id_variable, you can use it with the println! macro.
The second match arm matches if id is between 10 and 12, but it doesn't capture which specific value was matched.
The third match arm doesn't limit the range, so it matches if no previous arms did.
In this way, using @ allows for simultaneous pattern matching and binding, enabling more flexible matching.
Under Construction 🚧
(Last updated: 2024-01-22)
Discussion