Structs in Rust
Tags: rust
Published: 2022-05-03 15:55:34


Structs are one of the most useful features that Rust provides. They give programmers the ability to model complex data structures. And, they also enable programmers to abstract objects involved in a problem. In this article, we'll go through their basics and see how we can improve our codes with them.


## Prerequisites

Before you fully understand this article it will be nice to have the following.
* You should have a basic understanding of Rust
* You should also have Rust version 1.0 upwards installed


## What is a Struct?

This is a keyword used in Rust for building data structures. These structures are used to group related data together. Being able to hold related data can make development speed and runtime of your code much faster. Aside from improving those, they also help in debugging and in understanding how a piece of data interacts with other pieces.

Data structures are the core of software development. They help in organizing data within our code. With the right data structures, our code can easily either take up less memory space, less runtime or both. This is especially useful when trying to create scalable systems that serve millions of users at once.


## Types

There are three different types of structs in rust, they are field structs, tuple structs and unit structs.


### Field Structs

In this type of structs, each field has a name and the type of data they hold. This is like the regular structs you see in C. In Rust, the way we define them is by using the **struct** keyword. For example, we will be creating a **Person** struct. This struct has the properties **name** and **age**.


```rust
struct Person {
    name: String,
    age: u8,
}
```


In this snippet, the properties are called _fields_. The fields of these structs are defined in a pair of curly braces "{ }" and separated by a comma.

After we have defined the struct, we can then initialize it. Initialization is the process of creating an instance of a struct. The struct is like a blueprint, and the instance is the adaptation of the blueprint. These are similar to objects in object-oriented programming. Although it's similar, there are still some differences. Because Rust is not that type of language, some core features like Inheritance, construction and deconstruction are not supported.

To initialize the struct above we do the following.


```rust
let person = Person {
    name: "John".to_string(),
    age: 23,
};
```


In this example, we initialize the name of the person as "John" and set the age to 23. We can now access the fields in this struct. We get the name of the person using **person.name** and the age using **person.age**. 


```rust
println!("Name: {}\nAge: {}", person.name, person.age);
```


And we can have a full program like so.


```rust
struct Person {
    name: String,
    age: u8,
}

fn main() {
    let person = Person {
        name: "John".to_string(),
        age: 23,
    };

    println!("Name: {}\nAge: {}", person.name, person.age);
}
```


When we run this in the console, we get the following output.


```rust
Name: John
Age: 23
```



### Tuple Structs

These are structs that hold data in a tuple-like format. A tuple is a collection of data in series. The data stored can either be of similar or different data types. This structure generally gives a name to a tuple structure. Using the **person** example, we will be declaring the tuple struct of it.


```rust
struct Person (String, u8);
```


Now that we have created it, we can initialize it. The initialization process of this structure is similar to a normal tuple. The only difference is that we put the name of the struct before the parentheses.


```rust
let person = Person ("John".to_string(), 23);
```


We access the elements in this struct using the position they are in inside the tuple. This is the same with normal tuples. We access the person's name using **person.0** and the age using **person.1**. 


```rust
println!("Name: {}\nAge: {}", person.0, person.1);
```


Using this concept, we can create a simple program like so.


```rust
struct Person (String, u8);

fn main() {
    let person = Person ("John".to_string(), 23);

    println!("Name: {}\nAge: {}", person.0, person.1);
}
```



### Unit Structs

These types of structs do not hold any data. This makes their size zero bytes. It is useful when we want to group related functions together without necessarily holding any data. You will see how we can use them to do that later in this tutorial.

We declare them using the struct keyword like so.


```rust
struct Unit;
```


After declaring, we can now initialize it with the name of the struct below.


```rust
let unit_struct = Unit;
```



## Mutability of Structs

Aside from creating and using structs, Rust also gives us the ability to change and modify them. This is a useful feature if our struct will need to change while our code is running. 

A good example of this would be activating a user's account. The user's account is going to be a data structure. In this structure we will have a **name** and an **is_active** field.


```rust
struct User {
    name: String,
    is_active: bool,
}
```


We initialize it using the **mut** keyword. This keyword is short for _mutable_. Meaning we are making our variable mutable. Meaning that it is open to modifications. Without it, Rust will not allow the variable to change once it is initialized. 


```rust
let mut user = User {
    name: "John".to_string(),
    is_active: false,
};
```


After initializing this struct as mutable, we can then modify the **is_active** field.


```rust
user.is_active = true;
```


We can now have a simple code like this. 


