Easy Tutorial
❮ Rust Project Management Rust Conditions ❯

Rust Structs

In Rust, both structs and tuples can bundle several data of potentially different types into a single entity. However, each member of a struct, as well as the struct itself, has a name. This means you don't have to remember indices to access its members. Tuples are often used for ad-hoc value passing, while structs are used to standardize commonly used data structures. Each member of a struct is called a "field."

Struct Definition

Here is a struct definition:

struct Site {
    domain: String,
    name: String,
    nation: String,
    found: u32
}

Note: If you are familiar with C/C++, remember that in Rust, the struct statement is only for defining and cannot declare instances. It does not require a semicolon at the end, and fields are separated by commas.

Struct Instantiation

Rust has been influenced by JavaScript in many ways, and this is evident in how structs are instantiated using the key: value syntax of JSON objects:

let tutorialpro = Site {
    domain: String::from("www.tutorialpro.org"),
    name: String::from("tutorialpro"),
    nation: String::from("China"),
    found: 2013
};

If you are not familiar with JSON objects, you can ignore that and just remember the format:

StructName {
    fieldName: fieldValue,
    ...
}

This approach not only makes the program more intuitive but also allows you to input member values without adhering to the defined order.

If the fields of the struct being instantiated have the same names as existing variables, you can simplify the notation:

let domain = String::from("www.tutorialpro.org");
let name = String::from("tutorialpro");
let tutorialpro = Site {
    domain,  // equivalent to domain: domain,
    name,    // equivalent to name: name,
    nation: String::from("China"),
    traffic: 2013
};

There is a scenario where you want to create a new struct instance with most properties identical to an existing one, but with a few fields changed. You can use the struct update syntax:

let site = Site {
    domain: String::from("www.tutorialpro.org"),
    name: String::from("tutorialpro"),
    ..tutorialpro
};

Note: After ..tutorialpro, a comma is not allowed. This syntax does not allow an exact copy of another struct instance; you must at least reset the value of one field to reference values from another instance.

Tuple Structs

There is a simpler way to define and use structs: tuple structs.

Tuple structs are structs that have the form of tuples.

Unlike regular tuples, they have names and a fixed type format. They are useful for types that need to be defined (often used) but are too simple to warrant a complex structure:

struct Color(u8, u8, u8);
struct Point(f64, f64);

let black = Color(0, 0, 0);
let origin = Point(0.0, 0.0);

"Color" and "point coordinates" are commonly used data types. However, Rust addresses the issue of sacrificing convenience for readability by using tuple structs. Tuple struct objects are accessed using the dot notation and indices, similar to tuples:

fn main() {
    struct Color(u8, u8, u8);
    struct Point(f64, f64);

    let black = Color(0, 0, 0);
    let origin = Point(0.0, 0.0);

    println!("black = ({}, {}, {})", black.0, black.1, black.2);
    println!("origin = ({}, {})", origin.0, origin.1);
}

Output:

black = (0, 0, 0)
origin = (0, 0)

Struct Ownership

A struct must own its field values because the struct releases all its fields when it goes out of scope.

This is why String types are used in the examples instead of &str.

However, this does not mean that structs cannot define reference fields; this requires the use of "lifetimes." It is currently difficult to explain the concept of "lifecycle," so it will be discussed in later chapters.

Output Structure

During debugging, it is very useful to display a complete instance of a structure. However, manually writing a format can be quite inconvenient. Therefore, Rust provides a convenient way to output an entire structure:

Example

#[derive(Debug)]

struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };

    println!("rect1 is {:?}", rect1);
}

As shown in the first line: Be sure to import the debug library #[derive(Debug)], and then you can use the {:?} placeholder in the println and print macros to output an entire structure:

rect1 is Rectangle { width: 30, height: 50 }

If there are many attributes, you can use another placeholder {:#?}.

Output result:

rect1 is Rectangle {
    width: 30,
    height: 50
}

Struct Methods

Methods are similar to functions, except that they are used to operate on struct instances.

If you have learned some object-oriented languages, you are certainly familiar with the fact that functions are generally placed within class definitions and use this to represent the instance being operated on.

Rust is not an object-oriented language, as can be seen from its innovative ownership mechanism. However, the valuable ideas of object-oriented programming can be implemented in Rust.

The first parameter of a struct method must be &self, and no type needs to be declared because self is not a style but a keyword.

Calculate the area of a rectangle:

Example

struct Rectangle {
    width: u32,
    height: u32,
}

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

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    println!("rect1's area is {}", rect1.area());
}

Output result:

rect1's area is 1500

Note that when calling struct methods, you do not need to fill in self, which is for the sake of convenience.

An example with multiple parameters:

Example

struct Rectangle {
    width: u32,
    height: u32,
}

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

    fn wider(&self, rect: &Rectangle) -> bool {
        self.width > rect.width
    }
}

fn main() {
    let rect1 = Rectangle { width: 30, height: 50 };
    let rect2 = Rectangle { width: 40, height: 20 };

    println!("{}", rect1.wider(&rect2));
}

Running result:

false

This program calculates whether rect1 is wider than rect2.


Struct Associated Functions

The term "struct method" is not called "struct function" because the word "function" is reserved for functions like this: it is in the impl block but does not have the &self parameter.

Such functions do not depend on instances, but using them requires declaring which impl block they are in.

The String::from function we have been using is an "associated function."

Example

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn create(width: u32, height: u32) -> Rectangle {
        Rectangle { width, height }
    }
}

fn main() {
    let rect = Rectangle::create(30, 50);
    println!("{:?}", rect);
}

Running result:

Rectangle { width: 30, height: 50 }

Tip: The impl block for a struct can be written several times, and the effect is equivalent to concatenating their contents!

Unit Struct

Unit structs are useful when you need to implement a trait on some type but don’t have any data that you want to store in the type itself. A struct can exist purely as a symbol without any members:

struct UnitStruct;

We call this type of struct without a body a unit struct (Unit Struct).

❮ Rust Project Management Rust Conditions ❯