DEV Community

Cover image for Learning Rust πŸ¦€: 10 - The Struct
Fady GA 😎
Fady GA 😎

Posted on • Edited on

Learning Rust πŸ¦€: 10 - The Struct

Now we will have our first look at how we can make our own custom types in Rust. Let's jump in!

⚠️ Remember!

You can find all the code snippets for this series in its accompanying repo

If you don't want to install Rust locally, you can play with all the code of this series in the official Rust Playground that can be found on its official page.

⚠️⚠️ The articles in this series are loosely following the contents of "The Rust Programming Language, 2nd Edition" by Steve Klabnik and Carol Nichols in a way that reflects my understanding from a Python developer's perspective.

⭐ I try to publish a new article every week (maybe more if the Rust gods πŸ™Œ are generous 😁) so stay tuned πŸ˜‰. I'll be posting "new articles updates" on my LinkedIn and Twitter.

Table of Contents:

Struct:

We have looked previously at some Rust types that groups together "related" data like Array (same type) and Tuple (different types). Basically, a Struct - or structure - is a way to group related data with different types but also naming them in a custom type. They are more or less like an Object (or more precisely, a Class) in OOP languages.

We define a named Struct with the following syntax:

Yes, there are unnamed (tuple) or even with only the struct name. but we will see those in later posts.

struct Car {
    maker: String,
    model: String,
    year_of_making: u32,
    color: String,
}
Enter fullscreen mode Exit fullscreen mode

This defines a Struct called Car that has named ... attributes - if we can call them that - and their types. This definition is located outside of any function including the main function.

To use the Struct, we have to create an instance of it. We can then access its attributes with the dot . notation as follows:

fn main() {
    // Define and using a Struct
    let car1 = Car {
        model: String::from("model1"),
        maker: String::from("Maker1"),
        color: String::from("Red"),
        year_of_making: 2023,
    };

    println!("Car 1 is a {} and of {} color", car1.maker, car1.color);
}
Enter fullscreen mode Exit fullscreen mode

In this code snippet, we have created an immutable Car instance and printed out its maker and color. Note the attributes order we used. The instantiation order doesn't have to match the definition order.

As Struct is considered a type, we can pass and return it to/from functions. One way we can use that is in a factory function that returns a new instance of the Struct. We can also use a neat shorthand that Rust provides that we don't have to write the name of the attribute and its variable as long as both have the same name. Meaning that instead of writing Car { maker: maker } we can write it like that Car { maker } which proves very usefull for Struct with many attributes:

fn main() {
    .....

    let car2 = get_car(
        String::from("Maker2"),
        String::from("Model2"),
        2022,
        String::from("Yellow"),
    );

    println!("Car 2 is a {} of {} color", car2.maker, car2.color);
}
fn get_car(maker: String, model: String, year_of_making: u32, color: String) -> Car {
    Car {
        maker,
        model,
        year_of_making,
        color,
    }
}
Enter fullscreen mode Exit fullscreen mode

Notice the shorthand usage in the get_car function.

We can also create Struct instances from the instances of the same type containing some or all the attributes of the original instance using the .. syntax. But be careful, as this operation moves the original attributes if complex types (heap data) are used i.e. the new instance take ownership. When using the .., you can modify some of the original attributes' values and any attribute that wasn't explicitly modified will get its value from the original attributes. The .. should be written at the end of the Struct instantiation.

fn main() {
    // Define and using a Struct
    let car1 = Car {
        model: String::from("model1"),
        maker: String::from("Maker1"),
        color: String::from("Red"),
        year_of_making: 2023,
    };

    let mut car3 = Car {
        color: String::from("Blue"),
        ..car1
    };

    println!("Can we print car1 maker now? {}", car1.maker);  // ERROR: borrow of moved value: `car1.maker`
    println!("Car 3 is a {} and of {} color", car3.maker, car3.color);

    car3.color = String::from("Green");

    println!("Now car 3 is of {} color", car3.color);

}
Enter fullscreen mode Exit fullscreen mode

Note that the last println! macro will cause a compile time error as car1.maker was moved into car3.maker. Also, if the Struct is mutable, we can modify its values with the dot . notation.

We can "borrow" the values and not move them between instances, but this will require the use of "lifetimes" which are a topic for another article πŸ˜‰.

