DEV Community

Chigozie Oduah
Chigozie Oduah

Posted on

Structs in Rust

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.

struct Person {
    name: String,
    age: u8,
}
Enter fullscreen mode Exit fullscreen mode

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.

let person = Person {
    name: "John".to_string(),
    age: 23,
};
Enter fullscreen mode Exit fullscreen mode

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.

println!("Name: {}\nAge: {}", person.name, person.age);
Enter fullscreen mode Exit fullscreen mode

And we can have a full program like so.

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

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

    println!("Name: {}\nAge: {}", person.name, person.age);
}
Enter fullscreen mode Exit fullscreen mode

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

Name: John
Age: 23
Enter fullscreen mode Exit fullscreen mode

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.

struct Person (String, u8);
Enter fullscreen mode Exit fullscreen mode

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.

let person = Person ("John".to_string(), 23);
Enter fullscreen mode Exit fullscreen mode

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.

println!("Name: {}\nAge: {}", person.0, person.1);
Enter fullscreen mode Exit fullscreen mode

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

struct Person (String, u8);

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

    println!("Name: {}\nAge: {}", person.0, person.1);
}
Enter fullscreen mode Exit fullscreen mode

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.

struct Unit;
Enter fullscreen mode Exit fullscreen mode

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

let unit_struct = Unit;
Enter fullscreen mode Exit fullscreen mode

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.

struct User {
    name: String,
    is_active: bool,
}
Enter fullscreen mode Exit fullscreen mode

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.

let mut user = User {
    name: "John".to_string(),
    is_active: false,
};
Enter fullscreen mode Exit fullscreen mode

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

user.is_active = true;
Enter fullscreen mode Exit fullscreen mode

We can now have a simple code like this.

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);
}
Enter fullscreen mode Exit fullscreen mode

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

John is active: false

John is active: true
Enter fullscreen mode Exit fullscreen mode

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.

fn create_person(name: String, age: u8) -> Person {
    return Person {
        name: name,
        age: age,
    };
}
Enter fullscreen mode Exit fullscreen mode

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.

fn create_person(name: String, age: u8) -> Person {
    Person {
        name: name,
        age: age,
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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);
}
Enter fullscreen mode Exit fullscreen mode

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.

struct Person {
    name: String,
    age: u8,
}
Enter fullscreen mode Exit fullscreen mode

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

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

let person = Person::create_person("John".to_string(), 23);
Enter fullscreen mode Exit fullscreen mode

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.

person.display();
Enter fullscreen mode Exit fullscreen mode

A simple program based on the previous example is.

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();
}
Enter fullscreen mode Exit fullscreen mode

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.

struct SomeFunctions;

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

    fn print_goodbye() {
        println!("Goodbye");
    }
}
Enter fullscreen mode Exit fullscreen mode

Which can then be accessed like so.

SomeFunctions::print_hello();
SomeFunctions::print_goodbye();
Enter fullscreen mode Exit fullscreen mode

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!

Latest comments (0)