```rust
struct User {
    name: String,
    is_active: bool,
}

fn main() {
    let mut user = User {
        name: "John".to_string(),
        is_active: false,
    };

    println!("{} is active: {}", user.name, user.is_active);

    // Perform some processes needed
    // to activate the user's account.
    // Then activate it
    user.is_active = true;
    println!("{} is active: {}", user.name, user.is_active);
}
```


When we run the above in our terminal, we get the output below.

```bash
John is active: false

John is active: true
```

## Passing and Returning From Functions

Structs can also be passed into functions and returned from them. This improves the level of abstraction we can make in our code. A higher level of abstraction can be really helpful in making your code easier to understand. By abstracting our code, we are grouping lower-level actions into a single high-level operation. For example, we will be creating a function called **create_person**. That function will create a new instance of the struct whenever it is called.


```rust
fn create_person(name: String, age: u8) -> Person {
    return Person {
        name: name,
        age: age,
    };
}
```


With this in our code, we will be able to create an instance by passing the name and the age to our function. Then, our function will return an instance of the **Person** struct. In Rust, there is a shortcut we can use to return data from functions. That is, to remove the semi-colon of the last expression. If we do this there, we will not need to use **return**.


```rust
fn create_person(name: String, age: u8) -> Person {
    Person {
        name: name,
        age: age,
    }
}
```


To pass a struct to a function,  we set the data type of the argument to the name of the struct. For example, we will create a **display** function that prints the struct to the terminal.


```rust
fn display(person: Person) {
    println!("Name: {}\nAge: {}", person.name, person.age);
}
```


In this example, what we did was to create an argument **person** with struct-type **Person**. This is where we will be passing our struct to. Then, we can access its elements. Now we can have a full program.


```rust
struct Person {
    name: String,
    age: u8,
}

fn main() {
    let person = create_person("John".to_string(), 23);
    display(person);
}

fn create_person(name: String, age: u8) -> Person {
    Person {
        name: name,
        age: age,
    }
}

fn display(person: Person) {
    println!("Name: {}\nAge: {}", person.name, person.age);
}
```



## Using impl

Sometimes, when we create functions that interact with our structs they usually get mixed up with other functions. This disorder makes it hard to easily understand what our code does. Fortunately, Rust allows us to group functions directly related to the struct together. It is done using the **impl** block. For example, we start with our struct.


```rust
struct Person {
    name: String,
    age: u8,
}
```


Then we have an **impl** block to define the functions we will be using with this struct.


```rust
impl Person {
    fn create_person(name: String, age: u8) -> Person {
        Person {
            name: name,
            age: age,
        }
    }

    fn display(&self) {
        println!("Name: {}\nAge: {}", self.name, self.age);
    }
}
```


In the snippet above, we added the name of the struct we are modifying immediately after the **impl** keyword. This tells Rust that any function declared in the block will be attached to the struct.

In our example, we created two types of functions for the struct. The first one is a static function and the second is a method. The static functions are functions that are accessed directly from the struct and not its instance. The static function in our code is **create_person** and the way we access it is as follows.


```rust
let person = Person::create_person("John".to_string(), 23);
```


Methods are a type of function that belongs to an instance of the struct. The way we declare a method is by adding **&self** as the first argument of the function. And then we can access the elements of the struct instance that calls it using **self.property_name**. The method in our code is called **display** and the way we assess it is as follows.


```rust
person.display();
```


A simple program based on the previous example is.


```rust
struct Person {
    name: String,
    age: u8,
}

impl Person {
    fn create_person(name: String, age: u8) -> Person {
        return Person {
            name: name,
            age: age,
        };
    }

    fn display(&self) {
        println!("Name: {}\nAge: {}", self.name, self.age);
    }
}

fn main() {
    let person = Person::create_person("John".to_string(), 23);
    person.display();
}
```


Finally, we can use the unit structures to hold related functions. The functions will then be accessed like a namespace. These functions are static and will be created in an **impl** block. For example, have a simple one like this.


```rust
struct SomeFunctions;

impl SomeFunctions {
    fn print_hello() {
        println!("Hello");
    }

    fn print_goodbye() {
        println!("Goodbye");
    }
}
```


Which can then be accessed like so.


```rust
SomeFunctions::print_hello();
SomeFunctions::print_goodbye();
```



## Conclusion

In this tutorial, we walked through the process of creating a data structure using struct. Then, we also added more functionality to the structs. And, we saw how we can use empty structs to create modules to organize our code.

I hope this helps in understanding how structs work in rust and how we can use them to improve the quality of our code.

Thanks for reading and happy hacking!