Welcome to module 5,
a short introduction to generic type parameters. In this module, we'll first talk about why we're making this detour.
Then, we'll talk about what generic type parameters are and how to use them in definitions.
We'll
look at what calling generic code looks like and discuss the analysis
the compiler does.
Let's get started by clarifying the purpose of this detour in the middle of the unit on lifetimes.
You may be familiar with the concept of code using generic types from other programming languages.
Even if you aren't familiar,
the ideas behind generic type parameters are an extension of the idea of types in programming at all.
Understanding generic type parameters is a small mental leap to take.
In contrast,
very few programming languages have a first-class concept of lifetimes.
Understanding lifetime parameters is a larger mental leap.
However,
generic type parameters and generic lifetime parameters in Rust have some similarities,
and we hope that by relating those ideas,
the mental leaps will be smaller.
With that in mind,
let's talk about using generic type parameters in definitions.
Generic type parameters are an abstraction that allow us to write code once that can be used
many times with different types.
For instance,
consider an application that has an approval workflow of many different types of domain objects:
articles have to be approved,
new users have to be approved, and comments have to be approved.
We can define one struct that manages approval of any of these types. In the struct definition,
after we put the struct
keyword and the name of the struct, Approval,
we're going to put angle brackets.
Inside the angle brackets, we declare the name of a generic type parameter.
T is a common name for one.
Within the struct definition,
we can now use T to stand in for any concrete type we want to be able to use.
In this case,
we're going to have a field named item that has the generic type T, as well as a field named approval of type Boolean that keeps track of whether this item has been approved or not. To use generic type parameters in implementation blocks of structs,
we need to declare the generic type in angle brackets after the impl keyword.
Then, we'll immediately use the generic type, T, in the name of the type
we're implementing methods on, which in this case, is Approval of T.
Any methods, or associated functions, defined on Approval of T that want to have arguments or return types that involve the type of item, can use T because it's been declared after the impl keyword.
For example,
here's a new associated function that takes an item of type T and returns an Approval of T with approved set to false. Methods or associative functions within this impl block
don't have to use the generic type parameter if their parameters or return types don't need it.
For example,
here's a method named approve that takes a mutable reference to self,
sets the approved field to true, and returns nothing.
Say we have a requirement for a replace method that takes a different item and returns a new
Approval instance with the same approved value and the new item.
If we want to allow the replacement item to be a different type than the current item,
we need a new generic type parameter.
We've chosen
U here,
declared U within angle brackets after the method name, and then used U for the type of the other_item parameter and the return type.
Because the U type is only relevant to the replace method,
we declare U in the method's signature rather than at the start of the impl block.
If we had wanted to require the other item to be the same type as the current item,
we wouldn't declare a new parameter for the replace method.
We'd use the same T that goes with the current instance of Approval.
To round out the Approval type's functionality, we'll add a method named approved_item that takes a reference to self and returns an option of a reference to T.
We talked about the Option enum in unit 3.
Its definition also uses generics.
The Some(T) variant can hold a value of any type.
In this case,
we're making use of Option and, in place of Option's generic type T,
we're substituting a reference to Approval's
generic type T.
If the approved field on this instance is true,
we'll return the Some variant holding a reference to the item.
If the approved field is false,
we'll return the None variant.
Now the Approval struct prevents access to the inner item
unless the item has been approved.
This code by itself compiles successfully.
The compiler does type checking, keeping in mind that T will be filled in with some value and make sure that all of the code using T is consistent with itself.
Rust doesn't need to know what T will be to check internal type correctness.
Next,
let's write some code that uses this definition and substitutes concrete types for the generic type parameters.
When we write a main function that uses Approval,
we can create an approval with,
say,
an integer amount that is requested for budgeting.
After the amount is approved,
the approved_item method will return a reference to that integer amount.
We can also create an approval instance with an IP address that may or may not be allowed to access something.
When we approve the IP address,
the approved_item method returns a reference to that IP address.
This code compiles and runs, and what Rust does
behind the scenes is generate the Approval struct's code with each data type used in place of T.
Let's look at what happens if we do something that violates the generic type constraints when we use the definition. Recall that we're currently requiring the other item passed to the replace method to be the same type as the type
in the current instance.
If we try to call the replace method on the approval instance,
holding an IP address, and pass an integer instead, we'll get a compile-time error that says that there are mismatched types.
This instance uses an IP address,
but we gave the replace method an integer.
Even though the code
defined with generic type parameters compiles, the code
using the definitions doesn't conform to the constraints.
There's much more to generic type parameters that we're not going to cover in this detour.
If you're interested in topics such as how to add trait bounds to generic types,
what the differences are between generic types and associated types,
and how to specify a default concrete type
for a generic type,
check out the online Rust language documentation.
In this module, we discussed that we're taking a detour through generic type parameters to use them as a stepping stone
to better understand generic lifetime parameters.
We saw that we can declare generic type parameters within angle brackets, and then use the generic types in definitions.
The compiler verifies the internal consistency of definitions using generics. When we write code using definitions containing generic type parameters,
we substitute concrete types,
and the compiler verifies that the concrete types don't violate the constraints.
Next,
let's see how generic lifetime parameters are similar to generic type parameters.