Easy Rust

repository·master·Indexed 27 days ago

https://github.com/dhghomon/easy_rust

A textbook designed to teach the Rust programming language using simplified English, specifically aimed at non-native English speakers and companies onboarding developers. It covers fundamental concepts including primitive types, type casting, variable mutability, shadowing, references, borrowing, and string formatting.

Tokens
37.4K
Snippets
119
Records
174
Agent score
44%

What's inside Easy Rust

  1. Understand Crates and Modules in Rust

    master

    In Rust, code is organized into crates and mods:

    • Crate: The collection of files that make up your project.
    • Mod (Module): A namespace used to organize functions, structs, and other items. Modules help with code structure, readability, and privacy.

    By default, everything in Rust is private. To make an item accessible from outside its module, you must use the pub keyword.

  2. Understand and use Enums in Rust

    master

    An enum (enumeration) is used when you want to represent a choice between multiple variants (one thing OR another thing). This differs from a struct, which is used for grouping multiple pieces of data together (one thing AND another thing).

    To declare an enum, use the enum keyword followed by the name and a block of comma-separated variants.

    enum ThingsInTheSky {
        Sun,
        Stars,
    }
  3. Manage variable mutability with `mut`

    master

    By default, variables declared with let are immutable (cannot be changed after assignment). To allow a variable to be modified, you must explicitly declare it as mutable using the mut keyword.

    Note: While mut allows you to change the value of a variable, it does not allow you to change the type of the variable.

  4. Implement a custom Error type

    master

    To create a custom error type in Rust that can be used with Box<dyn Error>, you must implement three things:

    1. std::error::Error trait.
    2. std::fmt::Debug trait (usually via #[derive(Debug)]).
    3. std::fmt::Display trait (by implementing the .fmt() method).

    Once implemented, these errors can be returned in a Result<T, Box<dyn Error>> to allow for heterogeneous error types in a single function.

    use std::error::Error;
    use std::fmt;
    
    #[derive(Debug)]
    struct ErrorOne;
    
    impl Error for ErrorOne {}
    
    impl fmt::Display for ErrorOne {
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
            write!(f, "You got the first error!")
        }
    }
    
    // Usage in a function:
    fn returns_errors(input: u8) -> Result<String, Box<dyn Error>> {
        match input {
            0 => Err(Box::new(ErrorOne)),
            _ => Ok("Looks fine to me".to_string()),
        }
    }
  5. Create type aliases to simplify complex types

    master

    Type aliases allow you to give a new, more readable name to an existing type using the type keyword. This is helpful for shortening long, nested types or giving descriptive names to generic collections.

    Note: A type alias is not a new type; the compiler treats it as the underlying type. For example, type File = String; means File and String are interchangeable.

  6. Measure string length in bytes vs characters

    master

    In Rust, strings are encoded to use the least amount of memory needed for each character.

    • .len(): Returns the length of the string in bytes.
    • .chars().count(): Returns the length of the string in characters.

    Because different characters (like emojis or non-Latin scripts) use different numbers of bytes, .len() may return a value larger than the actual number of characters.

    fn main() {
        let slice = "Hello!";
        println!("Slice is {} bytes and also {} characters.", slice.len(), slice.chars().count());
    
        let slice2 = "안녕!"; // Korean for "hi"
        println!("Slice2 is {} bytes but only {} characters.", slice2.len(), slice2.chars().count());
    }
  7. Use uninitialized variables for scoped values

    master

    An uninitialized variable is declared using let without an immediate value. While you cannot use an uninitialized variable before it is assigned, they are useful when a value is calculated inside a specific code block but needs to persist outside that block.

    Note that a variable does not need to be marked mut if it is declared without a value and then assigned exactly once.

    fn main() {
        let my_number;
    
        {
            let number = 57;
            my_number = number;
        }
    
        println!("{}", my_number);
    }
  8. Chain methods using functional style

    master

    Rust supports both imperative (separate commands on separate lines) and functional styles. Functional style allows you to chain multiple methods together in a single statement, often making code more concise. For better readability, you can place each method on a new line.

    Commonly used methods for chaining include:

    • .into_iter(): Creates an iterator that takes ownership of the items (gives owned values, not references).
    • .skip(n): Skips the first n items.
    • .take(n): Takes the next n items.
    • .collect::<Type>(): Transforms the iterator back into a collection (e.g., Vec<T>). You must specify the target type using the turbofish syntax ::<Type> if it cannot be inferred.
    let my_vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    
    let new_vec = my_vec
        .into_iter() // iterate over the items
        .skip(3)     // skip over three items: 0, 1, and 2
        .take(4)     // take the next four: 3, 4, 5, and 6
        .collect::<Vec<i32>>(); // put them in a new Vec<i32>
  9. Use raw strings and raw identifiers with `r#`

    master

    In Rust, you can use r# to create raw strings, which ignore escape characters like \n or \t. This is useful for paths or strings containing many quotes.

    • To handle single quotes or double quotes, use r#"..."#.
    • If your string contains # characters, increase the number of hashes on both sides (e.g., r##"..."## or r####"..."####) to match the highest count of # in your text.

    Additionally, r# allows you to use Rust keywords (like let, mut, or return) as variable names or function names.

  10. Use type inference and explicit type annotations in Rust

    master

    Rust uses type inference, meaning the compiler can often guess the type of a variable (e.g., let x = 8; defaults to i32).

    You can explicitly specify a type by adding a colon after the variable name:

    let small_number: u8 = 10;

    You can also specify the type directly after a numeric literal:

    let small_number = 10u8;

    To improve readability for large numbers, use underscores (_). They do not change the value:

    let big_number = 100_000_000_i32;
    let easy_read = 1_624_i32;
    fn main() {
        let small_number: u8 = 10;
    }
  11. Implement the Default trait

    master

    The Default trait allows you to provide a standard set of initial values for a struct or enum. This is useful when most instances of a type will share common properties. Most primitive types in Rust (like 0, "", or false) already implement Default.

    fn main() {
        let default_i8: i8 = Default::default();
        let default_str: String = Default::default();
        let default_bool: bool = Default::default();
    
        println!("'{}', '{}', '{}'", default_i8, default_str, default_bool);
    }
  12. Implement and use Traits in Rust

    master

    Traits define shared behavior for types. You can implement them manually using impl or automatically for common traits like Debug, Copy, and Clone using the #[derive(...)] attribute.

    When defining a trait, you can provide default method implementations or just the function signatures. If you only provide the signature, the implementing type must provide the logic. When overriding a trait method, you must match the original signature (parameters and return types) exactly.

    #[derive(Debug)]
    struct MyStruct {
        number: usize,
    }
    
    trait Dog {
        fn bark(&self);
        fn run(&self);
    }
    
    struct Animal {
        name: String,
    }
    
    impl Dog for Animal {
        fn bark(&self) {
            println!("{}, stop barking!!", self.name);
        }
        fn run(&self) {
            println!("{} is running!", self.name);
        }
    }
    
    fn main() {
        let rover = Animal {
            name: "Rover".to_string(),
        };
        rover.bark();
        rover.run();
    }