DEV Community

loading...
ITNEXT

Rust: structs, methods, and traits

Abhishek Gupta
Mostly work with open source tech including Kafka, Databases etc.
・5 min read

This blog offers a quick tour of Rust structs, methods and traits. It uses simple examples to demonstrate the concepts.

the code is available on GitHub

struct

A struct in Rust is the same as a Class in Java or a struct in Golang. Its a named type to which you can assign state (attributes/fields) and behavior (methods/functions).

Here is a struct with fields

struct Programmer {
    email: String,
    github: String,
    blog: String,
}
Enter fullscreen mode Exit fullscreen mode

To instantiate a Programmer, you can simply:

let pg1 = Programmer {
        email: String::from("abhirockzz@gmail.com"),
        github: String::from("https://github.com/abhirockzz"),
        blog: String::from("https://dev.to/abhirockzz"),
    };
Enter fullscreen mode Exit fullscreen mode

Methods

Methods are behavior associated with a given type. The first parameter in a method is always self, which represents the instance on which the method is being invoked.

Let's add a method to Programmer. To do that, we will need use an impl block:

impl Programmer {
    fn is_same_as(&self, other: Programmer) -> bool {
        return self.email == other.email;
    }
}
Enter fullscreen mode Exit fullscreen mode

The is_same_as method accepts a reference to the instance being invoked on (&self) and another Programmer instance. To call it, create another instance of a Programmer (pg2) and compare pg1 with it.

let pg2 = Programmer {
        email: String::from("abhirockzz@gmail.com"),
        github: String::from("https://github.com/abhirockzz"),
        blog: String::from("https://medium.com/@abhishek1987"),
    };

println!("pg1 same as pg2? {}", pg1.is_same_as(pg2));
Enter fullscreen mode Exit fullscreen mode

self types

We used &self as the parameter for is_same_as. This way, we will only pass a reference and the function will not own the value - only borrow it (see Rust: Ownership and Borrowing). It is also possible to use self and &mut self.

You can use self, but be careful that it will pass on the ownership to the function. Let's see this function

fn details(self) {
        println!(
            "Email: {},\nGitHub repo: {},\nBlog: {}",
            self.email, self.github, self.blog
        )
}
Enter fullscreen mode Exit fullscreen mode

You can invoke it using the pg2 instance as such pg2.details(); and you should get back

Email: abhirockzz@gmail.com,
GitHub repo: https://github.com/abhirockzz,
Blog: https://medium.com/@abhishek1987
Enter fullscreen mode Exit fullscreen mode

If you try to use pg2 again (e.g. pg2.is_same_as(&pg1);), you will get a compiler error

error[E0382]: borrow of moved value: `pg2`
Enter fullscreen mode Exit fullscreen mode

If you want to mutate the Programmer instance, make use of &mut self as follows:

fn some_function(&mut self) {
    //use self
}

//to invoke it
let mut pg3 = Programmer{...};
pg3.some_function();
Enter fullscreen mode Exit fullscreen mode

If you don't mark the variable (pg3) as mut (mutable), you'll get a compiler error

error[E0596]: cannot borrow `pg2` as mutable, as it is not declared as mutable
Enter fullscreen mode Exit fullscreen mode

Other functions related to the struct

You can add associated functions which are tied with the instance of a struct - think of it as a static method in Java. These are commonly used as constructors

it's different to how constructors are used in Java but similar to Go approach

fn new(email: String, github: String, blog: String) -> Self {
    return Programmer {
        email: email,
        github: github,
        blog: blog,
    };
}
Enter fullscreen mode Exit fullscreen mode

Here is how you can use it:

let pg3 = Programmer::new(
        String::from("abhirockzz@gmail.com"),
        String::from("https://github.com/abhirockzz"),
        String::from("https://medium.com/@abhishek1987"),
    );
Enter fullscreen mode Exit fullscreen mode

Notice that we are using :: (Programmer::new) to access associated members of a struct (a function in this case)

It is possible to use different impl blocks for the same struct

Trait

A Trait in Rust is similar to Interface in other languages such as Java etc. They help define one or more set of behaviors which can be implemented by different types in their own unique way. The way a Trait is implemented in Rust is quite similar to how it's done in Java. In Java, you can use the implements keyword, which Rust uses impl