Our Rectangle type:

We will now start building our Rectangle type (Struct). It will contain two main attributes, the width and the height. We can also create a function to calculate its area taking a Rectangle as input:

struct Rectangle {
    height: u32,
    width: u32,
}
fn main() {
    let rect1 = Rectangle {
        height: 10,
        width: 5,
    };
    println!("rect1 is {} x {} and its area is: {}", rect1.width, rect1.height, area(&rect1);
}

fn area(rect: &Rectangle) -> u32 {
    rect.width * rect.height
}
Enter fullscreen mode Exit fullscreen mode

For this particular case, we could have used a tuple and accessed its width and height like this:

let rect1 = (5, 10);
println!("rect1 is {} x {}", rect1.0, rect1.1);
....
Enter fullscreen mode Exit fullscreen mode

But the Struct one is cleaner! With the Tuple, it isn't clear which value is the width and which is the height. We rely on the order of the items in the tuple which will make our code harder to read for anyone trying to use it!

Structs and Methods:

Our area function is very specific to the Rectangle type. We can "associate" it with the Struct with the impl (implementations) keyword. If we passed &self to this function, it will become a method (Yup! this weird naming scheme again πŸ€·β€β™‚οΈ). So, let's do that:

&self is a shorthand for self: &Self. The Self type is an alias of the type that is in the impl block. We call them "associated" functions as they are "associated" with the type.

struct Rectangle {
    height: u32,
    width: u32,
}
impl Rectangle {
    fn area(&self) -> u32 {
        self.height * self. width
    }
}

fn main() {
    let rect1 = Rectangle {
        height: 10,
        width: 5,
    };
    println!("rect1 is {} x {}", rect1.width, rect1.height);
    println!("rect1 area is: {}", rect1.area());
}
Enter fullscreen mode Exit fullscreen mode

We can also create methods that compare the owner instance with other instances of the same type. Like for example, we can write a method that determines if a rectangle can fit other ones comparing their width and height:

struct Rectangle {
    height: u32,
    width: u32,
}
impl Rectangle {
    fn area(&self) -> u32 {
        self.height * self.width
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        (self.width >= other.width) & (self.height >= other. height)
    }
}

fn main() {
    let rect1 = Rectangle {
        height: 10,
        width: 5,
    };
    println!("rect1 is {} x {}", rect1.width, rect1.height);
    println!("rect1 area is: {}", rect1.area());

    let rect2 = Rectangle {
        height: 9,
        width: 4,
    };

    let rect3 = Rectangle {
        height: 11,
        width: 6,
    };

    println!("Can rect1 hold rect2: {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3: {}", rect1.can_hold(&rect3));

}
Enter fullscreen mode Exit fullscreen mode

With the can_hold method, we are passing a borrow of another rectangle and see if our owner rectangle can fit those. The output will be true and false for the last two prints.

Associated functions that are not methods:

We can write associated functions (or methods or call them whatever you like 😁) in the impl that doesn't receive &self as a parameter (or whatever you call them). In this case, we can call them with the :: notation (like in the String::new() but this one is module "namespacing" which can also be used with Structs)

struct Rectangle {
    height: u32,
    width: u32,
}
impl Rectangle {
    ....
    }
    fn square(dimension: u32) -> Rectangle {
        Rectangle {
            width: dimension,
            height: dimension,
        }
    }
}

fn main() {

    let square1 = Rectangle::square(5);

    println!("square1 is {} x {}", square1.width, square1.height);
}
Enter fullscreen mode Exit fullscreen mode

Here, square function is just returning a special Rectangle with both its width and height are equal to the passed value to it.

In the next article, we will talk about how we can debug our custom types! See you then πŸ‘‹.

Top comments (2)

Collapse
 
maryhale profile image
Mary Hale • Edited

I liked your article on learning Rust and exploring its struct concept as I want to know more about this programming language. Learning new skills and concepts can be challenging, but it's rewarding to see how they come together. I am always in the process of learning and I recently came across an article on usa essay writing services that can provide support for students with complex subjects and assignments. Thanks one more time for your article that gives more understanding Rust's struct.

Collapse
 
fadygrab profile image
Fady GA 😎

My pleasure. I'm glad that you find it useful ☺️.