diff options
| author | Alex Crichton <alex@alexcrichton.com> | 2015-01-02 09:22:10 -0800 |
|---|---|---|
| committer | Alex Crichton <alex@alexcrichton.com> | 2015-01-02 09:22:10 -0800 |
| commit | 5bd7a78f66e4b60d760c087964df622f79047cbb (patch) | |
| tree | 30faa81942e766705793ff4ea3bbd30108b429c3 /src/doc | |
| parent | 4b0e084aa12cbc25bb2314997b65d28fb6a31692 (diff) | |
| parent | daed54d016bd7940d11d2d5c25f7e778ab24810b (diff) | |
| download | rust-5bd7a78f66e4b60d760c087964df622f79047cbb.tar.gz rust-5bd7a78f66e4b60d760c087964df622f79047cbb.zip | |
rollup merge of #19625: mrhota/guide_traits
Nothing major. Clarification, copy-editing, typographical and grammatical consistency
Diffstat (limited to 'src/doc')
| -rw-r--r-- | src/doc/guide.md | 232 |
1 files changed, 110 insertions, 122 deletions
diff --git a/src/doc/guide.md b/src/doc/guide.md index dba05e60ed1..0a531a11a65 100644 --- a/src/doc/guide.md +++ b/src/doc/guide.md @@ -4769,13 +4769,13 @@ enum OptionalFloat64 { } ``` -This is really unfortunate. Luckily, Rust has a feature that gives us a better -way: generics. Generics are called **parametric polymorphism** in type theory, -which means that they are types or functions that have multiple forms ("poly" -is multiple, "morph" is form) over a given parameter ("parametric"). +Such repetition is unfortunate. Luckily, Rust has a feature that gives us a +better way: **generics**. Generics are called **parametric polymorphism** in +type theory, which means that they are types or functions that have multiple +forms over a given parameter ("parametric"). -Anyway, enough with type theory declarations, let's check out the generic form -of `OptionalInt`. It is actually provided by Rust itself, and looks like this: +Let's see how generics help us escape `OptionalInt`. `Option` is already +provided in Rust's standard library and looks like this: ```rust enum Option<T> { @@ -4784,25 +4784,27 @@ enum Option<T> { } ``` -The `<T>` part, which you've seen a few times before, indicates that this is -a generic data type. Inside the declaration of our enum, wherever we see a `T`, -we substitute that type for the same type used in the generic. Here's an -example of using `Option<T>`, with some extra type annotations: +The `<T>` part, which you've seen a few times before, indicates that this is a +generic data type. `T` is called a **type parameter**. When we create instances +of `Option`, we need to provide a concrete type in place of the type +parameter. For example, if we wanted something like our `OptionalInt`, we would +need to instantiate an `Option<int>`. Inside the declaration of our enum, +wherever we see a `T`, we replace it with the type specified (or inferred by the +the compiler). ```{rust} let x: Option<int> = Some(5i); ``` -In the type declaration, we say `Option<int>`. Note how similar this looks to -`Option<T>`. So, in this particular `Option`, `T` has the value of `int`. On -the right-hand side of the binding, we do make a `Some(T)`, where `T` is `5i`. -Since that's an `int`, the two sides match, and Rust is happy. If they didn't -match, we'd get an error: +In this particular `Option`, `T` has the value of `int`. On the right-hand side +of the binding, we do make a `Some(T)`, where `T` is `5i`. Since that's an +`int`, the two sides match, and Rust is happy. If they didn't match, we'd get an +error: ```{rust,ignore} let x: Option<f64> = Some(5i); -// error: mismatched types: expected `core::option::Option<f64>` -// but found `core::option::Option<int>` (expected f64 but found int) +// error: mismatched types: expected `core::option::Option<f64>`, +// found `core::option::Option<int>` (expected f64, found int) ``` That doesn't mean we can't make `Option<T>`s that hold an `f64`! They just have to @@ -4813,8 +4815,6 @@ let x: Option<int> = Some(5i); let y: Option<f64> = Some(5.0f64); ``` -This is just fine. One definition, multiple uses. - Generics don't have to only be generic over one type. Consider Rust's built-in `Result<T, E>` type: @@ -4835,20 +4835,20 @@ enum Result<H, N> { } ``` -if we wanted to. Convention says that the first generic parameter should be -`T`, for 'type,' and that we use `E` for 'error'. Rust doesn't care, however. +Convention says that the first generic parameter should be `T`, for "type," and +that we use `E` for "error." -The `Result<T, E>` type is intended to -be used to return the result of a computation, and to have the ability to -return an error if it didn't work out. Here's an example: +The `Result<T, E>` type is intended to be used to return the result of a +computation and to have the ability to return an error if it didn't work +out. Here's an example: ```{rust} let x: Result<f64, String> = Ok(2.3f64); let y: Result<f64, String> = Err("There was an error.".to_string()); ``` -This particular Result will return an `f64` if there's a success, and a -`String` if there's a failure. Let's write a function that uses `Result<T, E>`: +This particular `Result` will return an `f64` upon success and a `String` if +there's a failure. Let's write a function that uses `Result<T, E>`: ```{rust} fn inverse(x: f64) -> Result<f64, String> { @@ -4858,17 +4858,18 @@ fn inverse(x: f64) -> Result<f64, String> { } ``` -We don't want to take the inverse of zero, so we check to make sure that we -weren't passed zero. If we were, then we return an `Err`, with a message. If -it's okay, we return an `Ok`, with the answer. +We want to indicate that `inverse(0.0f64)` is undefined or is an erroneous usage +of the function, so we check to make sure that we weren't passed zero. If we +were, we return an `Err` with a message. If it's okay, we return an `Ok` with +the answer. Why does this matter? Well, remember how `match` does exhaustive matches? Here's how this function gets used: ```{rust} # fn inverse(x: f64) -> Result<f64, String> { -# if x == 0.0f64 { return Err("x cannot be zero!".to_string()); } -# Ok(1.0f64 / x) +# if x == 0.0f64 { return Err("x cannot be zero!".to_string()); } +# Ok(1.0f64 / x) # } let x = inverse(25.0f64); @@ -4889,8 +4890,8 @@ println!("{}", x + 2.0f64); // error: binary operation `+` cannot be applied ``` This function is great, but there's one other problem: it only works for 64 bit -floating point values. What if we wanted to handle 32 bit floating point as -well? We'd have to write this: +floating point values. If we wanted to handle 32 bit floating point values we'd +have to write this: ```{rust} fn inverse32(x: f32) -> Result<f32, String> { @@ -4900,9 +4901,9 @@ fn inverse32(x: f32) -> Result<f32, String> { } ``` -Bummer. What we need is a **generic function**. Luckily, we can write one! -However, it won't _quite_ work yet. Before we get into that, let's talk syntax. -A generic version of `inverse` would look something like this: +What we need is a **generic function**. We can do that with Rust! However, it +won't _quite_ work yet. We need to talk about syntax. A first attempt at a +generic version of `inverse` might look something like this: ```{rust,ignore} fn inverse<T>(x: T) -> Result<T, String> { @@ -4912,24 +4913,34 @@ fn inverse<T>(x: T) -> Result<T, String> { } ``` -Just like how we had `Option<T>`, we use a similar syntax for `inverse<T>`. -We can then use `T` inside the rest of the signature: `x` has type `T`, and half -of the `Result` has type `T`. However, if we try to compile that example, we'll get -an error: +Just like how we had `Option<T>`, we use a similar syntax for `inverse<T>`. We +can then use `T` inside the rest of the signature: `x` has type `T`, and half of +the `Result` has type `T`. However, if we try to compile that example, we'll get +some errors: ```text error: binary operation `==` cannot be applied to type `T` + if x == 0.0 { return Err("x cannot be zero!".to_string()); } + ^~~~~~~~ +error: mismatched types: expected `_`, found `T` (expected floating-point variable, found type parameter) + Ok(1.0 / x) + ^ +error: mismatched types: expected `core::result::Result<T, collections::string::String>`, found `core::result::Result<_, _>` (expected type parameter, found floating-point variable) + Ok(1.0 / x) + ^~~~~~~~~~~ ``` -Because `T` can be _any_ type, it may be a type that doesn't implement `==`, -and therefore, the first line would be wrong. What do we do? +The problem is that `T` is unconstrained: it can be _any_ type. It could be a +`String`, and the expression `1.0 / x` has no meaning if `x` is a `String`. It +may be a type that doesn't implement `==`, and the first line would be +wrong. What do we do? -To fix this example, we need to learn about another Rust feature: traits. +To fix this example, we need to learn about another Rust feature: **traits**. # Traits -Do you remember the `impl` keyword, used to call a function with method -syntax? +Our discussion of **traits** begins with the `impl` keyword. We used it before +to specify methods. ```{rust} struct Circle { @@ -4945,8 +4956,8 @@ impl Circle { } ``` -Traits are similar, except that we define a trait with just the method -signature, then implement the trait for that struct. Like this: +We define a trait in terms of its methods. We then `impl` a trait `for` a type +(or many types). ```{rust} struct Circle { @@ -4966,19 +4977,18 @@ impl HasArea for Circle { } ``` -As you can see, the `trait` block looks very similar to the `impl` block, -but we don't define a body, just a type signature. When we `impl` a trait, -we use `impl Trait for Item`, rather than just `impl Item`. +The `trait` block defines only type signatures. When we `impl` a trait, we use +`impl Trait for Item`, rather than just `impl Item`. -So what's the big deal? Remember the error we were getting with our generic -`inverse` function? +The first of the three errors we got with our generic `inverse` function was +this: ```text error: binary operation `==` cannot be applied to type `T` ``` -We can use traits to constrain our generics. Consider this function, which -does not compile, and gives us a similar error: +We can use traits to constrain generic type parameters. Consider this function, +which does not compile, and gives us a similar error: ```{rust,ignore} fn print_area<T>(shape: T) { @@ -4993,8 +5003,9 @@ error: type `T` does not implement any method in scope named `area` ``` Because `T` can be any type, we can't be sure that it implements the `area` -method. But we can add a **trait constraint** to our generic `T`, ensuring -that it does: +method. But we can add a **trait constraint** to our generic `T`, ensuring that +we can only compile the function if it's called with types which `impl` the +`HasArea` trait: ```{rust} # trait HasArea { @@ -5005,9 +5016,9 @@ fn print_area<T: HasArea>(shape: T) { } ``` -The syntax `<T: HasArea>` means `any type that implements the HasArea trait`. -Because traits define function type signatures, we can be sure that any type -which implements `HasArea` will have an `.area()` method. +The syntax `<T: HasArea>` means "any type that implements the HasArea trait." +Because traits define method signatures, we can be sure that any type which +implements `HasArea` will have an `area` method. Here's an extended example of how this works: @@ -5105,55 +5116,22 @@ impl HasArea for int { It is considered poor style to implement methods on such primitive types, even though it is possible. -This may seem like the Wild West, but there are two other restrictions around -implementing traits that prevent this from getting out of hand. First, traits -must be `use`d in any scope where you wish to use the trait's method. So for -example, this does not work: - -```{rust,ignore} -mod shapes { - use std::f64::consts; - - trait HasArea { - fn area(&self) -> f64; - } - - struct Circle { - x: f64, - y: f64, - radius: f64, - } - - impl HasArea for Circle { - fn area(&self) -> f64 { - consts::PI * (self.radius * self.radius) - } - } -} - -fn main() { - let c = shapes::Circle { - x: 0.0f64, - y: 0.0f64, - radius: 1.0f64, - }; +## Scoped Method Resolution and Orphan `impl`s - println!("{}", c.area()); -} -``` - -Now that we've moved the structs and traits into their own module, we get an -error: +There are two restrictions for implementing traits that prevent this from +getting out of hand. -```text -error: type `shapes::Circle` does not implement any method in scope named `area` -``` +1. **Scope-based Method Resolution**: Traits must be `use`d in any scope where + you wish to use the trait's methods +2. **No Orphan `impl`s**: Either the trait or the type you're writing the `impl` + for must be inside your crate. -If we add a `use` line right above `main` and make the right things public, -everything is fine: +If we organize our crate differently by using modules, we'll need to ensure both +of the conditions are satisfied. Don't worry, you can lean on the compiler since +it won't let you get away with violating them. ```{rust} -use shapes::HasArea; +use shapes::HasArea; // satisfies #1 mod shapes { use std::f64::consts; @@ -5175,8 +5153,8 @@ mod shapes { } } - fn main() { + // use shapes::HasArea; // This would satisfy #1, too let c = shapes::Circle { x: 0.0f64, y: 0.0f64, @@ -5187,18 +5165,25 @@ fn main() { } ``` -This means that even if someone does something bad like add methods to `int`, -it won't affect you, unless you `use` that trait. +Requiring us to `use` traits whose methods we want means that even if someone +does something bad like add methods to `int`, it won't affect us, unless you +`use` that trait. + +The second condition allows us to `impl` built-in `trait`s for types we define, +or allows us to `impl` our own `trait`s for built-in types, but restricts us +from mixing and matching third party or built-in `impl`s with third party or +built-in types. -There's one more restriction on implementing traits. Either the trait or the -type you're writing the `impl` for must be inside your crate. So, we could -implement the `HasArea` type for `int`, because `HasArea` is in our crate. But -if we tried to implement `Float`, a trait provided by Rust, for `int`, we could -not, because both the trait and the type aren't in our crate. +We could `impl` the `HasArea` trait for `int`, because `HasArea` is in our +crate. But if we tried to implement `Float`, a standard library `trait`, for +`int`, we could not, because neither the `trait` nor the `type` are in our +crate. -One last thing about traits: generic functions with a trait bound use -**monomorphization** ("mono": one, "morph": form), so they are statically -dispatched. What's that mean? Well, let's take a look at `print_area` again: +## Monomorphization + +One last thing about generics and traits: the compiler performs +**monomorphization** on generic functions so they are statically dispatched. To +see what that means, let's take a look at `print_area` again: ```{rust,ignore} fn print_area<T: HasArea>(shape: T) { @@ -5215,10 +5200,11 @@ fn main() { } ``` -When we use this trait with `Circle` and `Square`, Rust ends up generating -two different functions with the concrete type, and replacing the call sites with -calls to the concrete implementations. In other words, you get something like -this: +Because we have called `print_area` with two different types in place of its +type paramater `T`, Rust will generate two versions of the function with the +appropriate concrete types, replacing the call sites with calls to the concrete +implementations. In other words, the compiler will actually compile something +more like this: ```{rust,ignore} fn __print_area_circle(shape: Circle) { @@ -5239,10 +5225,12 @@ fn main() { } ``` -The names don't actually change to this, it's just for illustration. But -as you can see, there's no overhead of deciding which version to call here, -hence 'statically dispatched'. The downside is that we have two copies of -the same function, so our binary is a little bit larger. +These names are for illustration; the compiler will generate its own cryptic +names for internal uses. The point is that there is no runtime overhead of +deciding which version to call. The function to be called is determined +statically, at compile time. Thus, generic functions are **statically +dispatched**. The downside is that we have two similar functions, so our binary +is larger. # Threads |
