DEV Community

Tal
Tal

Posted on

Rust Generic Types in Method Definitions

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) {
        ...
    }
}
Enter fullscreen mode Exit fullscreen mode

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]> {

}
Enter fullscreen mode Exit fullscreen mode

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!");
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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!");
    }
}
Enter fullscreen mode Exit fullscreen mode

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!");
    }
}
Enter fullscreen mode Exit fullscreen mode

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> { }
Enter fullscreen mode Exit fullscreen mode

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> { }
Enter fullscreen mode Exit fullscreen mode

Neither will this:

struct Container<T, V> { }
impl Container { }
Enter fullscreen mode Exit fullscreen mode

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> { }
Enter fullscreen mode Exit fullscreen mode

As does this (2 generic args in struct definition, 1 in impl definition):

struct Container<T, V> { }
impl<T> Container<T, i32> { }
Enter fullscreen mode Exit fullscreen mode

As does this (2 generic args in struct definition, 2 in impl definition):

struct Container<T, V> { }
impl<T, V> Container<T, V> { }
Enter fullscreen mode Exit fullscreen mode

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> { }
Enter fullscreen mode Exit fullscreen mode

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> { }
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (0)