A struct, or structure, is a custom data type that lets you package together and name multiple related values that make up a meaningful group.
You have already seen them when studying C, but Rust's structs are much more powerful.
#include <stdint.h>
#include <stdio.h>
struct Color {
uint8_t r;
uint8_t g;
uint8_t b;
};
typedef struct Point {
int x;
int y;
} Point_t;
int main(void) {
struct Color color = {
.r = 230,
.g = 100,
.b = 10,
};
printf("%d %d %d\n", color.r, color.g, color.b);
struct Color black = {0};
printf("%d %d %d\n", black.r, black.g, black.b);
Point_t point;
point.x = 10; point.y = -10;
printf("%d %d\n", point.x, point.y);
return 0;
}
If there is already a variable with the same name of the field, you can avoid repeating yourself.
Do you find yourself wanting to copy some values from one struct to another?
The update syntax is for this situation. Set the fields you want to be different, in any order and then write ..[copied_struct_name]
.
#[derive(Debug)]
will automatically implement a function that println!("{:?}")
will call to print the values of your struct.
The function called by {:?}
can be generated automatically, but you can't do the same for {}
.
{var:?}
to use the debug function on var
, or {:#?}
to have nicer looking debug output.
It's common for structs, especially if they have many fields, to have a default that you can copy from, instead of writing a value for all fields.
Default::default()
is a function that returns a default value for a type.
We can decide to implement the function ourselves (how to do it will be explained on day 4), or tell Rust to call Default::default()
automagically
on all fields recursively with #[derive(Default)]
.
For std library types like numbers, String
, Vec
, etc. the function is implemented by the std library. But if the type doesn't have a Default::default()
, derive will fail.
The same as normal tuples, but it has a specific name instead of having to specify the types each time.
Vec
is an owned type, which means that the struct owns completely all the data in the scores
fields.
Meanwhile the slice type is borrowed, which means that the compiler needs some kind of assurance that the scores, and whoever owns them, will exist at least as long as the struct.
Remember lifetimes?Structs own all the memory of their fields, and therefore borrowing a field from a struct is (in terms of borrow checking not of actual assembly) the same as borrowing all the struct.
#include <stdio.h>
struct Rectangle {
float width;
float height;
};
struct Rectangle rectangle_new(float width, float height) {
struct Rectangle rect = {.width = width, .height = height};
return rect;
}
float rectangle_area(struct Rectangle *self) {
return self->height * self->width;
}
int main(void) {
struct Rectangle rect = rectangle_new(10.0, 30.0);
printf("%f\n", rectangle_area(&rect));
return 0;
}
impl
blockIt's better to define functions related to a struct inside an impl
block. This way they will be easier to find and use.
Inside the impl
block we can also use Self
, which is a shorthand that is replaced with the name of the type we are implenting the functions for, and self
which is an argument implicitly of type Self
.
The ::
is like a path to tell the compiler where to find a method, for example Rectangle::new()
tells it to look for the new()
function inside Rectangle
's impl
block.
This is called namespacing.
C doesn't have namespacing, which means that name collisions are extremely common for large projects.
rect.area()
is the same as Rectangle::area(&rect)
, the '.'
simply passes the variable as the self
argument of the function.
It can also pass the reference or dereference to the variable without adding '&'
or '*'
, simply by looking at the function signature.