Current mission: finish filling in the OTDs

SNCA:Rust/Tutorial

From Soyjak Wiki, the free ensoyclopedia
Jump to navigationJump to search

The following is a brief tutorial on the Rust programming language. Because much of the design of Rust is influenced by C++, this tutorial makes the assumption you are at least familiar with C++; see the C++ tutorial for additional context.

Hello World[edit | edit source]

The Hello World program in Rust is as follows:

fn main() {
    println!("Hello, world!");
}

Unlike in C and C++, the standard library is always implicitly available in Rust. Also, the main function in Rust typically returns () (the "unit type"), basically similar to void in C, C++, Java, C#, etc.

println!() is a macro. Unlike macros in C/C++, Rust macros interact directly with the syntax tree of Rust rather than through direct text substitution. A macro in Rust ends with an exclamation mark at the end of its name.

Basic types[edit | edit source]

Rust consists of the following basic types:

  • i8, i16, i32, i64, i128: signed integers of sizes 8, 16, 32, 64, 128 respectively
  • u8, u16, u32, u64, u128: unsigned integers of sizes 8, 16, 32, 64, 128 respectively
  • f32, f64, f128: floating point numbers of sizes 32, 64, and 128 respectively
  • bool: a boolean value, either true or false
  • char: a single Unicode scalar value (note that unlike C/C++ where char is ASCII, char in Rust is a full Unicode character)
  • (): the unit type, has exactly one value (itself) and represents nothing, similar to void in other languages
  • str: a string slice, the most primitive string type
  • isize: a pointer-sized signed integer type, usually 8 bytes, similar to ssize_t in C
  • usize: a pointer-sized unsigned integer type, usually 8 bytes, similar to size_t in C

Other deeply fundamental types include:

  • String: a UTF-8 encoded growable string that owns its contents. It is distinct from str (the primitive).
  • Option<T>: an option type, representing either the presence of a value (of type T) or the absence of the object. If there is nothing in Option, its value is None.
  • Result<T, E>: a result type, representing either an object (of type T) or an error (of type E)
  • [T; N]: an array of objects of type T of length N
  • (T1, T2, ..., TN): a tuple type, which can hold an indefinite number of objects of any type
  • Vec<T>: a vector type, representing a dynamic array storing objects of type T
  • HashMap<K, V>: a dictionary-like type storing keys of type K, and mapping them to objects of type V

Variables[edit | edit source]

Much like in other languages, variables in Rust have a similar declaration, but use the keyword let. In most cases, the type of the variable can be omitted because it can usually be inferred by the compiler, but to explicitly label the type, use a colon.

fn main() {
    let age = 18;
    let name: &str = "Nate"; // Explicitly declare the type as &str (string slice)
    let website = "soyjak.party";
    println!("{name} is {age}, and thus old enough to post on {website}!");
}

In Rust, variables are immutable (like C/C++ const) by default. To define a compile-time constant, use const (which is like C/C++ constexpr). To allow variables to be mutable, use let mut.

let a: i32 = -5;
let mut b: u16 = 8;
const MAX_VALUE: usize = 300;

Control flow[edit | edit source]

Rust features the usual control flow structures as seen in C:

let x = 5;

if x > 0 {
    println!("x = {x} is positive");
} else if x < 0 {
    println!("x = {x} is negative");
} else {
    println!("x is exactly zero");
}

Rust also features a if let, which unwraps an Option<T>:

let opt: Option<i32> = Some(5);
if let Some(x) = opt {
    println!("x's value is {x}");
}

Also, instead of switch, Rust introduces the match block. This acts more generally than switch, particularly useful in pattern matching.

let day = 2;

match day {
    1 => println!("Monday"),
    2 => println!("Tuesday"),
    3 => println!("Wednesday"),
    _ => println!("Some other day"),
}

Loops[edit | edit source]

Rust features the same loops as seen in C and C++, and the same break and continue statements.

For loop[edit | edit source]

For loops in Rust have a different syntax, as they are iterator-based. This is more similar to the range-based for loop in C++:

// prints for i = 0, 1, 2, 3, 4
for i in 0..5 {
    println!("{i}");
}

// prints for i = 0, 1, 2, 3, 4, 5
for i in 0..=5 {
    println!("i = {i}");
}

While loop[edit | edit source]

The while loop in Rust is basically the same as in C/C++:

let mut x = 3;

while x > 0 {
    println!("x = {x}");
    x -= 1;
}

Infinite loop[edit | edit source]

In Rust, instead of using while true, there is an explicit syntax for an infinite loop:

loop {
    println!("Cobson will always be a gem!");
}

Ownership[edit | edit source]

In Rust, every value has only a single owner. Once that owner goes out of scope, the value is dropped (i.e. destroyed, freed). When a value is passed or assigned, ownership moves:

let s1 = String::from("hello");
let s2 = s1; // ownership moves to s2

