Adding Behavior

impl blocks

We can add behavior to individual types with a simple impl block:

`struct BasicGreeter { greeting: String, }

impl BasicGreeter { fn greet(&self, name: &str) { println!("{}, {name}!", self.greeting); } }

let g = BasicGreeter { greeting: "Welcome".to_string(), }; g.greet("John");`

Output:

Welcome, John!

The &self parameter is special: it causes a function to be a method, operating on an instance of a type, as opposed to an associated function, which does not necessarily operate on an instance. In object-oriented terms, functions that take a self parameter (or any of the variants) are like instance methods, and functions that do not are like static methods.

Traits

Traits are the primary form of abstraction in Rust. A trait describes a set of behaviors that a type implements. It’s very similar to an interface in a language like Java. Actually, we’ve been using some traits already, in a subtle sort of way.

Remember how to print things to the screen? println!(...)? As you probably noticed by now, we can print a bunch of different things. Numbers, strings, booleans, etc.

println!("{}, {}, {}, {}", "hello", 42, 3.14, true);

This is an example of abstraction: all of the different types all support the behavior of “being printed.”

In Rust, this behavior is described by the [Display](<https://doc.rust-lang.org/std/fmt/trait.Display.html>) trait, which is like adding a toString method to a class in Java.5

All of these types (String, &str, u32, f32, bool, …) implement Display.

Let’s take a look at writing a trait and implementing it.

`// create a trait trait Greeter { fn greet(&self, name: &str); }

struct MorningGreeter;

// implement the trait on MorningGreeter impl Greeter for MorningGreeter { fn greet(&self, name: &str) { println!("Good morning, {name}!"); } }

struct EveningGreeter;

// implement the trait on EveningGreeter impl Greeter for EveningGreeter { fn greet(&self, name: &str) { println!("Good evening, {name}!"); } }

let g1 = MorningGreeter; g1.greet("Alice"); // -> Good morning, Alice!

let g2 = EveningGreeter; g2.greet("Bob"); // -> Good evening, Bob!`

This isn’t terribly interesting yet. Let’s spice it up some.

`fn greet_wizard<G: Greeter>(g: G) { g.greet("Gandalf"); }

greet_wizard(EveningGreeter); // -> Good evening, Gandalf!`

This uses a generic type parameter to allow us to pass it any parameter that implements the Greeter trait. If you’re familiar with Java, etc., the angle bracket <> syntax might look familiar.

The colon in G: Greeter means “G implements Greeter.” You can specify multiple trait bounds using a +, like so: T: Debug + Display.

If you have too many generic parameters, or if the bounds are too complex, you can move them to a where clause to organize your function signature a little:

fn my_function<T>(t: T) where T: Send + Sync {}

If you don’t need to refer to the generic parameter by name, you can use a shorthand:

`fn greet_wizard(g: impl Greeter) { g.greet("Gandalf"); }

greet_wizard(EveningGreeter); // -> Good evening, Gandalf!`

This code is identical to the previous greet_wizard example, but it’s a little easier to read, since there’s no G generic parameter floating around.

Trait objects

There’s another way to accept parameters based on what traits they implement, as opposed to by concrete type. Using the dyn keyword, we can create a trait object. It’s a bit of a hairy topic if you dive deeply into it, but for now, keep in mind the following properties:

  • dyn Trait is unsized, meaning you usually can’t work with it directly, since the compiler cannot know how big it is.

  • Therefore, trait objects are usually used behind some form of pointer, either regular (&dyn Trait) or smart (Box<dyn Trait>).

  • Trait objects include a vtable, which can make function access a tiny bit slower. Usually, Rust’s monomorphized generics are preferred, since they can be optimized per-type, and also preserve type information across the codebase.

Drop

Called automatically when an object goes out of scope. The main use of the Drop trait is to free the resources that the implementor instance owns.

Last updated