Structs with methods and toString
Gathering related values into a type of your own, giving that type behaviour, teaching it to describe itself, and making a finished record immutable.
A struct is a named shape
struct Book { string title; int pages; bool read; } defines a new type. From that line on, Book sits beside int in a declaration, Book gathers three values under one name, and each of those values is a field reached with a dot: book.pages.
A struct is a value, so assigning one to another copies every field. That is what makes a list of records simple: [Book("Dune", 412, false)] is one array of one type, a function takes one parameter, and adding a field later means changing the type in one place rather than every parallel array that mirrored it.
Methods and toString
Functions declared inside the struct are its methods, and they are called with a dot: book.finish(). A method that changes the record can assign to the fields directly, because it already holds the record it was called on. A method that only reads marks itself const after the parentheses, which promises the compiler that the record will not change.
toString is the method the printing functions look for. Define string toString() const and writeln(book) prints the text it returns instead of a generic description. It is the cheapest way to make a custom type readable in every later diagnostic and report.
toString is also what to!string calls, and what a %s in a format string uses, so one method covers every way the record is turned into text. If the method is missing, the default prints the type name and the field values in a form that is useful for debugging but not for a report.
immutable structs
A struct value can be declared immutable, and then no field can be written after it is built: immutable origin = Book("Emma", 474, true);. The compiler rejects origin.read = false, which is exactly the check you want on a record that represents a finished fact.
This is where a struct differs from a class, which the next chapter introduces. A class variable is a reference: copying it shares one object, and the object can be changed through any of its names. A struct variable is the value itself, copied on assignment, which is why the course uses structs for records.
An immutable struct is not just a variable that cannot be reassigned. Every field is fixed, and a slice or a reference inside the struct inherits the same promise, which is why an immutable record can be shared between threads without a lock.
What the value looks like
A struct's fields sit next to each other in the order they are declared, with padding inserted so each field starts at an address its type can use. int a; int b; packs into eight bytes; byte a; int b; spends three bytes of padding between them. Point.sizeof reports the whole footprint, padding included, which is why field order can change the size of a type.
Assignment copies that block of bytes. Passing a struct to a function copies it unless the parameter is ref, and returning one copies it back. For a small record that is cheap and completely safe; for a large one, ref or const ref avoids the copy without changing the meaning.
Constructors and const methods
A struct can declare a constructor named this, called by Point(3, 4) just like the positional literal. The constructor runs once per value and is the right place to check that the fields make sense. A method marked const after its parameter list promises not to write to the record, which is what lets an immutable value call it at all.
The compiler checks that promise in the body rather than taking your word. A const method that assigns a field is refused, so a record that was built correctly stays readable and consistent for as long as it exists.
An immutable struct with a const method
import std.stdio;
struct Point {
int x;
int y;
int total() const {
return x + y;
}
}
void main() {
immutable origin = Point(2, 3);
writeln(origin.total());
}A book that finishes itself
Run it and watch read change after finish. Then add a field for the author, fill it in the literal, include it in toString, and print the book again.
Book("Dune", 412, false) is a struct literal: the fields in declaration order. When a record grows past four fields the order stops being obvious, and a small factory function with named steps reads better than a long literal.
A point that can build and describe itself
Run it, then change the point and the scale factor. Add a `translate` method that returns a new point moved by one on each axis, and print the result beside the original.
Three mistakes: writing x = x; in a constructor, which assigns the parameter to itself instead of this.x = x;; marking a method const while it writes a field, which the compiler refuses; and inserting a field in the middle of a struct whose values are built with positional literals, which silently changes what every literal means.
Try these next
- Add a
translatemethod that returns a new point moved by one on each axis. - Add a
boolfield and printPoint.sizeofbefore and after, then explain the change. - Build two points, assign one to the other, change the copy, and print both.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What happens when one struct variable is assigned to another?
