Rise In Logo





Build on Stellar

Structs

Welcome to the world of structs in Rust! In this section, we will introduce you to structs, explain why they are useful, and provide a basic example to get you started.

Structs, short for structures, are a way to group related data together in a custom data type. They are very helpful in organizing your code and making it more readable and maintainable. You can think of a struct as a blueprint for creating objects with specific properties and behaviors.

For example, let's say you are building a program that deals with information about books. A book typically has a title, an author, and a publication year. Instead of dealing with these pieces of information separately, you can create a struct that represents a book, combining all these data into a single unit.

Here's a simple example of a struct in Rust:

struct Book {
  title: String,
  author: String,
  publication_year: u32,
}

In this example, we define a Book struct with three fields: title, author, and publication_year. Each field has a specific type (in this case, String for title and author, and u32 for publication_year). With this Book struct, we can now easily represent books and their properties in our program.

Defining Structs

In Rust, structs are custom data types that help you group related data together. Defining a struct is quite simple. You start with the struct keyword, followed by the name of the struct and a pair of curly braces {}. Inside the curly braces, you define the fields of the struct, each with a name and a type, separated by a colon.

Let's look at an example. Imagine we want to represent a point in 2D space. We can create a Point struct with x and y fields as follows:

struct Point {
  x: f32,
  y: f32,
}

In this example, we defined a Point struct with two fields, x and y, both of type f32 (32-bit floating-point numbers). The fields are separated by commas.

You can create more complex structs by adding more fields with different types. Just remember to separate the fields with commas, and don't forget the closing curly brace.

Creating Instances of Structs

In this section, we'll learn how to create instances of a struct and initialize their fields. Creating a struct instance is similar to defining a variable with a specific type.

First, let's consider our simple Point struct that represents a point in a 2D coordinate system:

struct Point {
  x: f32,
  y: f32,
}

To create an instance of the Point struct, we use the following syntax:

let point = Point { x: 1, y: 2 };

Here, we've created a Point instance called point, and we've initialized its x and y fields with the values 1 and 2, respectively. The curly braces {} are used to assign values to the fields of the struct.

We can also create instances with different field values:

let another_point = Point { x: -3, y: 4 };

In this example, we've created a new Point instance called another_point with x set to -3 and y set to 4. Notice that we use the same syntax to create a new instance, but we just provide different values for the fields.

Now that you know how to create instances of a struct, you can practice by creating instances of various structs and initializing their fields with different values.

Accessing Struct Fields

In this section, we will learn how to access the fields of a struct instance. Accessing struct fields is a simple process that allows you to read and modify the values of the fields.

To access a field of a struct, you can use the dot notation followed by the field name. Let's take a look at an example:

struct Person {
  name: String,
  age: u32,
}
 
fn main() {
  let alice = Person {
    name: String::from("Alice"),
    age: 30,
  };
 
  // Accessing the 'name' and 'age' fields
  println!("Name: {}", alice.name);
  println!("Age: {}", alice.age);
}

In the example above, we create an instance of the Person struct called alice. We then access the name and age fields using the dot notation: alice.name and alice.age.

You can also modify the values of the fields if the struct instance is mutable. To do this, simply use the dot notation and assign a new value to the field:

struct Person {
  name: String,
  age: u32,
}
fn main() {
  let mut alice = Person {
    name: String::from("Alice"),
    age: 30,
  };
 
  // Modifying the 'age' field
  alice.age = 31;
  println!("Updated Age: {}", alice.age);
}

In this example, we declare alice as mutable using let mut. We then update the age field to 31 using the dot notation and assignment operator.

Functions and Structs

In this section, we'll learn how to use structs with functions. Functions can take structs as parameters, and they can also return structs. This allows us to create more complex and organized code.

Let's start with our Point struct example

struct Point {
  x: f64,
  y: f64,
}

Now, let's create a function that calculates the distance between two points:

fn distance(p1: Point, p2: Point) -> f64 {
  let x_diff = p1.x - p2.x;
  let y_diff = p1.y - p2.y;
  (x_diff * x_diff + y_diff * y_diff).sqrt()
}

In this example, the distance function takes two Point structs as parameters, p1 and p2. We calculate the difference between their x and y coordinates and use the Pythagorean theorem to find the distance between them. Finally, we return the calculated distance as an f64.

We can use this function in our main function like this:

fn main() {
  let point1 = Point { x: 1.0, y: 1.0 };
  let point2 = Point { x: 4.0, y: 5.0 };
 
  let dist = distance(point1, point2);
  println!("The distance between point1 and point2 is: {}", dist);
}

Functions can also return structs. For instance, let's create a function that returns the midpoint of two points:

fn midpoint(p1: Point, p2: Point) -> Point {
  let x_mid = (p1.x + p2.x) / 2.0;
  let y_mid = (p1.y + p2.y) / 2.0;
  Point { x: x_mid, y: y_mid }
}

This midpoint function calculates the average of the x and y coordinates of p1 and p2 and returns a new Point struct representing the midpoint.

We can now use this function in our main function:

