In this exercise we're going to define methods for a struct, define and implement a trait, and look into how to make structs and traits generic.
You will learn how to:
- implement methods for a
struct
- when to use
Self
,self
,&self
and&mut self
in methods - define a trait with required methods
- make a type generic over
T
- how to constrain
T
You can find a complete solution
-
Make a new library project called
shapes
-
Make two structs,
Circle
with fieldradius
andSquare
with fieldside
to use as types. Decide on appropriate types forradius
andside
. -
Make an
impl
block and implement the following methods for each type. Consider when to useself
,&self
,&mut self
andSelf
.-
fn new(...) -> ...
- creates an instance of the shape with a certain size (radius or side length).
-
fn area(...) -> ...
- calculates the area of the shape.
-
fn scale(...)
- changes the size of an instance of the shape.
-
fn destroy(...) -> ...
- destroys the instance of a shape and returns the value of its field.
-
You can find a complete solution
- Define a Trait
HasArea
with a mandatory method:fn area(&self) -> f32
. - Implement
HasArea
forSquare
andCircle
. You can defer to the existing method but may need to cast the return type. - Abstract over
Circle
andSquare
by defining an enumShape
that contains both as variants. - Implement
HasArea
forShape
.
You can find a complete solution
We want to make Square
and Circle
generic over T
, so we can use other numeric types and not just u32
and f32
.
-
Add the generic type parameter
<T>
toSquare
. You can temporarily removeenum Shape
to make this easier. -
Import the
num
crate, version 0.4.0, to use thenum::Num
trait as bound for the generic type<T>
. This assures thatT
is a numeric type, and also makes some guarantees about operations that can be performed on<T>
. -
Add a
where
clause on the methods ofSquare
, as required, e.g.:where T: num::Num
-
Depending on the operations performed in that function, you may need to add further trait bounds, such as
Copy
andstd::ops::MulAssign
. You can add them to thewhere
clause with a+
sign, likeT: num::Num + Copy
. -
Add the generic type parameter
<T>
toCircle
and then appropriatewhere
clauses. -
Re-introduce
Shape
but with the generic type parameter<T>
, and then add appropriatewhere
clauses.
This section gives partial solutions to look at or refer to.
We also recommend using the official Rust documentation to figure out unfamiliar concepts. If you ever feel completely stuck, or if you haven’t understood something specific, please hail the trainers quickly.
Create a new library Cargo project, check the build and see if it runs:
$ cargo new --lib shapes
$ cd shapes
$ cargo run
Each of your shape types (Square
, Circle
, etc.) will need some fields (or
properties) to identify its geometry. Use ///
to add documentation to
each field.
/// Describes a human individual
struct Person {
/// How old this person is
age: u8
}
Does your function need to take ownership of the shape in order to calculate its area? Or is it sufficient to merely take a read-only look at the shape for a short period of time?
You can pass arguments by reference in Rust by making your function take x: &MyShape
, and passing them with &my_shape
.
You can also associate your function with a specific type by placing it inside a block like impl MyShape { ... }
impl Pentagon {
fn area(&self) -> u32 {
// calculate the area of the pentagon here...
}
}
You can use an enum
to provide a single type that can be any of your supported shapes. If we were working with fruit, we might say:
struct Banana { ... }
struct Apple { ... }
enum Fruit {
Banana(Banana),
Apple(Apple),
}
If you wanted to count the pips in a piece of Fruit, you might just call to the num_pips()
method on the appropriate constituent fruit. This might look like:
impl Fruit {
fn num_pips(&self) -> u8 {
match self {
Fruit::Apple(apple) => apple.num_pips(),
Fruit::Banana(banana) => banana.num_pips(),
}
}
}
The f32
type also has its own module in the standard library called std::f32
. If you look at the docs, you will find a defined constant for π: std::f32::consts::PI
.
If you want to convert a Pi constant to some type T, you need a where
bound like:
where T: num::Num + From<f32>
This restricts T to values that can be converted from an f32
(or, types you can convert an f32
into). You can then call let my_pi: T = my_f32_pi.into();
to convert your f32
value into a T
value.
A trait has a name, and lists function definitions that make guarantees about the name of a method, its arguments and return types.
pub trait Color {
fn red() -> u8;
}
pub struct Square<T> {
/// The length of one side of the square
side: T,
}
impl<T> Square<T> {
// ...
}