Intro
If you're new to Rust, reading through the official Rust book (which you definitely should), you may have been confused by the examples in the In Method Definitions section of Chapter 10.1 like I was. In this article, I'll try to clear up any confusion you may have.
For this article to make sense, you are expected to have already read through the book up to that part, and have a basic understanding of generic data types.
I think that most of the confusion stems from the fact that the topic is introduced using a common, but complex example. I submitted a ticket in the github repo for the book with some suggestions to fix this, but until then, lets explore the topic here.
Simple Example
We'll start simple. If you have a struct
, and you want to add methods to it, you would do something like this:
struct Container {
field: i32,
}
impl Container {
fn foo(&self) {
...
}
}
If you've read the book up to Chapter 10.1, this should make sense to you.
Simple Example Using Generic Type
If your struct
contains a generic type on the other hand, your impl
MUST also specify a type. Ex:
struct Container<T> {
field: T,
}
impl Container<[TYPE]> {
}
where [TYPE]
is the type that your impl
block will be implemented for.
For example, if you were to do this:
struct Container<T> {
field: T,
}
impl Container<i32> {
fn foo(&self) {
println!("I exist!");
}
}
That means that you can create an instance of your struct
using any type, BUT, only instances that use the i32
type will have the foo
method.
If we were to create a few instances, we can confirm this:
let inst1 = Container { field: 'c' };
let inst2 = Container { field: 10 };
let inst3 = Container { field: 12.2 };
inst1.foo(); // Won't work - method doesn't exist
inst2.foo(); // Works fine
inst3.foo(); // Won't work - method doesn't exist
These 3 instances are of the same struct
, but only inst2
has i32
as the generic type being passed into it when the instance is created, so it is the only instance that will have the foo
method.
Another way to say this is that the [TYPE]
from above is a matcher. All the methods defined in the impl
block will only be attached to instances that match that type.
If this makes sense to you - great! Read on. If not, read it again, because understanding the idea above is the key to understanding the example in the book.
Simple Example Using Custom Generic Type
Because rust allows you to define your own types, like a struct
or an enum
, the code above is almost exactly the same as the following:
struct Color {
red: u8,
green: u8,
blue: u8,
}
struct Container<T> {
field: T,
}
impl Container<Color> {
fn foo(&self) {
println!("I exist");
}
}
fn main() {
let inst1 = Container {
field: Color { red: 10, green: 20, blue: 30 },
};
let inst2 = Container { field: 10 };
let inst3 = Container { field: 12.2 };
inst1.foo(); // This will work
inst2.foo(); // Won't work - method doesn't exist
inst3.foo(); // Won't work - method doesn't exist
}
Here, we've defined a custom type called Color
, and our impl
block for our Container
struct says that the foo
method should only be added to instances where the T
(the generic type) is of type Color
. This is true for inst1
(where T is of type Color
), but not true for inst2
(where T is of type i32
) and inst3
(where T is of type f64
).
Common beginner mistake in impl block
Now, knowing all that, what happens if we want the methods in the impl
block to be available on all instances of our struct, no matter what type it is?
Your first thought may be to do something like this:
struct Container<T> {
field: T,
}
impl Container<T> {
fn foo(&self) {
println!("I exist!");
}
}
Unfortunately, this won't quite work the way you might expect. This is almost exactly the same as the example above that uses a custom Color
type. You are saying that you want foo
implemented only where the generic type = T
, but you have not defined type T
anywhere. The compiler doesn't know that when you wrote impl Container<T>
, what you meant was a generic type. It tries to find an actual, concrete definition somewhere in this scope for a type T
, and since that doesn't exist, the code will fail to compile.
Note that while the struct Container<T>
line does define a generic type called T
, this definition's scope is only within that struct's body. That type does NOT exist for the impl
definition to use.
impl block matching all types
What you need is a way to tell the compiler that your impl
block should match against all types. To do this, you must define a generic type within the impl
definition. This looks like the following:
struct Container<T> {
field: T,
}
impl<T> Container<T> {
fn foo(&self) {
println!("I exist!");
}
}
Here, within the impl
definition, we have defined a generic type T
, and later specified that this impl
block should match any instances that match this generic type, which, without any trait bounds (which you learn about in the following section of the book) is all types. The result is that your foo
method will be available on all instances of your struct
, no matter what your generic type T
ended up being.
Hopefully, this explains why you need to write <T>
twice within the impl
line - once to define a generic type, and once to match against it.
Rules
While working with generic types in impl
definitions, there are a few rules that are important to remember:
Rule 1
The number of matchers in your impl
block must be the same as the number of generic type args in your struct
definition
Ex:
struct Container<T, V> { }
impl Container<i32, i32> { }
Since there are 2 generic type args in the struct
definition, there MUST be 2 in the matcher of the impl
block.
This won't work:
struct Container<T, V> { }
impl Container<i32> { }
Neither will this:
struct Container<T, V> { }
impl Container { }
Rule 2
The number of generic args defined in the impl
block do NOT need to match the number of generic args defined in the struct
.
This works (2 generic args in struct
definition, 0 in impl
definition):
struct Container<T, V> { }
impl Container<i32, i32> { }
As does this (2 generic args in struct
definition, 1 in impl
definition):
struct Container<T, V> { }
impl<T> Container<T, i32> { }
As does this (2 generic args in struct
definition, 2 in impl
definition):
struct Container<T, V> { }
impl<T, V> Container<T, V> { }
Rule 3
The names of the generic args in the impl
block do NOT need to match the names of the generic args in the struct
.
This works fine, and does exactly the same thing as the above example:
struct Container<T, V> { }
impl<A, B> Container<A, B> { }
Matching against duplicate types
Besides matching on concrete types, you can also match against instances where both generic types are the same. Ex:
struct Container<T, V> { }
impl<X> Container<X, X> { }
Here, we've defined a single generic type in the impl
line called X
, and have stated that this impl
block will only match against instances where both the generic args are of the same type, no matter what that type happens to be. Again - this doesn't mean you can't create an instance where T
and V
are different types - you can - but the methods in this impl
block will not be available on those instances.
Conclusion
The official Rust book is amazing, and introduces a fairly complex language in a very approachable way. This topic is one of the few where the book falls a little short - at least in my opinion. I'm sure that with time, the book will expand on this topic, but until then, I really hope that this article clears things up for people also confused after reading that particular section of the book, like I was when I first read it.
Top comments (1)
Excellent description, thank you for posting this! I was confused at this point in the book, and this cleared up my confusion perfectly!