There is an explicit association b/w the interface and the type implementing it. This is quite different compared to Go, where you don't need to declare which interface you're implementing - if you have implemented the required behavior, the compiler will be happy.

Let's start by defining a Trait

trait PrettyPrint {
    fn pretty_print(&self);
}
Enter fullscreen mode Exit fullscreen mode

.. and implementing it

don't worry about the specifics of the example, just focus on the key parts

impl PrettyPrint for Programmer {
    fn pretty_print(&self) {
        println!(
            "{{\n\t\"email\": {},\n\t\"github_repo\" repo: {},\n\t\"blog_url\": {}\n}}",
            self.email, self.github, self.blog
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

It's quite simple - use the impl keyword followed by the trait you're implementing and include the type which is implementing the trait after the for keyword. Now you can use it just like any other method:

let pg = Programmer::new(...);
pg.pretty_print();
Enter fullscreen mode Exit fullscreen mode

Use a std library trait

Rust standard library provides a number of Traits which you can use. Let's implement the std::fmt::Display trait. This is for user-facing output e.g. we can use it with the println! macro

impl std::fmt::Display for Programmer {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "({}, {}, {})", self.email, self.github, self.blog)
    }
}
Enter fullscreen mode Exit fullscreen mode

Don't worry about the specifics of the implementation and the types such as std::fmt::Formatter etc. For now, just understand that you can implement Display trait from the Rust std::fmt module.

From now on, you can use an instance of a Programmer with println! as such

let another_pg = Programmer::new(...);
println!("programmer details: {}", another_pg);

//output:
programmer details: (abhirockzz@gmail.com, https://github.com/abhirockzz, https://medium.com/@abhishek1987)
Enter fullscreen mode Exit fullscreen mode

Freebies!

Rust provides many useful traits that you can use for free! This means that you don't need to implement them explicitly (you certainly can if you want to). The way you do it is by using the #[derive] attribute.

An attribute is just a piece of metadata that you can apply to structs etc. They remind me of Java annotations

Here is an example. We can use apply the std::fmt::Debug trait on Programmer and then use it with println!. This is similar to what we did with Display but the key difference is that its not possible to derive the Display (you have to implement it). All you need to do is add #[derive(Debug)] to the the Programmer struct:

#[derive(Debug)]
struct Programmer {
    email: String,
    github: String,
    blog: String,
}
Enter fullscreen mode Exit fullscreen mode

Simply use the :? format to leverage the default Debug functionality

let pg = Programmer::new(...);
println!("{:?}", pg);

//output:
Programmer { email: "abhirockzz@gmail.com", github: "https://github.com/abhirockzz", blog: "https://medium.com/@abhishek1987" }
Enter fullscreen mode Exit fullscreen mode

If you add a # to the mix, you get pretty printing for free.. yay!

println!("{:#?}", pg);

//output
Programmer {
    email: "abhirockzz@gmail.com",
    github: "https://github.com/abhirockzz",
    blog: "https://medium.com/@abhishek1987",
}
Enter fullscreen mode Exit fullscreen mode

There are other utility traits such as Eq, Clone, PartialEq etc. which you can derive

Using traits

To take advantage of traits, you should be able to accept and return them from functions and methods in order to make use of the "general" behavior. We can write a function which accepts a type that implements PrettyPrint

fn print_the_printable(p: impl PrettyPrint) {
    p.pretty_print()
}

//invoke
let pg = Programmer::new(...);
print_the_printable(pg);
Enter fullscreen mode Exit fullscreen mode

note that impl has been added to the parameter

A PrettyPrint can be returned as well

fn get_printable(info: Vec<String>) -> impl PrettyPrint {
    Programmer::new(
        String::from(&info[0]),
        String::from(&info[1]),
        String::from(&info[2]),
    )
}

//invoke
let info: Vec<String> = vec![
        String::from("abhirockzz@gmail.com"),
        String::from("https://github.com/abhirockzz"),
        String::from("https://medium.com/@abhishek1987"),
    ];
get_printable(info).pretty_print();
Enter fullscreen mode Exit fullscreen mode

Default methods

It is possible to provide default implementations of methods within the trait itself (these can be overridden if required). These methods can also invoke other trait methods (default or not)

Discussion (0)

Forem Open with the Forem app