Struct Method Syntax
Move supports defining methods on structs using a receiver-style syntax. This allows you to call functions using dot notation on struct values, making code more readable and object-oriented in style.
Defining Methods
A method is a function whose first parameter is a reference to the struct type. The method can then be called using dot notation.
module my_addr::counter {
struct Counter has key {
value: u64,
}
/// Read the current value -- called as counter.value()
public fun value(self: &Counter): u64 {
self.value
}
/// Increment the counter -- called as counter.increment()
public fun increment(self: &mut Counter) {
self.value = self.value + 1;
}
}
Calling Methods
Once defined, methods can be called with dot notation:
fun example() acquires Counter {
let counter_ref = &Counter[@0x1];
let val = counter_ref.value(); // Method call
let counter_mut = &mut Counter[@0x1];
counter_mut.increment(); // Mutable method call
}
Receiver Types
Methods can take three types of receivers:
Immutable Reference (&Self)
Used for read-only operations:
public fun is_empty(self: &Counter): bool {
self.value == 0
}
Mutable Reference (&mut Self)
Used for operations that modify the struct:
public fun reset(self: &mut Counter) {
self.value = 0;
}
By Value (Self)
Used for operations that consume the struct:
public fun destroy(self: Counter): u64 {
let Counter { value } = self;
value
}
Practical Example
Here is a more complete example showing methods in a token module:
module my_addr::simple_token {
use std::string::String;
struct Token has store {
name: String,
balance: u64,
}
/// Create a new token
public fun new(name: String, initial_balance: u64): Token {
Token { name, balance: initial_balance }
}
/// Get the token name
public fun name(self: &Token): &String {
&self.name
}
/// Get the current balance
public fun balance(self: &Token): u64 {
self.balance
}
/// Check if the token has any balance
public fun has_balance(self: &Token): bool {
self.balance > 0
}
/// Add to the balance
public fun deposit(self: &mut Token, amount: u64) {
self.balance = self.balance + amount;
}
/// Subtract from the balance
public fun withdraw(self: &mut Token, amount: u64): u64 {
assert!(self.balance >= amount, 1);
self.balance = self.balance - amount;
amount
}
/// Consume the token and return the balance
public fun redeem(self: Token): u64 {
let Token { name: _, balance } = self;
balance
}
}
Usage with dot notation:
fun example() {
let mut token = simple_token::new(
std::string::utf8(b"MyToken"),
1000,
);
let name = token.name();
let bal = token.balance();
assert!(token.has_balance());
token.deposit(500);
let withdrawn = token.withdraw(200);
let remaining = token.redeem();
}
Method Resolution
When you write x.method(), the compiler looks for a function named method in the module where the type of x is defined. The first parameter of that function must be a reference to (or value of) the type of x.
The compiler automatically inserts the appropriate borrow. For example:
let counter = Counter { value: 0 };
counter.value() // Compiler inserts &counter as the first argument
Best Practices
- Use methods for type-specific operations: Any function that logically belongs to a struct should be defined as a method.
- Prefer
&selfover&mut self: Use the least permissive receiver type. - Name methods clearly: Use verbs for actions (
deposit,withdraw) and nouns for accessors (balance,name). - Keep methods focused: Each method should do one thing well.