Lesson 32 of 94
Trait Bounds & where Clauses
The previous lesson introduced bounds like T: PartialOrd. This lesson covers everything you can do with them: combining several bounds, moving them into where clauses, putting them on impl blocks so methods exist only for some types, and implementing a trait for every type that meets a bound.
| Use case | Rust |
|---|---|
| Requiring an ordering | <T: Ord> |
| Combining several bounds | <T: Copy + Ord> or a where clause |
A slice of any Shape |
&[impl Shape] or &[T] where T: Shape |
Methods that exist only for some T |
impl<T: Display> Wrapper<T> { ... } |
| Implementing a trait for every matching type | Blanket impls: impl<T: Display> MyTrait for T |
1. Several bounds with +
use std::fmt::Display;
fn announce_winner<T: Display + PartialOrd>(a: T, b: T) {
if a >= b {
println!("{a} wins over {b}");
} else {
println!("{b} wins over {a}");
}
}
fn main() {
announce_winner(42, 17);
announce_winner("zebra", "apple");
announce_winner(2.5, 9.75);
}42 wins over 17
zebra wins over apple
9.75 wins over 2.5Display is needed to print with {}, and PartialOrd to compare with >=. Each bound unlocks exactly the operations you use.
2. Printing a generic value needs Debug or Display
fn show<T>(value: T) {
println!("{:?}", value);
}
fn main() {
show(5);
}error[E0277]: `T` doesn't implement `Debug`
--> src/main.rs:2:22
|
2 | println!("{:?}", value);
| ---- ^^^^^ `T` cannot be formatted using `{:?}` because it doesn't implement `Debug`
| |
| required by this formatting parameter
|
help: consider restricting type parameter `T` with trait `Debug`
|
1 | fn show<T: std::fmt::Debug>(value: T) {
| +++++++++++++++++The fix is fn show<T: std::fmt::Debug>(value: T). The compiler's help: suggests exactly that.
3. where clauses
When bounds get long, move them after the signature into a where clause. It means exactly the same thing and reads better.
use std::fmt::{Debug, Display};
fn report<K, V>(key: K, values: &[V]) -> String
where
K: Display,
V: Debug + Clone + PartialOrd,
{
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
format!("{key}: {:?}", sorted)
}
fn main() {
println!("{}", report("latencies", &[120, 45, 300]));
println!("{}", report(2026, &["q3", "q1", "q2"]));
}latencies: [45, 120, 300]
2026: ["q1", "q2", "q3"]4. impl Trait parameters vs generic parameters
fn f(x: impl Trait) is shorthand for fn f<T: Trait>(x: T). The difference shows up with two parameters: two impl Trait parameters may be different types, while one type parameter T forces them to be the same type.
use std::fmt::Display;
fn pair_any(a: impl Display, b: impl Display) -> String {
format!("{a} & {b}")
}
fn pair_same<T: Display>(a: T, b: T) -> String {
format!("{a} & {b}")
}
fn main() {
println!("{}", pair_any(1, "one")); // i32 and &str: fine
println!("{}", pair_same("x", "y")); // both &str: fine
println!("{}", pair_same(1.5, 2.5)); // both f64: fine
}1 & one
x & y
1.5 & 2.5use std::fmt::Display;
fn pair_same<T: Display>(a: T, b: T) -> String {
format!("{a} & {b}")
}
fn main() {
println!("{}", pair_same(1, "one"));
}error[E0308]: mismatched types
--> src/main.rs:8:33
|
8 | println!("{}", pair_same(1, "one"));
| --------- - ^^^^^ expected integer, found `&str`
| | |
| | expected all arguments to be this integer type because they need to match the type of this parameter
| arguments to this function are incorrect
|
note: function defined here
--> src/main.rs:3:4
|
3 | fn pair_same<T: Display>(a: T, b: T) -> String {
| ^^^^^^^^^ - ---- ---- this parameter needs to match the integer type of `a`
| | |
| | `b` needs to match the integer type of this parameter
| `a` and `b` both reference this parameter `T`5. Bounds on a whole impl block: conditional methods
Putting a bound on the impl block makes its methods exist only for types that meet the bound. Other methods of the same type remain available for every T.
use std::fmt::Display;
struct Pair<T> {
a: T,
b: T,
}
impl<T> Pair<T> {
fn new(a: T, b: T) -> Self {
Self { a, b }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn show_larger(&self) {
if self.a >= self.b {
println!("larger: {}", self.a);
} else {
println!("larger: {}", self.b);
}
}
}
struct Opaque;
fn main() {
Pair::new(3, 8).show_larger();
Pair::new("kiwi", "fig").show_larger();
let _p = Pair::new(Opaque, Opaque); // allowed: `new` has no bound
// _p.show_larger(); // does not compile: Opaque is not Display + PartialOrd
}larger: 8
larger: kiwi6. Calling a conditional method on the wrong type
use std::fmt::Display;
struct Pair<T> {
a: T,
b: T,
}
impl<T: Display + PartialOrd> Pair<T> {
fn show_larger(&self) {
println!("{}", if self.a >= self.b { &self.a } else { &self.b });
}
}
struct Opaque;
fn main() {
let p = Pair { a: Opaque, b: Opaque };
p.show_larger();
}error[E0599]: the method `show_larger` exists for struct `Pair<Opaque>`, but its trait bounds were not satisfied
--> src/main.rs:18:7
|
3 | struct Pair<T> {
| -------------- method `show_larger` not found for this struct
...
14 | struct Opaque;
| ------------- doesn't satisfy `Opaque: PartialOrd` or `Opaque: std::fmt::Display`
...
18 | p.show_larger();
| ^^^^^^^^^^^ method cannot be called on `Pair<Opaque>` due to unsatisfied trait bounds
|
note: the following trait bounds were not satisfied:
`Opaque: PartialOrd`
`Opaque: std::fmt::Display`
--> src/main.rs:8:9
|
8 | impl<T: Display + PartialOrd> Pair<T> {
| ^^^^^^^ ^^^^^^^^^^ -------
| | |
| | unsatisfied trait bound introduced here
| unsatisfied trait bound introduced here
note: the trait `std::fmt::Display` must be implemented
--> /rustc/<version>/library/core/src/fmt/mod.rs:1186:0
help: consider annotating `Opaque` with `#[derive(PartialEq, PartialOrd)]`
|
14 + #[derive(PartialEq, PartialOrd)]
15 | struct Opaque;
|7. Blanket implementations
You can implement a trait for every type that satisfies a bound. The standard library does this everywhere; for example, every type that implements Display automatically gets ToString.
use std::fmt::Display;
trait Loud {
fn loud(&self) -> String;
}
impl<T: Display> Loud for T {
fn loud(&self) -> String {
format!("{}!!!", self.to_string().to_uppercase())
}
}
fn main() {
println!("{}", "hello".loud());
println!("{}", 42.loud());
println!("{}", 'x'.loud());
println!("{}", 3.5.loud());
}HELLO!!!
42!!!
X!!!
3.5!!!One impl added the method to &str, i32, char, f64, and every other printable type, including types defined later by other code.
8. A generic function over your own trait
trait Area {
fn area(&self) -> f64;
}
struct Square(f64);
struct Circle(f64);
impl Area for Square {
fn area(&self) -> f64 {
self.0 * self.0
}
}
impl Area for Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * self.0 * self.0
}
}
fn total_area<T: Area>(shapes: &[T]) -> f64 {
let mut total = 0.0;
for s in shapes {
total += s.area();
}
total
}
fn main() {
let tiles = [Square(1.0), Square(2.0), Square(0.5)];
let coins = [Circle(1.0), Circle(1.0)];
println!("{:.2}", total_area(&tiles));
println!("{:.2}", total_area(&coins));
}5.25
6.28A slice &[T] holds values of one type T, so total_area can't take squares and circles mixed together. Mixed collections need trait objects, the next lesson.
9. Generic arithmetic with std::ops
Operators are traits too: + is std::ops::Add, * is Mul, and so on. A generic sum needs to say that T can be added and has a zero value.
use std::ops::Add;
fn sum<T>(values: &[T]) -> T
where
T: Copy + Default + Add<Output = T>,
{
let mut total = T::default();
for &v in values {
total = total + v;
}
total
}
fn main() {
println!("{}", sum(&[1, 2, 3, 4]));
println!("{}", sum(&[0.5, 0.25, 0.125]));
println!("{}", sum::<u8>(&[]));
}10
0.875
0Add<Output = T> means "adding two Ts gives a T". Output is an associated type, explained in Advanced Traits.
10. Generic average with conversion
To divide by the count, the values need converting to f64. The bound Into<f64> accepts every type that converts to f64 losslessly.
fn average<T>(values: &[T]) -> Option<f64>
where
T: Copy + Into<f64>,
{
if values.is_empty() {
return None;
}
let mut total = 0.0;
for &v in values {
total += v.into();
}
Some(total / values.len() as f64)
}
fn main() {
println!("{:?}", average(&[1_u8, 2, 4]));
println!("{:?}", average(&[10_i32, -4, 3]));
println!("{:?}", average(&[0.1_f32, 0.2]));
println!("{:?}", average::<u16>(&[]));
}Some(2.3333333333333335)
Some(3.0)
Some(0.15000000223517418)
Nonei64 does not implement Into<f64>, because large i64 values can't be represented exactly. The compiler would reject average(&[1_i64]), which is exactly the kind of silent precision loss the trait bound prevents.
11. Bounds that relate two type parameters
A bound can mention another type parameter. T: PartialEq<U> means "a T can be compared with a U".
fn count_matches<T, U>(items: &[T], target: &U) -> usize
where
T: PartialEq<U>,
{
let mut n = 0;
for item in items {
if item == target {
n += 1;
}
}
n
}
fn main() {
let names = vec![String::from("ana"), String::from("ben"), String::from("ana")];
println!("{}", count_matches(&names, &"ana")); // String compared with &str
println!("{}", count_matches(&[1, 2, 1, 1], &1));
}2
312. Returning impl Trait
A function can return "some type implementing a trait" without naming it. The caller can only use the trait's methods.
use std::fmt::Display;
fn status_label(code: u16) -> impl Display {
format!("HTTP {code}")
}
fn evens(limit: u32) -> impl Iterator<Item = u32> {
(0..limit).filter(|n| n % 2 == 0)
}
fn main() {
println!("{}", status_label(404));
let v: Vec<u32> = evens(10).collect();
println!("{:?}", v);
}HTTP 404
[0, 2, 4, 6, 8]Returning iterators as impl Iterator<Item = T> is the most common use. Their real types are often long and unnameable.
13. impl Trait returns must be one type
trait Shape {
fn area(&self) -> f64;
}
struct Square(f64);
struct Circle(f64);
impl Shape for Square {
fn area(&self) -> f64 { self.0 * self.0 }
}
impl Shape for Circle {
fn area(&self) -> f64 { 3.14 * self.0 * self.0 }
}
fn make(round: bool) -> impl Shape {
if round { Circle(1.0) } else { Square(1.0) }
}
fn main() {
println!("{}", make(true).area());
}error[E0308]: `if` and `else` have incompatible types
--> src/main.rs:17:37
|
17 | if round { Circle(1.0) } else { Square(1.0) }
| ----------- ^^^^^^^^^^^ expected `Circle`, found `Square`
| |
| expected because of this
|
help: you could change the return type to be a boxed trait object
|
16 - fn make(round: bool) -> impl Shape {
16 + fn make(round: bool) -> Box<dyn Shape> {
|
help: if you change the return type to expect trait objects, box the returned expressions
|
17 | if round { Box::new(Circle(1.0)) } else { Box::new(Square(1.0)) }
| +++++++++ + +++++++++ +The fix is Box<dyn Shape>, a trait object, in the next lesson.
14. ?Sized: accepting str and slices directly
Type parameters normally must have a known size at compile time. str and [T] don't, which is why they are always used behind references. Adding ?Sized relaxes that requirement, so T can be str itself.
use std::fmt::Display;
fn bracket<T: Display + ?Sized>(value: &T) -> String {
format!("[{value}]")
}
fn main() {
let owned = String::from("owned");
println!("{}", bracket("a str literal")); // T = str
println!("{}", bracket(&owned)); // T = String
println!("{}", bracket(&42)); // T = i32
}[a str literal]
[owned]
[42]Without ?Sized, calling bracket("a str literal") would infer T = str and fail with "the size for values of type str cannot be known".
15. Generic methods in a trait
A trait method can be generic itself.
use std::fmt::Debug;
trait Logger {
fn log<T: Debug>(&self, label: &str, value: &T);
}
struct Console {
prefix: &'static str,
}
impl Logger for Console {
fn log<T: Debug>(&self, label: &str, value: &T) {
println!("{} {label} = {:?}", self.prefix, value);
}
}
fn main() {
let log = Console { prefix: "[app]" };
log.log("retries", &3);
log.log("hosts", &vec!["a", "b"]);
log.log("ratio", &Some(0.75));
}[app] retries = 3
[app] hosts = ["a", "b"]
[app] ratio = Some(0.75)Traits with generic methods can't be used as dyn Trait. The next lesson explains why.
16. Bounds on impl for a trait: implementing a trait conditionally
You can implement a trait for a generic type only when its parameter meets a bound. Here Wrapper<T> is Display only when T is.
use std::fmt;
struct Labeled<T> {
label: &'static str,
value: T,
}
impl<T: fmt::Display> fmt::Display for Labeled<T> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: {}", self.label, self.value)
}
}
fn main() {
println!("{}", Labeled { label: "cpu", value: "87%" });
println!("{}", Labeled { label: "uptime_days", value: 42 });
let hidden = Labeled { label: "raw", value: vec![1, 2] };
// println!("{hidden}"); // does not compile: Vec<i32> is not Display
println!("{} holds {} numbers", hidden.label, hidden.value.len());
}cpu: 87%
uptime_days: 42
raw holds 2 numbersThis is exactly how Vec<T> is Debug only when T: Debug, and Clone only when T: Clone.
17. Static dispatch has no runtime cost
Generic calls are resolved at compile time. Each call site calls the specialized function directly, and the optimizer can inline it.
trait Discount {
fn apply(&self, cents: u64) -> u64;
}
struct Percent(u64);
struct Flat(u64);
impl Discount for Percent {
fn apply(&self, cents: u64) -> u64 {
cents - cents * self.0 / 100
}
}
impl Discount for Flat {
fn apply(&self, cents: u64) -> u64 {
cents.saturating_sub(self.0)
}
}
fn checkout<D: Discount>(items: &[u64], discount: &D) -> u64 {
let mut total = 0;
for &i in items {
total += discount.apply(i); // a direct call to Percent::apply or Flat::apply
}
total
}
fn main() {
let cart = [1_000, 2_500, 4_000];
println!("{}", checkout(&cart, &Percent(10)));
println!("{}", checkout(&cart, &Flat(300)));
}6750
660018. Realistic example: top N of anything
fn top_n<T: Ord + Clone>(items: &[T], n: usize) -> Vec<T> {
let mut sorted = items.to_vec();
sorted.sort();
sorted.reverse();
sorted.truncate(n);
sorted
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
struct Score {
points: u32,
player: String,
}
fn main() {
println!("{:?}", top_n(&[4, 9, 1, 7, 9, 3], 3));
println!("{:?}", top_n(&["pear", "apple", "zucchini", "fig"], 2));
let scores = vec![
Score { points: 120, player: String::from("ana") },
Score { points: 340, player: String::from("ben") },
Score { points: 200, player: String::from("cy") },
];
for s in top_n(&scores, 2) {
println!("{} {}", s.player, s.points);
}
}[9, 9, 7]
["zucchini", "pear"]
ben 340
cy 20019. Realistic example: counting occurrences of any hashable value
HashMap keys must implement Hash + Eq. HashMap gets its own lesson in Collections; here it just counts.
use std::collections::HashMap;
use std::hash::Hash;
fn frequencies<T>(items: &[T]) -> Vec<(T, usize)>
where
T: Hash + Eq + Clone + Ord,
{
let mut counts: HashMap<T, usize> = HashMap::new();
for item in items {
*counts.entry(item.clone()).or_insert(0) += 1;
}
let mut result: Vec<(T, usize)> = counts.into_iter().collect();
result.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
result
}
fn main() {
let status_codes = [200, 404, 200, 500, 200, 404];
println!("{:?}", frequencies(&status_codes));
let words = ["rust", "java", "rust", "go", "java", "rust"];
println!("{:?}", frequencies(&words));
}[(200, 3), (404, 2), (500, 1)]
[("rust", 3), ("java", 2), ("go", 1)]20. Realistic example: a generic repository
A repository that works for any entity type, as long as the entity can report its ID. The requirement is expressed with a trait bound.
trait Entity {
fn id(&self) -> u64;
}
#[derive(Debug, Clone)]
struct User {
id: u64,
email: String,
}
#[derive(Debug, Clone)]
struct Product {
id: u64,
name: String,
price_cents: u32,
}
impl Entity for User {
fn id(&self) -> u64 {
self.id
}
}
impl Entity for Product {
fn id(&self) -> u64 {
self.id
}
}
struct Repository<T: Entity> {
rows: Vec<T>,
}
impl<T: Entity + Clone> Repository<T> {
fn new() -> Self {
Self { rows: Vec::new() }
}
fn save(&mut self, entity: T) {
match self.rows.iter_mut().find(|r| r.id() == entity.id()) {
Some(existing) => *existing = entity,
None => self.rows.push(entity),
}
}
fn find(&self, id: u64) -> Option<&T> {
self.rows.iter().find(|r| r.id() == id)
}
fn count(&self) -> usize {
self.rows.len()
}
}
fn main() {
let mut users = Repository::new();
users.save(User { id: 1, email: String::from("ana@example.com") });
users.save(User { id: 2, email: String::from("ben@example.com") });
users.save(User { id: 1, email: String::from("ana@new.example.com") });
println!("{} users, #1 = {:?}", users.count(), users.find(1));
let mut products = Repository::new();
products.save(Product { id: 10, name: String::from("lamp"), price_cents: 2_999 });
println!("{:?}", products.find(10).map(|p| (&p.name, p.price_cents)));
println!("{:?}", products.find(99).is_none());
}2 users, #1 = Some(User { id: 1, email: "ana@new.example.com" })
Some(("lamp", 2999))
true21. Realistic example: a table printer for any rows
use std::fmt::Display;
trait Row {
fn cells(&self) -> Vec<String>;
}
impl<A: Display, B: Display, C: Display> Row for (A, B, C) {
fn cells(&self) -> Vec<String> {
vec![self.0.to_string(), self.1.to_string(), self.2.to_string()]
}
}
fn print_table<R: Row>(headers: &[&str], rows: &[R]) {
let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
let rendered: Vec<Vec<String>> = rows.iter().map(|r| r.cells()).collect();
for row in &rendered {
for (i, cell) in row.iter().enumerate() {
widths[i] = widths[i].max(cell.len());
}
}
let line = |cells: &[String]| {
let parts: Vec<String> = cells.iter().enumerate().map(|(i, c)| format!("{:<w$}", c, w = widths[i])).collect();
println!("| {} |", parts.join(" | "));
};
line(&headers.iter().map(|h| h.to_string()).collect::<Vec<_>>());
for row in &rendered {
line(row);
}
}
fn main() {
print_table(&["service", "status", "p99 ms"], &[("auth", "ok", 42), ("payments", "degraded", 1_870), ("search", "ok", 95)]);
println!();
print_table(&["x", "y", "label"], &[(1.5, -2, 'A'), (10.25, 7, 'B')]);
}| service | status | p99 ms |
| auth | ok | 42 |
| payments | degraded | 1870 |
| search | ok | 95 |
| x | y | label |
| 1.5 | -2 | A |
| 10.25 | 7 | B |The blanket-style impl<A: Display, B: Display, C: Display> Row for (A, B, C) makes every three-element tuple of printable values a Row.
22. Realistic example: validators generic over their input
A trait can itself be generic, so one validator trait works for inputs of any type.
trait Validator<T: ?Sized> {
fn validate(&self, value: &T) -> Result<(), String>;
}
struct Range {
min: i64,
max: i64,
}
struct MaxLen(usize);
impl Validator<i64> for Range {
fn validate(&self, v: &i64) -> Result<(), String> {
if (self.min..=self.max).contains(v) { Ok(()) } else { Err(format!("{v} not in {}..={}", self.min, self.max)) }
}
}
impl Validator<str> for MaxLen {
fn validate(&self, v: &str) -> Result<(), String> {
if v.chars().count() <= self.0 { Ok(()) } else { Err(format!("'{v}' longer than {}", self.0)) }
}
}
fn check<T: ?Sized, V: Validator<T>>(label: &str, value: &T, validator: &V) {
match validator.validate(value) {
Ok(()) => println!("{label}: ok"),
Err(e) => println!("{label}: {e}"),
}
}
fn main() {
check("age", &34, &Range { min: 13, max: 120 });
check("age", &7, &Range { min: 13, max: 120 });
check("username", "ferris", &MaxLen(12));
check("username", "a_very_long_username", &MaxLen(12));
}age: ok
age: 7 not in 13..=120
username: ok
username: 'a_very_long_username' longer than 1223. Realistic example: merging two sorted lists of anything
fn merge_sorted<T: Ord + Clone>(a: &[T], b: &[T]) -> Vec<T> {
let mut out = Vec::with_capacity(a.len() + b.len());
let (mut i, mut j) = (0, 0);
while i < a.len() && j < b.len() {
if a[i] <= b[j] {
out.push(a[i].clone());
i += 1;
} else {
out.push(b[j].clone());
j += 1;
}
}
out.extend_from_slice(&a[i..]);
out.extend_from_slice(&b[j..]);
out
}
fn main() {
println!("{:?}", merge_sorted(&[1, 4, 9], &[2, 3, 10, 11]));
println!("{:?}", merge_sorted(&["ant", "cat"], &["bee", "dog", "eel"]));
}[1, 2, 3, 4, 9, 10, 11]
["ant", "bee", "cat", "dog", "eel"]24. Realistic example: a builder that accepts anything string-like
impl Into<String> parameters let callers pass &str, String, or anything else convertible, and the function stores an owned String. It's a very common pattern in Rust APIs; Conversion Traits covers Into fully.
#[derive(Debug, Default)]
struct Email {
to: Vec<String>,
subject: String,
body: String,
}
impl Email {
fn to(mut self, address: impl Into<String>) -> Self {
self.to.push(address.into());
self
}
fn subject(mut self, s: impl Into<String>) -> Self {
self.subject = s.into();
self
}
fn body(mut self, b: impl Into<String>) -> Self {
self.body = b.into();
self
}
}
fn main() {
let owner = String::from("owner@example.com");
let ticket = 4812;
let email = Email::default()
.to("ops@example.com") // &str
.to(owner) // String, moved in without a copy
.subject(format!("Ticket #{ticket} escalated"))
.body("Please take a look.");
println!("{:#?}", email);
}Email {
to: [
"ops@example.com",
"owner@example.com",
],
subject: "Ticket #4812 escalated",
body: "Please take a look.",
}25. Realistic example: a generic event bus with where clauses
use std::fmt::Debug;
trait Event: Debug {
fn topic(&self) -> &'static str;
}
#[derive(Debug)]
struct Signup {
email: String,
}
#[derive(Debug)]
struct Purchase {
cents: u64,
}
impl Event for Signup {
fn topic(&self) -> &'static str {
"users"
}
}
impl Event for Purchase {
fn topic(&self) -> &'static str {
"billing"
}
}
struct Bus {
log: Vec<String>,
}
impl Bus {
fn publish<E>(&mut self, event: E)
where
E: Event,
{
self.log.push(format!("[{}] {:?}", event.topic(), event));
}
fn publish_all<E, I>(&mut self, events: I)
where
E: Event,
I: IntoIterator<Item = E>,
{
for e in events {
self.publish(e);
}
}
}
fn main() {
let mut bus = Bus { log: Vec::new() };
bus.publish(Signup { email: String::from("ana@example.com") });
bus.publish_all(vec![Purchase { cents: 1_999 }, Purchase { cents: 450 }]);
bus.publish_all([Signup { email: String::from("ben@example.com") }]);
for line in &bus.log {
println!("{line}");
}
}[users] Signup { email: "ana@example.com" }
[billing] Purchase { cents: 1999 }
[billing] Purchase { cents: 450 }
[users] Signup { email: "ben@example.com" }trait Event: Debug means every Event must also be Debug, a supertrait, covered in Advanced Traits. I: IntoIterator<Item = E> accepts a Vec, an array, or any other iterable of events.
26. Realistic example: a generic clamp for configuration values
use std::fmt::Display;
fn clamp_setting<T>(name: &str, value: T, min: T, max: T) -> T
where
T: PartialOrd + Display + Copy,
{
if value < min {
println!("{name}={value} is below {min}, using {min}");
min
} else if value > max {
println!("{name}={value} is above {max}, using {max}");
max
} else {
value
}
}
fn main() {
let workers = clamp_setting("workers", 64_u32, 1, 32);
let ratio = clamp_setting("sample_ratio", 0.05_f64, 0.1, 1.0);
let port = clamp_setting("port", 8080_u16, 1024, 65535);
println!("workers={workers} ratio={ratio} port={port}");
}workers=64 is above 32, using 32
sample_ratio=0.05 is below 0.1, using 0.1
workers=32 ratio=0.1 port=8080Summary
- Combine bounds with
+:T: Display + PartialOrd. Move long bounds into awhereclause. impl Traitin a parameter is shorthand for a generic parameter. Twoimpl Traitparameters can be different types; oneTcan't.- Bounds on an
implblock make methods conditional. Bounds on a traitimplmake a type implement a trait conditionally. - Blanket implementations (
impl<T: Display> MyTrait for T) add a trait to every type meeting a bound. - Operators are traits in
std::ops, so generic arithmetic uses bounds likeAdd<Output = T>. ?Sizedlets a type parameter bestror[T].-> impl Traitreturns one unnamed concrete type. Returning different types needsBox<dyn Trait>.