fn main() {
  let point1 = Point { x: 1.0, y: 1.0 };
  let point2 = Point { x: 4.0, y: 5.0 };
 
  let mid_point = midpoint(point1, point2);
  println!("The midpoint of point1 and point2 is: ({}, {})", mid_point.x, mid_point.y);
}

In this section, we've learned how to use functions with structs as parameters and return values. This allows us to create more organized and efficient code when working with complex data structures.

Tuple Structs

In this section, we'll explore tuple structs, a variant of the traditional structs that combines features of both structs and tuples.

A tuple struct is similar to a regular struct, but its fields do not have names. Instead, they are accessed by their position, just like the elements of a tuple. Tuple structs can be useful when you want to create a simple data structure with a few fields but don't need the complexity of named fields.

To define a tuple struct, you use the struct keyword followed by the name of the struct and the types of its fields enclosed in parentheses. Here's an example of a tuple struct representing a 3D point:

struct Point3D(f32, f32, f32);

To create an instance of a tuple struct, you provide the values for each field in the same order as they were defined, enclosed in parentheses:

let point = Point3D(1.0, 2.0, 3.0);

Accessing the fields of a tuple struct is done using dot notation followed by the index of the field, starting from 0:

let x = point.0;
  let y = point.1;
  let z = point.2;

Keep in mind that, like with regular structs, you can also use tuple structs as parameters and return values for functions:

fn calculate_distance(point1: Point3D, point2: Point3D) -> f32 {
  let dx = point1.0 - point2.0;
  let dy = point1.1 - point2.1;
  let dz = point1.2 - point2.2;
 
  (dx*dx + dy*dy + dz*dz).sqrt()
}

Now you know how to define and use tuple structs in Rust. They provide a lightweight alternative to regular structs when you need a simple data structure without named fields.

Unit Structs

Unit structs are a special kind of struct in Rust that don't have any fields. They are useful when you want to create a distinct type without carrying any data with it. Unit structs can be helpful for implementing type-based abstractions, marker traits, or creating a new type for better code organization.

Let's take a look at an example of a unit struct:

struct Empty;

In this example, we define a unit struct named Empty. As you can see, there are no fields within the curly braces. This is how you create a unit struct.

You can create an instance of a unit struct like this:

let empty_instance = Empty;

Even though unit structs don't have any fields, they still can be used as distinct types in your program. For example, you can implement methods for unit structs or use them as type constraints in generic functions or structures.

Here's an example of implementing a method for our Empty unit struct:

impl Empty {
  fn greet(&self) {
    println!("Hello, I am an empty struct!");
  }
}
 
let empty_instance = Empty;
empty_instance.greet();

In this example, we implement a greet method for the Empty struct, which simply prints a message to the console. As you can see, even though the unit struct doesn't have any fields, it can still have associated methods.

Remember that unit structs are best used for specific scenarios where you need a distinct type without any associated data. They can help you create cleaner, more organized code in such cases.

Debugging with Structs

In this section, we'll learn how to make debugging easier by displaying the contents of a struct instance in a human-readable format. To achieve this, we'll use the Debug trait, which is part of Rust's standard library.

The Debug trait allows you to print the contents of a struct instance using the {:?} format specifier, which is helpful when debugging your code.

To implement the Debug trait for your struct, simply add #[derive(Debug)] just above the struct definition, like this:

#[derive(Debug)]
struct Person {
  name: String,
  age: u32,
}
 
fn main() {
  let alice = Person {
    name: String::from("Alice"),
    age: 30,
  };
 
  println!("Alice's details: {:?}", alice);
}

In this example, we've implemented the Debug trait for the Person struct. Now, when we want to print the contents of a Person instance, we can use the {:?} format specifier in a println!() statement. The output of the example above will be:

Ø Alice's details: Person { name: "Alice", age: 30 }

By implementing the Debug trait, you can easily display the contents of your structs while debugging your code, making it more convenient to understand the state of your program.

Implementing Methods for Structs

In this section, we will learn about methods, which are functions associated with a specific struct, and how to implement them in Rust.

Methods allow us to define functions that are specifically tied to a struct, making it easier to work with the struct's data and providing a more organized way to interact with the struct. They are similar to regular functions, but they're defined within an impl block, which associates the methods with the struct.

Let's start with a simple example. Suppose we have a Rectangle struct representing a rectangle with a width and height:

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

Now, let's say we want to calculate the area of a rectangle. We can create a method for this purpose. To implement a method for our Rectangle struct, we need to define an impl block for it:

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

Here, we define a method called area that takes a reference to self as its parameter. The self keyword represents the instance of the struct that the method is being called on. The method returns the calculated area as a f64 value.

To call the area method on a Rectangle instance, we use the dot notation:

fn main() {
  let my_rectangle = Rectangle {
    width: 10.0,
    height: 5.0,
  };
 
  let area = my_rectangle.area();
  println!("The area of the rectangle is: {}", area);
}

In this example, we create a Rectangle instance called my_rectangle and call its area method to calculate and print the area.

Methods can also have additional parameters, just like regular functions. When defining methods with extra parameters, always list self as the first parameter.

Now that you have a good understanding of methods in Rust, try implementing more methods for the Rectangle struct, such as calculating the perimeter, checking if the rectangle is a square, or resizing the rectangle.

Rise In Logo

Rise together in web3