Unlock Rust's potential! Master impl
and Traits with Struct in Rust from scratch. Enhance your programming skills with this comprehensive tutorial.
Implement some functionality for a type.
The impl
keyword is primarily used to define implementations on types. Inherent implementations are standalone, while trait implementations are used to implement traits for types, or other traits.
Functions and consts can both be defined in an implementation. A function defined in an impl
block can be standalone, meaning it would be called like Foo::bar()
. If the function takes self
, &self
, or &mut self
as its first argument, it can also be called using method-call syntax, a familiar feature to any object oriented programmer, like foo.bar()
.
struct Example {
number: i32,
}
impl Example {
fn boo() {
println!("boo! Example::boo() was called!");
}
fn answer(&mut self) {
self.number += 42;
}
fn get_number(&self) -> i32 {
self.number
}
}
trait Thingy {
fn do_thingy(&self);
}
impl Thingy for Example {
fn do_thingy(&self) {
println!("doing a thing! also, number is {}!", self.number);
}
}
For more information on implementations, see the Rust book or the Reference.
The other use of the impl
keyword is in impl Trait
syntax, which can be seen as a shorthand for “a concrete type that implements this trait”. Its primary use is working with closures, which have type definitions generated at compile time that can’t be simply typed out.
fn thing_returning_closure() -> impl Fn(i32) -> bool {
println!("here's a closure for you!");
|x: i32| x % 3 == 0
}
For more information on impl Trait
syntax, see the Rust book.
A trait
is a collection of methods defined for an unknown type: Self
. They can access other methods declared in the same trait.
Traits can be implemented for any data type. In the example below, we define Animal
, a group of methods. The Animal
trait
is then implemented for the Sheep
data type, allowing the use of methods from Animal
with a Sheep
.
struct Sheep { naked: bool, name: &'static str }
trait Animal {
// Associated function signature; `Self` refers to the implementor type.
fn new(name: &'static str) -> Self;
// Method signatures; these will return a string.
fn name(&self) -> &'static str;
fn noise(&self) -> &'static str;
// Traits can provide default method definitions.
fn talk(&self) {
println!("{} says {}", self.name(), self.noise());
}
}
impl Sheep {
fn is_naked(&self) -> bool {
self.naked
}
fn shear(&mut self) {
if self.is_naked() {
// Implementor methods can use the implementor's trait methods.
println!("{} is already naked...", self.name());
} else {
println!("{} gets a haircut!", self.name);
self.naked = true;
}
}
}
// Implement the `Animal` trait for `Sheep`.
impl Animal for Sheep {
// `Self` is the implementor type: `Sheep`.
fn new(name: &'static str) -> Sheep {
Sheep { name: name, naked: false }
}
fn name(&self) -> &'static str {
self.name
}
fn noise(&self) -> &'static str {
if self.is_naked() {
"baaaaah?"
} else {
"baaaaah!"
}
}
// Default trait methods can be overridden.
fn talk(&self) {
// For example, we can add some quiet contemplation.
println!("{} pauses briefly... {}", self.name, self.noise());
}
}
fn main() {
// Type annotation is necessary in this case.
let mut dolly: Sheep = Animal::new("Dolly");
// TODO ^ Try removing the type annotations.
dolly.talk();
dolly.shear();
dolly.talk();
}
There are three types of structures ("structs") that can be created using the struct
keyword:
// An attribute to hide warnings for unused code.
#![allow(dead_code)]
#[derive(Debug)]
struct Person {
name: String,
age: u8,
}
// A unit struct
struct Unit;
// A tuple struct
struct Pair(i32, f32);
// A struct with two fields
struct Point {
x: f32,
y: f32,
}
// Structs can be reused as fields of another struct
struct Rectangle {
// A rectangle can be specified by where the top left and bottom right
// corners are in space.
top_left: Point,
bottom_right: Point,
}
fn main() {
// Create struct with field init shorthand
let name = String::from("Peter");
let age = 27;
let peter = Person { name, age };
// Print debug struct
println!("{:?}", peter);
// Instantiate a `Point`
let point: Point = Point { x: 10.3, y: 0.4 };
// Access the fields of the point
println!("point coordinates: ({}, {})", point.x, point.y);
// Make a new point by using struct update syntax to use the fields of our
// other one
let bottom_right = Point { x: 5.2, ..point };
// `bottom_right.y` will be the same as `point.y` because we used that field
// from `point`
println!("second point: ({}, {})", bottom_right.x, bottom_right.y);
// Destructure the point using a `let` binding
let Point { x: left_edge, y: top_edge } = point;
let _rectangle = Rectangle {
// struct instantiation is an expression too
top_left: Point { x: left_edge, y: top_edge },
bottom_right: bottom_right,
};
// Instantiate a unit struct
let _unit = Unit;
// Instantiate a tuple struct
let pair = Pair(1, 0.1);
// Access the fields of a tuple struct
println!("pair contains {:?} and {:?}", pair.0, pair.1);
// Destructure a tuple struct
let Pair(integer, decimal) = pair;
println!("pair contains {:?} and {:?}", integer, decimal);
}
rect_area
which calculates the area of a Rectangle
(try using nested destructuring).square
which takes a Point
and a f32
as arguments, and returns a Rectangle
with its top left corner on the point, and a width and height corresponding to the f32
.#rust