iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
⚙️

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:

2 + 2 * 2 = 2 + 4 = 6

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:

(2 + 2) * 2 = 4 * 2 = 8

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)

GitHubで編集を提案

Discussion