TRPL edits: traits

Let's talk about generics first, since we use traits to bound them
in funtions.

Partially addresses #24325

Fixes #24271
This commit is contained in:
Steve Klabnik 2015-04-18 17:21:26 -04:00
parent 1646ebd5ba
commit e289b689d4
2 changed files with 101 additions and 189 deletions

View File

@ -34,10 +34,10 @@
* [Method Syntax](method-syntax.md)
* [Vectors](vectors.md)
* [Strings](strings.md)
* [Generics](generics.md)
* [Traits](traits.md)
* [Operators and Overloading](operators-and-overloading.md)
* [Drop](drop.md)
* [Generics](generics.md)
* [if let](if-let.md)
* [Trait Objects](trait-objects.md)
* [Closures](closures.md)

View File

@ -1,10 +1,9 @@
% Traits
Do you remember the `impl` keyword, used to call a function with method
syntax?
Do you remember the `impl` keyword, used to call a function with [method
syntax][methodsyntax]?
```{rust}
# #![feature(core)]
```rust
struct Circle {
x: f64,
y: f64,
@ -18,11 +17,12 @@ impl Circle {
}
```
[methodsyntax]: method-syntax.html
Traits are similar, except that we define a trait with just the method
signature, then implement the trait for that struct. Like this:
```{rust}
# #![feature(core)]
```rust
struct Circle {
x: f64,
y: f64,
@ -41,20 +41,13 @@ 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,
but we dont define a body, just a type signature. 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?
```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:
```{rust,ignore}
```rust,ignore
fn print_area<T>(shape: T) {
println!("This shape has an area of {}", shape.area());
}
@ -66,11 +59,11 @@ Rust complains:
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
Because `T` can be any type, we cant be sure that it implements the `area`
method. But we can add a trait constraint to our generic `T`, ensuring
that it does:
```{rust}
```rust
# trait HasArea {
# fn area(&self) -> f64;
# }
@ -83,10 +76,9 @@ 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.
Here's an extended example of how this works:
Heres an extended example of how this works:
```{rust}
# #![feature(core)]
```rust
trait HasArea {
fn area(&self) -> f64;
}
@ -144,10 +136,10 @@ This shape has an area of 3.141593
This shape has an area of 1
```
As you can see, `print_area` is now generic, but also ensures that we
have passed in the correct types. If we pass in an incorrect type:
As you can see, `print_area` is now generic, but also ensures that we have
passed in the correct types. If we pass in an incorrect type:
```{rust,ignore}
```rust,ignore
print_area(5);
```
@ -157,11 +149,11 @@ We get a compile-time error:
error: failed to find an implementation of trait main::HasArea for int
```
So far, we've only added trait implementations to structs, but you can
implement a trait for any type. So technically, we _could_ implement
`HasArea` for `i32`:
So far, weve only added trait implementations to structs, but you can
implement a trait for any type. So technically, we _could_ implement `HasArea`
for `i32`:
```{rust}
```rust
trait HasArea {
fn area(&self) -> f64;
}
@ -181,102 +173,57 @@ 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:
implementing traits that prevent this from getting out of hand. The first is
that if the trait isnt defined in your scope, it doesnt apply. Heres an
example: the standard library provides a [`Write`][write] trait which adds
extra functionality to `File`s, for doing file I/O. By default, a `File`
wont have its methods:
```{rust,ignore}
mod shapes {
use std::f64::consts;
[write]: ../std/io/trait.Write.html
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,
};
println!("{}", c.area());
}
```rust,ignore
let mut f = std::fs::File::open("foo.txt").ok().expect("Couldnt open foo.txt");
let result = f.write("whatever".as_bytes());
# result.unwrap(); // ignore the erorr
```
Now that we've moved the structs and traits into their own module, we get an
error:
Heres the error:
```text
error: type `shapes::Circle` does not implement any method in scope named `area`
error: type `std::fs::File` does not implement any method in scope named `write`
let result = f.write(b”whatever”);
^~~~~~~~~~~~~~~~~~
```
If we add a `use` line right above `main` and make the right things public,
everything is fine:
We need to `use` the `Write` trait first:
```{rust}
# #![feature(core)]
mod shapes {
use std::f64::consts;
```rust,ignore
use std::io::Write;
pub trait HasArea {
fn area(&self) -> f64;
}
pub struct Circle {
pub x: f64,
pub y: f64,
pub radius: f64,
}
impl HasArea for Circle {
fn area(&self) -> f64 {
consts::PI * (self.radius * self.radius)
}
}
}
use shapes::HasArea;
fn main() {
let c = shapes::Circle {
x: 0.0f64,
y: 0.0f64,
radius: 1.0f64,
};
println!("{}", c.area());
}
let mut f = std::fs::File::open("foo.txt").ok().expect("Couldnt open foo.txt");
let result = f.write("whatever".as_bytes());
# result.unwrap(); // ignore the erorr
```
This will compile without error.
This means that even if someone does something bad like add methods to `int`,
it won't affect you, unless you `use` that trait.
it wont affect you, unless you `use` that trait.
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 `i32`, because `HasArea` is in our crate. But
Theres one more restriction on implementing traits. Either the trait or the
type youre writing the `impl` for must be defined by you. So, we could
implement the `HasArea` type for `i32`, because `HasArea` is in our code. But
if we tried to implement `Float`, a trait provided by Rust, for `i32`, we could
not, because both the trait and the type aren't in our crate.
not, because neither the trait nor the type are in our code.
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? Check out the chapter on [trait
objects](trait-objects.html) for more.
monomorphization (mono: one, morph: form), so they are statically dispatched.
Whats that mean? Check out the chapter on [trait objects][to] for more details.
## Multiple trait bounds
[to]: trait-objects.html
# Multiple trait bounds
Youve seen that you can bound a generic type parameter with a trait:
@ -299,10 +246,10 @@ fn foo<T: Clone + Debug>(x: T) {
`T` now needs to be both `Clone` as well as `Debug`.
## Where clause
# Where clause
Writing functions with only a few generic types and a small number of trait
bounds isn't too bad, but as the number increases, the syntax gets increasingly
bounds isnt too bad, but as the number increases, the syntax gets increasingly
awkward:
```
@ -318,7 +265,7 @@ fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) {
The name of the function is on the far left, and the parameter list is on the
far right. The bounds are getting in the way.
Rust has a solution, and it's called a '`where` clause':
Rust has a solution, and its called a `where` clause:
```
use std::fmt::Debug;
@ -389,84 +336,9 @@ This shows off the additional feature of `where` clauses: they allow bounds
where the left-hand side is an arbitrary type (`i32` in this case), not just a
plain type parameter (like `T`).
## Our `inverse` Example
# Default methods
Back in [Generics](generics.html), we were trying to write code like this:
```{rust,ignore}
fn inverse<T>(x: T) -> Result<T, String> {
if x == 0.0 { return Err("x cannot be zero!".to_string()); }
Ok(1.0 / x)
}
```
If we try to compile it, we get this error:
```text
error: binary operation `==` cannot be applied to type `T`
```
This is because `T` is too generic: we don't know if a random `T` can be
compared. For that, we can use trait bounds. It doesn't quite work, but try
this:
```{rust,ignore}
fn inverse<T: PartialEq>(x: T) -> Result<T, String> {
if x == 0.0 { return Err("x cannot be zero!".to_string()); }
Ok(1.0 / x)
}
```
You should get this error:
```text
error: mismatched types:
expected `T`,
found `_`
(expected type parameter,
found floating-point variable)
```
So this won't work. While our `T` is `PartialEq`, we expected to have another `T`,
but instead, we found a floating-point variable. We need a different bound. `Float`
to the rescue:
```
# #![feature(std_misc)]
use std::num::Float;
fn inverse<T: Float>(x: T) -> Result<T, String> {
if x == Float::zero() { return Err("x cannot be zero!".to_string()) }
let one: T = Float::one();
Ok(one / x)
}
```
We've had to replace our generic `0.0` and `1.0` with the appropriate methods
from the `Float` trait. Both `f32` and `f64` implement `Float`, so our function
works just fine:
```
# #![feature(std_misc)]
# use std::num::Float;
# fn inverse<T: Float>(x: T) -> Result<T, String> {
# if x == Float::zero() { return Err("x cannot be zero!".to_string()) }
# let one: T = Float::one();
# Ok(one / x)
# }
println!("the inverse of {} is {:?}", 2.0f32, inverse(2.0f32));
println!("the inverse of {} is {:?}", 2.0f64, inverse(2.0f64));
println!("the inverse of {} is {:?}", 0.0f32, inverse(0.0f32));
println!("the inverse of {} is {:?}", 0.0f64, inverse(0.0f64));
```
## Default methods
There's one last feature of traits we should cover: default methods. It's
Theres one last feature of traits we should cover: default methods. Its
easiest just to show an example:
```rust
@ -477,8 +349,8 @@ trait Foo {
}
```
Implementors of the `Foo` trait need to implement `bar()`, but they don't
need to implement `baz()`. They'll get this default behavior. They can
Implementors of the `Foo` trait need to implement `bar()`, but they dont
need to implement `baz()`. Theyll get this default behavior. They can
override the default if they so choose:
```rust
@ -506,3 +378,43 @@ default.baz(); // prints "We called baz."
let over = OverrideDefault;
over.baz(); // prints "Override baz!"
```
# Inheritance
Sometimes, implementing a trait requires implementing another trait:
```rust
trait Foo {
fn foo(&self);
}
trait FooBar : Foo {
fn foobar(&self);
}
```
Implementors of `FooBar` must also implement `Foo`, like this:
```rust
# trait Foo {
# fn foo(&self);
# }
# trait FooBar : Foo {
# fn foobar(&self);
# }
struct Baz;
impl Foo for Baz {
fn foo(&self) { println!("foo"); }
}
impl FooBar for Baz {
fn foobar(&self) { println!("foobar"); }
}
```
If we forget to implement `Foo`, Rust will tell us:
```text
error: the trait `main::Foo` is not implemented for the type `main::Baz` [E0277]
```