15.4Generic methods
A generic struct holds data of type T; to give it behavior, you write methods in an impl block, just like chapter 10. The only new wrinkle is that the impl itself has to be generic, and once you see why, a useful trick falls out: you can write methods that exist only for certain concrete types.
impl blocks on generic types
To add methods to Point<T>, the impl block must declare the type parameter too:
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn x(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("{}", p.x());
}5
Read the line carefully: impl<T> Point<T>. The first <T> declares a type parameter for the impl block, and Point<T> says "for Point of that type." You need both because the impl is itself generic; it provides this method for Point<T> whatever T is. The method x returns &T, a reference to the field, and works for Point<i32>, Point<String>, any Point. This is the lesson 10.5 method syntax, with <T> threaded through so the compiler knows the T in the method body is the same T as the struct's.
Inside such a method, T is still opaque: you can move it, return references to it, store it, but you can't compare or print it without a bound (the same rule as generic functions, lesson 15.2). Point::x only hands back a reference, so no bound is needed.
Methods for a specific type only
Here's the trick. An impl block doesn't have to be generic. You can write impl Point<f64> (a concrete type, no <T>), and the methods inside it exist only for Point<f64>, not for Point<i32> or any other instantiation:
impl Point<f64> {
fn distance_from_origin(&self) -> f64 {
(self.x * self.x + self.y * self.y).sqrt()
}
}
fn main() {
let float_point = Point { x: 3.0, y: 4.0 };
println!("{}", float_point.distance_from_origin());
let int_point = Point { x: 3, y: 4 };
// int_point.distance_from_origin(); // refused: only Point<f64> has it
}5
distance_from_origin calls .sqrt(), which exists for f64 but not i32, so it only makes sense for Point<f64>. By writing impl Point<f64> instead of impl<T> Point<T>, you give the method to just that type. Call it on a Point<i32> and the compiler says the method doesn't exist, because for Point<i32> it genuinely doesn't. This lets a generic type offer extra capabilities for the specific types that can support them, without forcing every instantiation to have them.
Key insight
impl<T> Point<T> adds a method to every Point; impl Point<f64> adds a method to only Point<f64>. The first is for behavior that works regardless of T; the second is for behavior that needs a specific type's capabilities (like f64's sqrt). You'll see both in the standard library: most of Vec's methods are generic over its element type, but some exist only when the elements meet certain conditions, the more general version of this same idea (which chapter 16's trait bounds make fully expressive).
Methods can have their own generics
A method can also introduce type parameters of its own, separate from the struct's. This is rarer but worth recognizing: the method's <U> is independent of the struct's <T>.
impl<T> Point<T> {
fn pair_with<U>(self, other: U) -> (T, U) {
(self.x, other)
}
}
pair_with is generic over U, a type chosen fresh at the call, distinct from the Point's T. It takes self by value (consuming the point, lesson 10.5) and returns a tuple of the point's x and the other value. You won't write these often, but they explain method signatures like Option::map that carry their own type parameters beyond the type they're defined on.
Quiz time
Question #1
Why does an impl block on a generic struct need to be written impl<T> Point<T> rather than just impl Point?
Show solution
Because Point by itself isn't a complete type; it's Point<T> for some T. The leading impl<T> declares the type parameter so the block can talk about Point<T> generically and so the T used in the method bodies refers to the struct's type parameter. impl Point alone wouldn't name the parameter.
Question #2
What's the difference between impl<T> Point<T> and impl Point<f64>?
Show solution
impl<T> Point<T> defines methods available on every Point, whatever its type. impl Point<f64> defines methods available only on Point<f64>. The concrete-type impl is how you give a method to one instantiation that needs that type's specific capabilities (like f64::sqrt), without requiring all instantiations to support it.
Question #3
You have a Wrapper<T> and want a describe method on all wrappers plus a sqrt method only on Wrapper<f64>. Sketch the two impl headers.
Show solution
impl<T> Wrapper<T> {
fn describe(&self) -> &str { "a wrapper" }
}
impl Wrapper<f64> {
fn sqrt(&self) -> f64 { self.value.sqrt() }
}
The first (impl<T>) gives describe to every Wrapper; the second (impl Wrapper<f64>) gives sqrt only to the f64 instantiation, where .sqrt() is available.
You can now write generic types and methods. There's one question left that's bothered careful readers since lesson 15.1: if one generic function serves many types, what does the compiled program actually contain? The next lesson answers it, and explains why Rust's generics are "zero-cost."