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,
}
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,
};
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,
}
}
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
}
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.
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);
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,
}
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)
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 yourstruc
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");
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)