Photo by Michele Blackwell on Unsplash

Rust — Structs, Functions and Methods

The basics of encapsulating data and behaviour in Rust

Gian Lorenzetto, PhD
8 min readOct 23, 2021

--

In my previous posts, I’ve covered —

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 type type
  • 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 and n2, both with the same type of i32.
  • 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 type SumArgs 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 the SumArgs 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 used create or make_a_new_one!
  • The new function returns Self, which is similar to this in other languages. The type name SumArgs could be used in place of Self, 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 and n2), via the self argument.
    Note that self is actually syntactic sugar for self: 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 call add_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 in args.

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!

--

--