Structs in Rust With Fields Methods and Associated Functions
Define a struct, give it methods in an impl block, add associated functions like new, and see tuple and unit structs plus update syntax and derive.

A struct is how you give a name and a shape to data that belongs together, and an impl block is how you attach behaviour to it. Rust has no classes and no inheritance, so these two pieces carry nearly everything that classes do in other languages. This post builds a struct with the three kinds of method, shows the four errors you will meet in your first week, and ends with a fact about memory layout that surprises people from C. All programs ran on Rust 1.99, edition 2024.
Fields, methods and an associated function
#[derive(Debug, Clone)]
struct User {
name: String,
email: String,
logins: u32,
active: bool,
}
impl User {
// Associated function: no self, called as User::new(..)
fn new(name: &str, email: &str) -> User {
User { name: name.to_string(), email: email.to_string(), logins: 0, active: true }
}
// &self: read-only method
fn display(&self) -> String {
format!("{} <{}>", self.name, self.email)
}
// &mut self: changes the value
fn record_login(&mut self) {
self.logins += 1;
}
// self: consumes the value
fn deactivate(mut self) -> User {
self.active = false;
self
}
}
fn main() {
let mut ada = User::new("Ada", "[email protected]");
ada.record_login();
ada.record_login();
println!("{} has {} logins", ada.display(), ada.logins);
let copy = User { name: "Ada II".to_string(), ..ada.clone() }; // struct update syntax
println!("{copy:?}");
let gone = ada.deactivate();
println!("{} active={}", gone.display(), gone.active);
}Ada <[email protected]> has 2 logins
User { name: "Ada II", email: "[email protected]", logins: 2, active: true }
Ada <[email protected]> active=falseUser has four named fields. The impl User block holds everything that belongs to it, and the first parameter of each function says what the function does with the value.
newhas noselfat all. It is an associated function, called with the type name:User::new(...). The namenewis a convention, not a keyword, and a constructor in Rust is just a function that returns the struct.display(&self)borrows the value read-only. You can call it any number of times and the value is untouched.record_login(&mut self)borrows it mutably, so it can changeself.logins. The caller needs alet mutbinding for this to be allowed.deactivate(mut self)takes the value by ownership. After the call the original is gone, and the method hands a changed value back.
The line with ..ada.clone() is struct update syntax: the fields you list are set, and the rest are copied or moved from the other value. Here the new user keeps Ada's email and her two logins but has a new name. Without the .clone() the update would move ada.email out of ada, which would partially consume it.
The method receiver is the contract
The three receivers matter because the compiler enforces them at the call site. Take ownership and then use the value again, and you get one of the most common errors in the language:
#[derive(Debug)]
struct User {
name: String,
}
impl User {
fn deactivate(self) -> User {
self
}
}
fn main() {
let ada = User { name: "Ada".to_string() };
let gone = ada.deactivate();
println!("{:?} {:?}", ada, gone);
}error[E0382]: borrow of moved value: `ada`
--> src/main.rs:15:27
|
13 | let ada = User { name: "Ada".to_string() };
| --- move occurs because `ada` has type `User`, which does not implement the `Copy` trait
14 | let gone = ada.deactivate();
| ------------ `ada` moved due to this method call
15 | println!("{:?} {:?}", ada, gone);
| ^^^ value borrowed here after move
|
note: `User::deactivate` takes ownership of the receiver `self`, which moves `ada`
--> src/main.rs:7:19E0382 says ada was moved by deactivate, and the note even names the cause: the method takes ownership of the receiver self. If you meant to keep using ada, the method should take &self or &mut self and change the value in place, or you should clone before calling. If you did mean to consume it, the error is correct, because ada no longer exists. Borrow of moved value explains the family, and ownership explains why.
A &mut self method needs a mutable binding:
struct Counter {
n: u32,
}
impl Counter {
fn bump(&mut self) {
self.n += 1;
}
}
fn main() {
let c = Counter { n: 0 };
c.bump();
println!("{}", c.n);
}error[E0596]: cannot borrow `c` as mutable, as it is not declared as mutable
--> src/main.rs:13:5
|
13 | c.bump();
| ^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
12 | let mut c = Counter { n: 0 };The fix is the one the compiler prints: let mut c. The rule is the same as for any mutation, covered in variables and mutability, and the receiver type is what tells the compiler that bump mutates.
Two construction errors
A struct literal must name every field, and a method must exist:
struct Config {
host: String,
port: u16,
}
fn main() {
let c = Config { host: "localhost".to_string() };
println!("{}", c.host);
}error[E0063]: missing field `port` in initializer of `Config`
--> src/main.rs:7:13
|
7 | let c = Config { host: "localhost".to_string() };
| ^^^^^^ missing `port`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0063`.E0063, missing field port. There are no default values unless you provide them, which is why structs with many optional fields usually offer a builder or implement Default, and then use ..Default::default() in the literal. See builder pattern variants for the options.
struct Point {
x: f64,
y: f64,
}
fn main() {
let p = Point { x: 3.0, y: 4.0 };
println!("{}", p.distance());
}error[E0599]: no method named `distance` found for struct `Point` in the current scope
--> src/main.rs:8:22
|
1 | struct Point {
| ------------ method `distance` not found for this struct
...
8 | println!("{}", p.distance());
| ^^^^^^^^ method not found in `Point`E0599, no method named distance. Methods do not appear because a field is named x and y; you write them. The compiler also does not guess that you meant a function from somewhere else. If the method is defined in a trait, the trait must be in scope as well, which is the other common cause of this error.
Tuple structs and unit structs
Structs do not have to name their fields:
// Tuple struct: a distinct type with unnamed fields (the newtype pattern)
struct Meters(f64);
struct Seconds(f64);
// Unit struct: no data, useful as a marker or a place to hang trait impls
struct Marker;
impl Marker {
fn hello(&self) -> &'static str {
"marker says hi"
}
}
fn speed(distance: Meters, time: Seconds) -> f64 {
distance.0 / time.0
}
fn main() {
println!("{}", speed(Meters(100.0), Seconds(9.58)));
println!("{}", Marker.hello());
println!("{} bytes", std::mem::size_of::<Marker>());
}10.438413361169102
marker says hi
0 bytesMeters(f64) is a tuple struct: its field is reached as .0. The value is just an f64, but the type is Meters, and the compiler treats Meters and Seconds as different types even though both wrap the same number. That is the newtype pattern, and its payoff is concrete:
struct Meters(f64);
struct Seconds(f64);
fn speed(distance: Meters, time: Seconds) -> f64 {
distance.0 / time.0
}
fn main() {
println!("{}", speed(Seconds(9.58), Meters(100.0)));
}error[E0308]: arguments to this function are incorrect
--> src/main.rs:9:20
|
9 | println!("{}", speed(Seconds(9.58), Meters(100.0)));
| ^^^^^ ------------- ------------- expected `Seconds`, found `Meters`
| |
| expected `Meters`, found `Seconds`Swapping the two arguments is a compile error, and the compiler suggests swapping them back. A function signature of fn speed(distance: f64, time: f64) would have accepted the swap and returned a wrong number. The newtype pattern goes into the cost (none) and the trade-offs. Marker is a unit struct: no fields, zero bytes, as the last line shows. It exists to carry methods or trait implementations without any data, and to act as a tag in the type system.
Rust may reorder your fields
In C, a struct is laid out in the order you declared it, with padding inserted to align each field. Rust makes no such promise by default, and uses the freedom:
use std::mem::{align_of, size_of};
#[allow(dead_code)]
struct Padded { a: u8, b: u32, c: u8 }
#[allow(dead_code)]
struct Reordered { b: u32, a: u8, c: u8 }
#[allow(dead_code)]
#[repr(C)]
struct CLayout { a: u8, b: u32, c: u8 }
fn main() {
println!("Padded size={} align={}", size_of::<Padded>(), align_of::<Padded>());
println!("Reordered size={} align={}", size_of::<Reordered>(), align_of::<Reordered>());
println!("repr(C) size={} align={}", size_of::<CLayout>(), align_of::<CLayout>());
}Padded size=8 align=4
Reordered size=8 align=4
repr(C) size=12 align=4All three structs hold a u8, a u32 and another u8: six bytes of data. Under repr(C), which asks for the C layout, the fields stay in order, the u32 needs 4-byte alignment, so three bytes of padding follow the first u8, and another three pad the end, for 12 bytes. In Rust's default representation, the compiler is free to place the u32 first and the two bytes after it, and it did: 8 bytes, with two bytes of padding at the end. The order you write fields in does not matter for size, but if you pass a struct to C code, write it to a file as raw bytes or share it between processes, you must say #[repr(C)], because the default layout is unspecified and can change between compiler versions. The padding rules are in alignment and padding.
Visibility and where the struct lives
Fields are private to the module that defines the struct unless marked pub. That is how a struct enforces its invariants: a Temperature that must stay above absolute zero can keep its field private and offer a constructor that checks the value. The rules are in modules and visibility.
Habits
- Give a struct private fields and a
newthat validates them when it has invariants to protect. - Take
&selfby default,&mut selfwhen the method changes the value andselfonly when it should consume it. - Derive
Debugon every struct; addClone,PartialEqandDefaultwhen they are useful. - Use a newtype whenever two values of the same primitive type could be confused.
- Do not add a getter and a setter for every field. If a field is public and has no invariant, access it directly.
When a struct is the wrong shape
If a value can be one of several different shapes, with different fields in each, use an enum, not a struct with many Option fields; enums shows why. If behaviour varies and you want to call it through a common interface, define a trait and implement it for several structs. And if you reach for inheritance to share fields between two structs, compose instead: put the shared struct inside both as a field and forward the few methods you need.


