Rust — Structs, Functions and Methods
The basics of encapsulating data and behaviour in Rust
In my previous posts, I’ve covered —
- The basics of the Rust toolchain,
- The Rust package management system, and
- The default Rust project structure and Rust modules.
Now it’s time to get into the actual syntax of the language itself.
The Rust encapsulation mechanism is actually very straight forward. Functions encapsulate behaviour, structs encapsulate data. That’s it!
“What about classes?” I hear you say? Well, Rust doesn’t have classes as such. But you can associate functions with structs, create tuple structs, and create methods giving functions access to struct internals.
But first, let’s start with the basic building block — functions!
Functions
Functions in rust look much like any other language — go ahead and create a new Rust project (with cargo new project_name
) and open the default main.rs —
fn main() {
println!(“Hello, world!”);
}
The main
function is the entry point for our application (recall that by default cargo will create a binary application for us). It’s looks a lot like any main function in C# (or even C), but this is where things deviate a little. You can create a function pretty much anywhere you like — no class needed (as in C# for example).
Let’s go ahead and create a new function called my_print
—
fn my_print() {
println!("Hello, world!");
}fn main() {
my_print();
}
Not super exciting, but let’s quickly go over what happened here —
- We declared a new function with the
fn
keyword giving it a name (with the Rust idiomatic snake casing). - The function scope is the main module (ie, at the root of project).
- The function takes no arguments and does not return a value.
- We called our new function from the
main
function.
Adding arguments and a return value should look very familiar —
fn add_numbers(n1: i32, n2: i32) -> i32 {
n1 + n2 <-- No need for `;`
}fn main() {
let n1 = 2;
let n2 = 3;
let sum = add_numbers(n1, n2);
println!("{} + {} = {}", n1, n2, sum);
}
- Rust function arguments look like
var_nam: {type}
- The
-> {type}
syntax tells Rust this function returns a value of typetype
- Rust does not require a
;
after expressions, hence there is no;
on the final expression in add_numbers. You can convert this to a statement like so —return n1 + n2;
But this is not idiomatic Rust and should be avoided.
Lastly, the call to the println!
macro illustrates its support for string interpolation. Note that the compiler will helpfully raise an error if the number of values to interpolate does not match the given number of parameters.
With that out of the way, let’s now jump over to structs!
Structs
Structs in Rust initially look very similar to many other languages. Let’s extend our app above a little to store the values to add. At the top of the main.rs add the following —
struct SumArgs {
n1: i32,
n2: i32, <-- Idiomatic Rust adds trailing ','
}
Again, this is quite straight-forward, but let’s go over what happened —
- We declared a new struct called SumArgs with the
struct
keyword. Note that idiomatic Rust for struct names is Pascal case. - Two fields have been declared on the struct,
n1
andn2
, both with the same type ofi32
. - Idiomatic Rust is to add the trailing
,
on the last line of a list of values. This just makes refactoring a little cleaner if you where to say, reorder the declarations.
Let’s put our struct to use and pass it to the add_numbers
function —
fn add_numbers(args: &SumArgs) -> i32 { <-- The '&' denotes a
args.n1 + args.n2 borrow
}
No surprises here —
- The argument list is now just the single
args
parameter with type&SumArgs
. - The
&
prefixing the typeSumArgs
is there is signify that we are borrowing the value. That is, we are not taking ownership, nor are we intending to modify the value. Borrowing is a complex topic for another article, but needless to say, in general you will pass borrowed values much more often than not. You can read more about references and ownership in the Rust book if you are keen! - The struct fields are accessed via the
.
syntax.
Lastly let’s update the main
function to use our new struct —
fn main() {
let args = SumArgs { n1: 2, n2: 3 };
let sum = add_numbers(&args);
println!("{} + {} = {}", args.n1, args.n2, sum);
}
We’ve replaced the individual initialisations of n1
and n2
with a single initialisation of the SumArgs struct. Note that there is no constructor or other mechanism for initialising the structs fields as yet.
Tuple Structs
For situations where you don’t necessarily care about the field names, Rust supports so called tuple structs. We can convert our simple SumArgs struct above to a tuple struct like so —
struct SumArgs(i32, i32);
The obvious difference is that we have specified the types of our fields directly on the struct declaration with (i32,i32)
syntax. Let’s look at what the rest of our program looks like now —
fn add_numbers(args: &SumArgs) -> i32 {
args.0 + args.1 <-- tuple struct access
}fn main() {
let args = SumArgs(2, 3); <-- tuple struct init
let sum = add_numbers(&args);
println!("{} + {} = {}", args.0, args.1, sum);
}
The initialisation of the tuple struct is much simpler, but what we gain in simplicity we lose in readability when accessing the tuple struct fields. Tuple structs are accessed via their position in the struct initialisation. That is, .0
and .1
in this case.
We can use Rust’s destructuring syntax to assign more meaningful names to the fields, as seen here in this minor change to the add_numbers
function —
fn add_numbers(args: &SumArgs) -> i32 {
match args {
SumArgs(n1, n2) => n1 + n2,
}
}
I haven’t touched on Rust’s pattern matching yet, but in the above we are matching on the value of args
, in this case to a pattern consisting of the type SumArgs
, whose fields are assigned to the variables n1
and n2
. We can then use n1
and n2
as we did before.
In this case, where not gaining anything with the tuple struct syntax, so for the rest of this article I’ll return to the original struct definition. But tuple structs are a nice shorthand when it’s suitable. Note that a tuple struct is actually just like any other struct in most other ways. That is, you can associate functions, which is exactly what we’ll look at next.
Associated Functions
Unlike languages like C#, Java or even C+, where methods are defined directly within the class declaration, Rust has a slightly different syntax. Let’s take a look back at our simple program, before the change to using tuple structs —
struct SumArgs {
n1: i32,
n2: i32,
}fn add_numbers(args: &SumArgs) -> i32 {
args.n1 + args.n2
}fn main() {
let args = SumArgs { n1: 2, n2: 3 };
let sum = add_numbers(&args);
println!("{} + {} = {}", args.n1, args.n2, sum);
}
Let’s start by moving the add_numbers
function onto the SumArgs
struct. Given the tight dependence between the values and the function itself, this seems to make sense —
impl SumArgs {
fn add_numbers(args: &SumArgs) -> i32 {
args.n1 + args.n2
}
}fn main() {
let args = SumArgs { n1: 2, n2: 3 };
let sum = SumArgs::add_numbers(&args);
println!("{} + {} = {}", args.n1, args.n2, sum);
}
The most interesting thing here is that we didn’t touch the struct definition itself. Breaking it down —
- There is a new
impl SumArgs
block which we use to define all functions that we want to be associated with theSumArgs
struct. - In order to access an associated function you must use the
::
syntax.
This is similar to declaring a static or class method in other languages. It is scoped to the struct type, but it has no other connection. That is, we can’t access the fields, so we’re still passing the args
object to the function. We haven’t gained very much at all, but we can do a lot better than this!
Constructors
First off, let’s see how we can create a constructor to get back to the simplicity of the tuple struct initialisation —
impl SumArgs {
fn new(n1: i32, n2: i32) -> Self { <-- Note the use of 'Self'
Self { n1, n2 } ... and here
}
...
}fn main() {
let args = SumArgs::new(2, 3);
let sum = SumArgs::add_numbers(&args);
println!("{} + {} = {}", args.n1, args.n2, sum);
}
Let’s break this down —
- There is a function called
new
associated with out struct. There is nothing special about the name itself, we could have usedcreate
ormake_a_new_one
! - The new function returns
Self
, which is similar tothis
in other languages. The type nameSumArgs
could be used in place ofSelf
, but this is a common pattern and nice syntactic sugar. - To create a new
SumArgs
object we call the associated function, passing through to the two parameters.
This is very similar to a static factory pattern for creating objects, but because of the syntax of associating functions with structs, this pattern is much more prevalent in Rust.
Methods
Thus far we’ve seen stand-alone functions and associated functions. There is no more type of function, referred to as a method in Rust. A method is an associated function (ie, declared against a struct) that takes a special parameter self
as the first argument.
Let’s see what our add_numbers
function looks like if we convert it to a method —
impl SumArgs {
fn new(n1: i32, n2: i32) -> Self {
Self { n1, n2 }
} fn add_numbers(&self) -> i32 { <-- Note the '&self'
self.n1 + self.n2
}
}fn main() {
let args = SumArgs::new(2, 3);
let sum = args.add_numbers();
println!("{} + {} = {}", args.n1, args.n2, sum);
}
Breaking this down —
- The
add_numbers
method takes the special parameter&self
, which gives us access to the structs fields (ie,n1
andn2
), via theself
argument.
Note thatself
is actually syntactic sugar forself: Self
. Also note that here we’re borrowing self because of the&
prefixing the type. - Calling a method requires an instance of the type. Since we previously created the
args
object, we can use the method access syntax.
to calladd_numbers
. - Lastly, although the method declaration takes one argument (
self
), Rust knows that we mean the instance on which the method was called, so there is no need to explicitly pass inargs
.
And there we have it!
Conclusion
Rust’s structs, functions and methods are very similar to many other languages, but the syntax and usage can be a bit foreign at first. For a thorough deep dive, Rust by Example has an excellent section on the subject of functions and methods. I’d encourage you go take a look as the while the above will get you started, there is plenty more to learn!