rust-book-cn/src/chZZ-generics.md
Carol (Nichols || Goulding) 6d617aba2a Change to not have parens in function names in prose
Unless we're quoting a line of code that actually does call the
function. But when we're talking about the definition, don't include the
parens.
2016-09-06 17:29:10 -04:00

5.2 KiB

Generics

We've been working with a Point struct that looks like this:

#[derive(Debug,Copy,Clone)]
struct Point {
    x: f64,
    y: f64,
}

But what if we didn't want to always use an f64 here? What about an f32 for when we need less precision? Or an i32 if we only want integer coordinates?

While our simple Point struct may be a bit too simple to bother making generic in a real application, we're going to stick with it to show you the syntax. Especially when building library code, generics allow for more code re-use, and unlock a lot of powerful techniques.

Generic data types

'Generics' let us write code that allows for several different types, while letting us have one definition. A more generic Point would look like this:

#[derive(Debug,Copy,Clone)]
struct Point<T> {
    x: T,
    y: T,
}

There are two changes here, and they both involve this new T. The first change is in the definition:

# #[derive(Debug,Copy,Clone)]
struct Point<T> {
#     x: T,
#     y: T,
# }

Our previous definition said, "We are defining a struct named Point." This definition says something slightly different: "We are defining a struct named Point with one type parameter T."

Let's talk about this term type parameter. We've already seen one other thing called a "parameter" in Rust: function parameters:

fn plus_one(x: i32) -> i32 {
    x + 1
}

Here, x is a parameter to this function. We can call this function with a different value, and x will change each time it's called:

# fn plus_one(x: i32) -> i32 {
#     x + 1
# }
let six = plus_one(5);
let eleven = plus_one(10);

In the same way, a type parameter allows us to define a data type which can be different each time we use it:

#[derive(Debug,Copy,Clone)]
struct Point<T> {
    x: T,
    y: T,
}

let integral_point = Point { x: 5, y: 5 };
let floating_point = Point { x: 5.0, y: 5.0 };

Here, integral_point uses i32 values for T, and floating_point uses f64 values. This also leads us to talk about the second change we made to Point:

# #[derive(Debug,Copy,Clone)]
# struct Point<T> {
    x: T,
    y: T,
# }

Instead of saying x: i32, we say x: T. This T is the same one that we used above in the struct declaration. Because x and y both use T, they'll be the same type. We could give them different types:

#[derive(Debug,Copy,Clone)]
struct Point<T, OtherT> {
    x: T,
    y: OtherT,
}

let different = Point { x: 5, y: 5.0 };
let same = Point { x: 5.0, y: 5.0 };

Here, instead of a single parameter, T, we have two: T and OtherT. Type parameters have the same naming convention as other types: CamelCase. However, you'll often see short, one-letter names used for types. T is very common, because it's short for "type", but you can name them something longer if you'd like. In this version of Point, we say that x has the type T, and y has the type OtherT. This lets us give them two different types, but they don't have to be.

Generic functions

Regular old functions can also take generic parameters, with a syntax that looks very similar:

fn foo<T>(x: T) {
    // ...
}

This foo function has one generic parameter, T, and takes one argument, x, which has the type T. Let's talk a little bit more about what this means.

Generic methods

We've seen how to define methods with the impl keyword. Our generic Point can have generic methods, too:

#[derive(Debug,Copy,Clone)]
struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn some_method(&self) {
        // ...
    }
}

We also need the <T> after impl. This line reads, "We will be implementing methods with one generic type parameter, T, for a type, Point, which takes one generic type T." In a sense, the impl<T> says "we will be using a type T" and the Point<T> says "that T is used for Point." In this simple case, this syntax can feel a bit redundant, but when we get into some of Rust's more advanced features later, this distinction will become more useful.

There's more to the story

This section covered the basic syntax of generics, but it's not the full story. For example, let's try to implement our foo function: we'll have it print out the value of x:

fn foo<T>(x: T) {
    println!("x is: {}", x);
}

We'll get an error:

error: the trait `core::fmt::Display` is not implemented for the type `T` [E0277]
println!("x is: {}", x);
                     ^

We can't print out x! The error messages reference something we talked about briefly before, the Display trait. In order to implement this function, we need to talk about traits. But we only need to talk about traits to implement our own generic functions; we don't need this understanding to use them. So rather than get into more details about this right now, let's talk about other useful Rust data types, and we can come back to implementing generic functions in the chapter about traits.

For now, the important bits to understand:

  • Generic type parameters are kind of like function parameters, but for types instead of values.
  • Type parameters go inside <>s and are usually named things like T.

With that, let's talk about another fundamental Rust data type: enums.