// println!("{}", s1);
// Error, as s1 is no longer valid

Passing a value into a function moves it, but it can be given back by returning it. Meanwhile, it can be "borrowed", i.e. taken a reference (no ownership is taken).

Each value has one owner and there can only be one owner at a time.

Rust enforces a set of rules that ensure memory safety without a garbage collector:

  • You may have any number of immutable references (&T), OR exactly one mutable reference (&mut T), but not both simultaneously.
  • References must always be valid; there are no dangling references in Rust.
  • Borrowing does not transfer ownership in Rust.
  • A borrow ends when a variable is last used. For example:
let mut name = String::from("Cobson");

let r1 = &s;
println!("{r1}"); // last use of r1

let r2 = &mut s; // allowed now
r2.push_str(" will always be a gem!");

Reading input[edit | edit source]

To read input, use std::io::stdin() and call the read_line() function.

use std::io;

fn main() {
    println!("What's your name, nusoi?");
    let mut name = String::new();
    io::stdin()
        .read_line(&mut name)
        .expect("Failed to read name!");
    println!("{name}? That's a gemmy name.");
}

Thus, to create a star pyramid in Rust:

use std::io::{self, Write};

fn main() {
    let mut input = String::new();

    print!("Enter number of rows: ");
    Write::flush(&mut io::stdout())
        .unwrap();
    io::stdin()
        .read_line(&mut input)
        .expect("Failed to read");
    let rows: i32 = input.trim()
        .parse()
        .expect("Not a number!");
    println!();
    let mut i = rows;

    while i > 0 {
        for _ in 1..=(i / 2) {
            print!(" ");
        }

        for _ in i..=rows {
            print!("*");
        }
        println!();
        i -= 2;
    }
}

Functions[edit | edit source]

In Rust, a function is declared with the fn keyword. To return a value from a function immediately, use the return keyword, but if you are at the end of the function, you can omit the return and semicolon at the end (no semicolon indicates an expression, which is returned, while a present semicolon indicates a statement and thus returns nothing (()).

fn post_bait() {
    println!("Trans rights are human rights!");
}

fn current_admin_number() -> i32 {
    6
}

Structs[edit | edit source]

A struct is like what it is in C. It defines a custom aggregate data type.

struct Nusoi {
    name: String,
    years: u16,
}

let nate: Nusoi = Nusoi {
    name: String::from("Nate Higgers"),
    years: 1,
};

However, methods cannot be defined here. Instead, you must define them in a impl block:

impl Nusoi {
    fn new(name: String, years: u16) -> Self {
        Self { name, years }
    }

    fn participate_in_raids(&self) {
        println!("OYYYY DOCTOS 'ox and 'ape this tranny!!!");
    }
}

Traits[edit | edit source]

Rust does not support inheritance like in C++ or Java, however a trait behaves similar to an interface in Java: a set of methods that a struct implements.

trait Admin {
    fn ban(&self, user: &Nusoi, reason: &str);
}

impl Admin for Nusoi {
    fn ban(&self, user: &Nusoi, reason: &str) {
        println!("Banning user {user.name} for reason: {reason}");
    }
}

Enums[edit | edit source]

Enums in Rust are similar to that of C:

enum Direction {
    North,
    South,
    East,
    West,
}

But, Rust enums are in fact much more powerful than in C. They can hold different data, and can be pattern-matched over with match:

enum Message {
    Text(String),
    Number(i32),
    Quit,
}

let m1 = Message::Text(String::from("o algo"));
let m2 = Message::Number(42);
let m3 = Message::Quit;

fn handle_message(msg: Message) {
    match msg {
        Message::Text(s) => println!("Text: {}", s),
        Message::Number(n) => println!("Number: {}", n),
        Message::Quit => println!("Quit message"),
    }
}

Enums may also have struct-like variants:

enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
}

let c = Shape::Circle { radius: 2.5 };

match c {
    Shape::Circle { radius } => println!("Circle with radius {}", radius),
    Shape::Rectangle { width, height } => {
        println!("Rectangle {} x {}", width, height)
    }
}

Furthermore, enums may also have methods with an impl block.

impl Shape {
    fn area(&self) -> f64 {
        match self {
            Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
            Shape::Rectangle { width, height } => width * height,
        }
    }
}

Generics[edit | edit source]

A generic in Rust is basically what templates are in C++: they allow for flexible, reusable code by instantiating a different version of the function for each type.

use std::ops::{Add, Sub, Mul, Div};

fn identity<T>(value: T) -> T {
    value
}

let x = identity(5); // T = i32
let y = identity("Cobson") // T = &str

trait Number: Copy + Add<Output = T> + Sub<Output = T> + Mul<Output = T> {}

impl<T> Number for T
where T: Copy + Add<Output = T> + Sub<Output = T> + Mul<Output = T> {}

#[derive(Debug, Clone, Copy)]
struct Point<T: Number> {
    x: T,
    y: T,
}