DEV Community

Cover image for 30 Days of Rust - Day 17
johnnylarner
johnnylarner

Posted on

30 Days of Rust - Day 17

Hello folks,
I do apologise for missing the last couple of days. I had a lot of work to finish up before taking some PTO and I couldn't quite manage the cognitive load of the blog. But now I'm back and ready to finish the 13 of the 30 days. Today we'll be diving into structs in more detail. I briefly introduced this topic back on day 4.

Yesterday's questions answered

  • Yes, Rust doesn't treat slice references any differently

Today's open questions

  • How many pointer can Rust automatically dereference?

Con- and destructuring structs

The way we instantiate structs in Rust is pretty typical. Take a simple user struct:

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}
Enter fullscreen mode Exit fullscreen mode

The most basic way to instantiate a user struct would be to pass values mapped to its fields :

let inactive_johnny = User {
    active: false,
    username: String::from("johnnylarner"),
    email: String::from("johnnylarner@gmail.com"),
    sign_in_count: 100,
};
Enter fullscreen mode Exit fullscreen mode

Now sometimes we make have default values for particular fields and thus often may want to parameterise only a subset of fields in a function:

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        email,
        username,
        sign_in_count: 1,
    }
}
Enter fullscreen mode Exit fullscreen mode

Now Rust provides developers with a so-called init shorthand syntax. Similar to object deconstruction in JavaScript, when we have an in-scope variable that matches a declared field name of a struct, we don't need to use the map syntax. Hence in the above function we are able to simple pass the email and username arguments as is.

Rust has also incorporated another JavaScript-esque feature. Take a look and see if you can see what I mean:

let activated_johnny = User {
    active: true,
    ..inactive_johnny
}
Enter fullscreen mode Exit fullscreen mode

Give yourself a pat on the back if you could see JavaScripts ... spread operator here. Or perhaps the Pythonistas see a ** key value unpack operator here. These operators will make shallow copies of the values that are yielded by the operator.

The update syntax in Rust allows us to move values from one instance of a struct to another. What does it moving mean again? It means we make the new variable activated_johnny the owner of the data previously owned by inactive_johnny. Thus, now trying to reference inactive_johnny will cause your code to not compile. One side note here, if all values were actually primitives stored on the stack, then you would actually by copying rather than moving the data. Move semantics here explain why we use the verb update rather than copy to describe this syntax.

Finally, I should add that structs are not required to have any fields:

struct AlwaysEqual;

let always_eq = AlwaysEqual; // This compiles.
Enter fullscreen mode Exit fullscreen mode

What should structs own?

For young Rustaceans like myself ownership can be relatively easy to comprehend on a component level. But advanced and optimised system design Rust must of course include a sophisticated ownership designs, where serious thought is given to the lifetime of data on the heap and which parts of the system should read and write that data.

In the context of structs I'm going to accept that the naΓ―ve solution for now is simply to say:

A struct should own its own data.

Later in the blog series, we'll challenge this hypothesis.

Named tuples??

Experienced Pythonistas should know about a lightweight alternative to classes: namedtuples. Now Rust has something similar, we'll at least at first glance. tuple structs are a quick way to group related data. They are less verbose than a struct and may often suffice if data we are trying to represent is conceptually concise. Now this comparison with Python is actually quite misleading as tuple structs do not contain any field names:

struct Point(i32, i32, i32);
struct Color(i32, i32, i32);
Enter fullscreen mode Exit fullscreen mode

Note how we wrap the declaration in parentheses (not curlies) and end it with a semi-colon.

Viewing your beautiful structs

Must like in Python, printing your struct to the console will not really help you see what's stored in your struct. In Python, you'll get a memory address. In Rust your code just won't compile.

The quick fix to this in Rust is to have your struct derive the Debug trait:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}
Enter fullscreen mode Exit fullscreen mode

With your struct declaration fixed up, you now have 3 options on how to view it:

let rect = Rectangle {width: 10, height: 100 }

println!("My ugly rectangle: {:?}", rect)
println!("My pretty rectangle: {:#?}", rect)
dbg!("My ugly rectangle: {:?}", rect)

Enter fullscreen mode Exit fullscreen mode

I'd suggest testing out the code locally to see the results. But summarised:

  • :? will give you an inline view of the struct
  • :#? will give each field on a new line and indented correctly
  • dbg! the debug macro takes (and returns) ownership of your struc and pretty prints it along with its line number in the source file.

Methods and associated functions

We can declare methods for strucs by using the impl keyword. Methods in the impl block are declared like functions: using the fn keyword. In theory, you can use an arbitrary number of impl blocks to declare methods (and traits). But typically you'll only want to use one impl block for all methods.

Methods borrow the struct's data using the &self as the first parameter. This are called by calling the . operator on an instance of the struct. The &self type also indicates to us that a method is read-only. If we want to edit the underlying data of a struct, we need the &mut self operator instead.

We've already seen how creating structs usually takes place via an associated function. I've definitely mistakenly called these static methods which is incorrect:

let my_string = String::from("My string");
Enter fullscreen mode Exit fullscreen mode

Here from is an associated function whose return type is Self, a reference to the type implementing the method.

Calling methods on pointers to structs

There may be cases when you actually have a pointer to a struct. In C-based languages, there is a specific operator to call methods on pointers: ->. If you call a method on a pointer to a struct, Rust will automatically follow the pointer and call the method on the struct. This is known as automatic dereferencing.

Top comments (0)