Adding Behavior
Last updated
Last updated
impl
blocksWe 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 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.
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!`
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.
Therefore, trait objects are usually used behind some form of pointer, either regular (&dyn Trait
) or smart (Box<dyn Trait>
).
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.
This uses a 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 using a +
, like so: T: Debug + Display
.
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 . 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 , meaning you usually can’t work with it directly, since the compiler cannot know how big it is.
Trait objects include a , which can make function access a tiny bit slower. Usually, Rust’s generics are preferred, since they can be optimized per-type, and also preserve type information across the codebase.