Iterators and Closures
The Iterator trait, lazy adapters such as map and filter, consumers such as sum and collect, and closures that capture their environment by reference, by mutable reference or by value.
The Iterator trait
Most loops in Rust do not count an index from 0 to the length. They
ask a value for its elements one at a time. An
iterator in Rust is any type
that implements the standard trait Iterator, whose one
required method is
fn next(&mut self) -> Option<Self::Item>:
each call returns Some(item) with the next element, or
None when there are no more. The type of the elements,
Item, is part of the trait. (Traits in general are on
Traits and Generics;
Option is on
Error Handling.)
fn main() {
let readings = vec![350, 412, 120];
let mut it = readings.iter();
println!("{:?}", it.next());
println!("{:?}", it.next());
println!("{:?}", it.next());
println!("{:?}", it.next());
}
Some(350) Some(412) Some(120) None
The iterator itself keeps track of where it is, which is why
next takes &mut self and why
it has to be declared mut. You rarely call
next yourself: a for loop does it for you,
and stops at the first None. On top of
next, the trait provides dozens of ready-made methods
(map, filter, sum,
max, zip, position …) that
work on every iterator, including ones you write yourself.
iter, iter_mut and into_iter
A collection such as Vec can hand out its elements in
three ways, and the difference is exactly the difference between
borrowing and moving from Ownership and
Borrowing and References:
| Method | Items for a Vec<u32> |
Afterward | In a for loop |
|---|---|---|---|
iter() |
&u32 (shared references) |
the vector is unchanged and still usable | for x in &v |
iter_mut() |
&mut u32 (mutable references) |
the vector is still yours, possibly changed | for x in &mut v |
into_iter() |
u32 (the values themselves) |
the vector is gone: it was moved into the iterator | for x in v |
fn main() {
let mut readings = vec![350, 412, 120];
// iter(): shared references, the vector stays usable
let total: u32 = readings.iter().sum();
println!("total {total} kWh");
// iter_mut(): mutable references, change the elements in place
for kwh in readings.iter_mut() {
*kwh += 10;
}
println!("corrected {readings:?}");
// into_iter(): the elements themselves; the vector is consumed
let labels: Vec<String> = readings
.into_iter()
.map(|kwh| format!("{kwh} kWh"))
.collect();
println!("{labels:?}");
}
total 882 kWh corrected [360, 422, 130] ["360 kWh", "422 kWh", "130 kWh"]
In the iter_mut loop, kwh is a
&mut u32, so *kwh += 10 changes the
element inside the vector. In the last statement the strings are
built from the values themselves, and the vector cannot be used any
more. That last point surprises nearly everyone once, because a
plain for loop over a vector calls
into_iter():
fn main() {
let readings = vec![350, 412, 120];
for kwh in readings {
println!("{kwh}");
}
println!("{} readings", readings.len());
}
example.rs:6:29: error[E0382]: borrow of moved value: `readings`: value borrowed here after move
The full message explains what happened and what to write:
$ rustc --edition 2024 example.rs
error[E0382]: borrow of moved value: `readings`
--> example.rs:6:29
|
2 | let readings = vec![350, 412, 120];
| -------- move occurs because `readings` has type `Vec<i32>`, which does not implement the `Copy` trait
3 | for kwh in readings {
| -------- `readings` moved due to this implicit call to `.into_iter()`
...
6 | println!("{} readings", readings.len());
| ^^^^^^^^ value borrowed here after move
|
note: the `for` loop is desugared into a call to `std::iter::IntoIterator::into_iter`, which takes ownership of the receiver `self`, which moves `readings`
--> /rustc/…/library/core/src/iter/traits/collect.rs:312:17
help: consider iterating over a slice of the `Vec<i32>`'s content to avoid moving into the `for` loop
|
3 | for kwh in &readings {
| +
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0382`.
Adapters and consumers
Iterator methods come in two kinds. An
iterator adapter takes an
iterator and returns a new iterator that changes the stream on its
way through: map transforms each item,
filter drops some, take(n) stops after
n, skip, enumerate,
zip, rev, chain and
flat_map do what their names say. A consumer (the Book
calls them consuming adapters) pulls the items through and returns a
single result: sum, count,
max, min, any,
all, find, position,
fold, last and collect.
fn main() {
let quarter_hours = [102, 98, 131, 260, 245, 110]; // Wh per quarter hour
for (i, wh) in quarter_hours.iter().enumerate().skip(2).take(2) {
println!("slot {i}: {wh} Wh");
}
let jumps: Vec<i32> = quarter_hours
.windows(2)
.map(|pair| pair[1] - pair[0])
.collect();
println!("changes: {jumps:?}");
let peak = quarter_hours.iter().max();
let busy = quarter_hours.iter().filter(|&&wh| wh > 200).count();
let any_zero = quarter_hours.iter().any(|&wh| wh == 0);
let total: i32 = quarter_hours.iter().sum();
println!("peak {peak:?}, {busy} busy slots, any zero: {any_zero}, total {total} Wh");
let newest_first: Vec<_> = quarter_hours.iter().rev().take(3).collect();
println!("newest first: {newest_first:?}");
}
slot 2: 131 Wh slot 3: 260 Wh changes: [-4, 33, 129, -15, -135] peak Some(260), 2 busy slots, any zero: false, total 946 Wh newest first: [110, 245, 260]
enumerate pairs each item with its position,
skip(2).take(2) selects slots 2 and 3, and
windows(2) (a method of slices, not of iterators)
yields overlapping pairs, so the closure can compute the change from
one quarter hour to the next. max returns an
Option because an empty sequence has no maximum. The
pattern |&&wh| in the filter looks
odd: iter() gives &i32 items and
filter lends each one to its closure as another
reference, so the pattern strips two & to get the
number.
Adapters are lazy
An adapter does no work when you call it; it only builds a new
iterator value that remembers what to do. Rust iterators are
lazy: the closures inside
map and filter run only when a consumer
asks for the next value, one item at a time, all the way through the
chain. An adapter chain without a consumer therefore does nothing at
all, and the compiler says so:
fn main() {
let readings = vec![350, 412, 120];
readings.iter().map(|kwh| kwh * 2700);
}
example.rs:3:5: error: unused `Map` that must be used
$ rustc --edition 2024 example.rs warning: unused `Map` that must be used --> example.rs:3:5 | 3 | readings.iter().map(|kwh| kwh * 2700); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: iterators are lazy and do nothing unless consumed = note: `#[warn(unused_must_use)]` (part of `#[warn(unused)]`) on by default help: use `let _ = ...` to ignore the resulting value | 3 | let _ = readings.iter().map(|kwh| kwh * 2700); | +++++++ warning: 1 warning emitted
Without -D warnings (which this site's examples use, to
turn warnings into errors) it is a warning, and the program runs
without ever multiplying anything. The order in which work happens
shows when the closures print what they do:
fn main() {
let readings = vec![350, 0, 412, 120];
let total: u32 = readings
.iter()
.map(|&kwh| {
println!(" map {kwh}");
kwh
})
.filter(|&kwh| {
println!(" filter {kwh}");
kwh > 0
})
.take(2)
.sum();
println!("total {total}");
}
map 350 filter 350 map 0 filter 0 map 412 filter 412 total 762
Each value travels through map and
filter before the next one starts. The 0 is dropped by
the filter, and once take(2) has passed on two values
(350 and 412) it stops asking, so the fourth reading, 120, is never
even read. That is what makes chains on very long or even endless
sequences possible: (1..).map(…).take(5) is fine,
because nothing beyond the fifth item is ever computed.
sum() asks, no closure in map or
filter runs.
collect: into a Vec, a String, a map or a Result
collect() turns an iterator into a collection, and the
type you ask for decides which one: anything that implements the
trait FromIterator. You state it on the variable (let v: Vec<u32> = …) or with the "turbofish" syntax on the call
(.collect::<Vec<_>>(), where
_ lets the compiler fill in the element type). One
target is especially useful: collecting an iterator of
Result values into a
Result<Vec<T>, E> gives
Ok with all values, or stops at the first
Err and returns it.
use std::collections::BTreeMap;
fn parse_kwh(field: &str) -> Result<u32, String> {
field
.trim()
.parse::<u32>()
.map_err(|e| format!("{field:?}: {e}"))
}
fn main() {
let good = ["350", "412", " 120"];
let bad = ["350", "41O", "120"];
// collect into a Result: Ok with every value, or the first error
let all: Result<Vec<u32>, String> = good.into_iter().map(parse_kwh).collect();
let broken: Result<Vec<u32>, String> = bad.into_iter().map(parse_kwh).collect();
println!("{all:?}");
println!("{broken:?}");
// the same values collected into a String and into a sorted map
let kwh = all.unwrap_or_default();
let sum: String = kwh.iter().map(|k| k.to_string()).collect::<Vec<_>>().join(" + ");
let by_meter: BTreeMap<&str, u32> = ["M-30007", "M-30004", "M-30012"]
.into_iter()
.zip(kwh)
.collect();
println!("{sum}");
println!("{by_meter:?}");
}
Ok([350, 412, 120])
Err("\"41O\": invalid digit found in string")
350 + 412 + 120
{"M-30004": 412, "M-30007": 350, "M-30012": 120}
.map(parse_kwh) passes a named function where a closure
is expected; any function with the right signature works. The
BTreeMap comes out sorted by key, which keeps the
output the same on every run (a HashMap would print in
a different order each time; see
Collections and Strings).
A telegram pipeline
Iterator chains fit text processing well. The program below works on the example P1 telegram from Smart Meters and the P1 Port. In this example, a developer on Kestrel's platform team is trying out the meter-data parts of the BUG-1060 story in Rust: counting the data lines, finding the current power, and computing the CRC-16/ARC checksum. The pilot dongle's firmware itself is the manufacturer's C code; this is a prototype for comparison.
const TELEGRAM: &str = concat!(
"/XYZ5\\2DEMO-P1-METER\r\n",
"\r\n",
"1-3:0.2.8(50)\r\n",
"0-0:1.0.0(261007193015S)\r\n",
"0-0:96.1.1(44454D4F303030303030303030303137)\r\n",
"1-0:1.8.1(000412.338*kWh)\r\n",
"1-0:1.8.2(000467.905*kWh)\r\n",
"1-0:2.8.1(000061.214*kWh)\r\n",
"1-0:2.8.2(000598.770*kWh)\r\n",
"0-0:96.14.0(0002)\r\n",
"1-0:1.7.0(01.234*kW)\r\n",
"1-0:2.7.0(00.000*kW)\r\n",
"0-0:96.7.21(00002)\r\n",
"0-0:96.7.9(00001)\r\n",
"1-0:99.97.0(1)(0-0:96.7.19)(260812021511S)(0000000397*s)\r\n",
"1-0:32.32.0(00000)\r\n",
"1-0:32.36.0(00000)\r\n",
"0-0:96.13.0()\r\n",
"1-0:32.7.0(231.4*V)\r\n",
"1-0:31.7.0(005*A)\r\n",
"1-0:21.7.0(01.234*kW)\r\n",
"1-0:22.7.0(00.000*kW)\r\n",
"0-1:24.1.0(003)\r\n",
"0-1:96.1.0(44454D4F474153303030303030303137)\r\n",
"0-1:24.2.1(261007192500S)(00092.418*m3)\r\n",
"!F1F4\r\n",
);
/// "1-0:1.7.0(01.234*kW)" -> Some(1234) watts; any other line -> None
fn power_watts(line: &str) -> Option<u32> {
let value = line.strip_prefix("1-0:1.7.0(")?.strip_suffix("*kW)")?;
let (kw, frac) = value.split_once('.')?;
if frac.len() != 3 {
return None;
}
Some(kw.parse::<u32>().ok()? * 1000 + frac.parse::<u32>().ok()?)
}
/// CRC-16/ARC as a fold: start at 0, combine every byte into the running value.
fn crc16_arc(data: &[u8]) -> u16 {
data.iter().fold(0u16, |crc, &byte| {
(0..8).fold(crc ^ u16::from(byte), |crc, _| {
if crc & 1 != 0 { (crc >> 1) ^ 0xA001 } else { crc >> 1 }
})
})
}
fn main() {
let lines = TELEGRAM.lines().count();
let obis_lines = TELEGRAM.lines().filter(|line| line.contains(':')).count();
println!("{lines} lines, {obis_lines} with an OBIS code");
println!("power: {:?} W", TELEGRAM.lines().find_map(power_watts));
println!("check value: {:04X}", crc16_arc(b"123456789"));
let end = TELEGRAM.find('!').expect("telegram has an end marker") + 1;
println!("telegram CRC: {:04X}", crc16_arc(&TELEGRAM.as_bytes()[..end]));
}
26 lines, 23 with an OBIS code power: Some(1234) W check value: BB3D telegram CRC: F1F4
Three iterator ideas are at work here:
-
lines()on a string slice is an iterator over its lines (it removes the\r\nendings), andfilter(…).count()counts the matching ones without building a list. -
find_map(power_watts)calls the function on each line and stops at the first one that returnsSome: the power value 01.234 kW becomes 1234 W, the same result as on the IoT page. The?insidepower_wattsturns every line that does not match intoNone. -
foldcarries a running value through the sequence: it starts with0u16and combines each byte into it. The CRC is a fold inside a fold (eight bit steps per byte). It gives BB3D for the standard check string and F1F4 for the telegram, the value on its last line. A plainforloop, as in the C version on the IoT page, is just as good; pick whichever reads better to you.
u16::from(byte) widens the byte without a cast, and the
shifts and XORs stay in u16: there is no
integer promotion
as in C to keep in mind.
Closures
A closure is an anonymous
function you can store in a variable or pass to another function,
and that can use variables from the scope where it is written. Its
parameters go between vertical bars, and the body follows:
|kwh| kwh * price / 100. When the body is more than one
expression it gets braces. Parameter and return types are usually
inferred from how the closure is used, though you may write them.
fn main() {
let price = 2700; // ten-thousandths of a euro per kWh: €0.2700
let cost_cents = |kwh: u64| kwh * price / 100;
let label = |cents: u64| format!("€{}.{:02}", cents / 100, cents % 100);
println!("{}", label(cost_cents(350)));
println!("{}", label(cost_cents(412)));
}
€94.50 €111.24
cost_cents uses price, a local variable of
main: the closure captures it. Money is kept
as whole numbers (ten-thousandths of a euro per kWh, cents in the
result) for the reasons on
Variables, Types and Mutability; €0.2700 × 350 kWh is €94.50. An ordinary fn
cannot do this, even when it is written inside
main:
fn main() {
let price = 2700;
fn cost_cents(kwh: u64) -> u64 {
kwh * price / 100
}
println!("{}", cost_cents(350));
}
example.rs:5:15: error[E0434]: can't capture dynamic environment in a fn item
The compiler's own help reads "use the || { ... }
closure form instead". (The second error, about the unused
price, follows from the first.) A nested
fn is a normal function that happens to be declared in
a block; it only sees its own parameters and global items.
How closures capture: Fn, FnMut and FnOnce
A closure captures each variable in the least demanding way its body allows. If it only reads a variable, it borrows it with a shared reference. If it changes it, it borrows it mutably. If it moves the value out (returns it, or passes it to something that takes ownership), it takes the value itself. From that the compiler gives the closure one or more of three traits, the Fn traits:
| Trait | The closure … | Can be called |
|---|---|---|
Fn |
only reads what it captured (or captures nothing) | any number of times, even through a shared reference |
FnMut |
changes what it captured |
any number of times, but one call at a time; the variable
holding it must be mut
|
FnOnce |
moves something it captured out of itself | once |
Every closure implements FnOnce, closures that do not
move anything out also implement FnMut, and those that
do not change anything also implement Fn. So an
Fn closure can be passed wherever an
FnMut or FnOnce is asked for, but not the
other way round.
fn main() {
let meter = String::from("M-30004");
// 1. reads meter: borrows it (Fn)
let show = || println!("meter {meter}");
show();
show();
println!("main can still read {meter}");
// 2. changes count: borrows it mutably (FnMut)
let mut count = 0;
let mut tick = || count += 1;
tick();
tick();
tick();
println!("count {count}");
// 3. gives meter away: takes ownership (FnOnce)
let hand_over = || meter;
let owned = hand_over();
println!("owned {owned}");
}
meter M-30004 meter M-30004 main can still read M-30004 count 3 owned M-30004
show borrows meter only while it exists,
so main can still read the string. While
tick holds its mutable borrow of count,
main cannot touch count; the borrow ends
after the last call, so the println! is fine.
hand_over returns meter, which moves the
string out of the closure, so it is FnOnce. Calling it
twice is refused:
fn main() {
let meter = String::from("M-30004");
let hand_over = || meter;
let first = hand_over();
let second = hand_over();
println!("{first} {second}");
}
example.rs:5:18: error[E0382]: use of moved value: `hand_over`: value used here after move
$ rustc --edition 2024 example.rs error[E0382]: use of moved value: `hand_over` --> example.rs:5:18 | 4 | let first = hand_over(); | ----------- `hand_over` moved due to this call 5 | let second = hand_over(); | ^^^^^^^^^ value used here after move | note: closure cannot be invoked more than once because it moves the variable `meter` out of its environment --> example.rs:3:24 | 3 | let hand_over = || meter; | ^^^^^ note: this value implements `FnOnce`, which causes it to be moved when called --> example.rs:4:17 | 4 | let first = hand_over(); | ^^^^^^^^^ error: aborting due to 1 previous error For more information about this error, try `rustc --explain E0382`.
The second note explains the rule in plain words. In the same way,
forgetting the mut on an FnMut closure is
caught, with the fix in the help:
fn main() {
let mut count = 0;
let tick = || count += 1;
tick();
println!("{count}");
}
example.rs:4:5: error[E0596]: cannot borrow `tick` as mutable, as it is not declared as mutable: cannot borrow as mutable
Taking closures as parameters
Each closure has its own unique, unnamed type, so a function that
accepts a closure names the trait instead:
f: impl Fn(&str), or the equivalent generic form
fn run<F: Fn(&str)>(f: F) (see
Traits and Generics). Ask for
the least you need: FnOnce if you call it once,
FnMut if you call it repeatedly, Fn only
if you need to call it through a shared reference. The caller then
has the most freedom.
// Fn: may be called many times, only reads what it captured
fn for_each_home(homes: &[&str], f: impl Fn(&str)) {
for home in homes {
f(home);
}
}
// FnMut: may be called many times and may change what it captured
fn repeat(times: u32, mut f: impl FnMut()) {
for _ in 0..times {
f();
}
}
// FnOnce: called at most once, may consume what it captured
fn finish(f: impl FnOnce() -> String) -> String {
f()
}
fn main() {
let prefix = "home";
for_each_home(&["h0017", "h0042"], |id| println!("{prefix} {id}"));
let mut retries = 0;
repeat(3, || retries += 1);
println!("retries {retries}");
let report = String::from("2 homes checked");
println!("{}", finish(move || report));
}
home h0017 home h0042 retries 3 2 homes checked
The standard library is full of such parameters:
map and filter take FnMut,
Option::unwrap_or_else and
thread::spawn take FnOnce, and
sort_by_key and retain take
FnMut:
struct Reading {
meter: &'static str,
kwh: u32,
}
fn main() {
let mut readings = vec![
Reading { meter: "M-30007", kwh: 412 },
Reading { meter: "M-30004", kwh: 350 },
Reading { meter: "M-30012", kwh: 0 },
];
readings.retain(|r| r.kwh > 0); // keep only real readings
readings.sort_by_key(|r| r.meter); // sort by meter number
for r in &readings {
println!("{} {} kWh", r.meter, r.kwh);
}
let largest = readings.iter().max_by_key(|r| r.kwh).map(|r| r.meter);
println!("largest: {}", largest.unwrap_or("none"));
let missing = readings.iter().find(|r| r.meter == "M-30099");
let kwh = missing.map_or(0, |r| r.kwh);
println!("M-30099: {kwh} kWh");
}
M-30004 350 kWh M-30007 412 kWh largest: M-30007 M-30099: 0 kWh
move closures
Borrowing works as long as the closure is used while the borrowed
variables still exist. A closure that is returned from a function,
stored for later or handed to another thread can outlive them, and
then a borrow would dangle. A
move closure, written with the
keyword move before the bars, takes ownership of
everything it captures instead: values are moved into it, and
Copy values such as numbers are copied. Without it, the
compiler refuses the code:
fn cost_per_kwh(price: u64) -> impl Fn(u64) -> u64 {
|kwh| kwh * price / 100
}
fn main() {
let fixed = cost_per_kwh(2700);
println!("{}", fixed(350));
}
example.rs:2:5: error[E0373]: closure may outlive the current function, but it borrows `price`, which is owned by the current function: may outlive borrowed value `price`
$ rustc --edition 2024 example.rs error[E0373]: closure may outlive the current function, but it borrows `price`, which is owned by the current function --> example.rs:2:5 | 2 | |kwh| kwh * price / 100 | ^^^^^ ----- `price` is borrowed here | | | may outlive borrowed value `price` | note: closure is returned here --> example.rs:2:5 | 2 | |kwh| kwh * price / 100 | ^^^^^^^^^^^^^^^^^^^^^^^ help: to force the closure to take ownership of `price` (and any other referenced variables), use the `move` keyword | 2 | move |kwh| kwh * price / 100 | ++++ error: aborting due to 1 previous error For more information about this error, try `rustc --explain E0373`.
price is a parameter: it lives on the stack frame of
cost_per_kwh, which is gone once the function returns.
With move, the closure keeps its own copy. Because each
closure has its own type, storing different closures together, for
example one per tariff in a map, needs a
Box<dyn Fn(u64) -> u64>: a pointer to a
closure on the heap plus the information to call it (see
Smart Pointers and trait objects
on Traits and Generics).
use std::collections::BTreeMap;
// Returns a closure that owns its own copy of `price`.
fn cost_per_kwh(price: u64) -> impl Fn(u64) -> u64 {
move |kwh| kwh * price / 100
}
fn main() {
let fixed = cost_per_kwh(2700);
println!("350 kWh fixed: {} cents", fixed(350));
// Different closures have different types; Box<dyn Fn> stores them side by side.
let mut tariffs: BTreeMap<&str, Box<dyn Fn(u64) -> u64>> = BTreeMap::new();
tariffs.insert("fixed", Box::new(cost_per_kwh(2700)));
tariffs.insert("variable", Box::new(|kwh| kwh * 2600 / 100 + 500));
for (name, cost) in &tariffs {
println!("{name}: {} cents", cost(350));
}
}
350 kWh fixed: 9450 cents fixed: 9450 cents variable: 9600 cents
The prices of €0.2700 (fixed) and €0.2600 (variable) per kWh are
Kestrel's tariffs; the second closure's monthly fee of 500 cents is
an example value. move does not change the closure's Fn
trait: a move closure that only reads its own copy is
still Fn. It only changes how the variables get into
the closure.
Writing your own iterator
Any type becomes an iterator by implementing next and
naming its item type. All the adapters and consumers then come for
free. This one yields the 96 quarter hours of a day, the unit in
which meter readings are usually stored:
/// The quarter hours of one day: "00:00", "00:15", … "23:45".
struct QuarterHours {
next_minute: u32,
}
impl Iterator for QuarterHours {
type Item = String;
fn next(&mut self) -> Option<String> {
if self.next_minute >= 24 * 60 {
return None;
}
let m = self.next_minute;
self.next_minute += 15;
Some(format!("{:02}:{:02}", m / 60, m % 60))
}
}
fn day() -> QuarterHours {
QuarterHours { next_minute: 0 }
}
fn main() {
println!("{}", day().count());
println!("{:?}", day().take(3).collect::<Vec<_>>());
println!("{:?}", day().last());
println!("{:?}", day().position(|t| t == "23:00"));
}
96
["00:00", "00:15", "00:30"]
Some("23:45")
Some(92)
count, take, collect,
last and position are provided methods of
the Iterator trait; the struct only says how to get the
next item. (A real day has 92 or 100 quarter hours on the days
daylight saving time starts or ends. Time zones are left out here.)
Each call to day() creates a fresh iterator; once an
iterator returns None, it is used up, and you create a
new one to start over.
Loops, iterators and speed
Readers coming from C often write
for i in 0..v.len() and index with v[i].
That works, but every v[i] is bounds-checked at run
time, and an off-by-one error becomes a panic. An iterator cannot
run past the end, and Clippy, Rust's linter, points the index loop
out:
$ clippy-driver --edition 2024 example.rs
warning: the loop variable `i` is only used to index `readings`
--> example.rs:3:14
|
3 | for i in 0..readings.len() {
| ^^^^^^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.99.0/index.html#needless_range_loop
= note: `#[warn(clippy::needless_range_loop)]` on by default
help: consider using an iterator
|
3 - for i in 0..readings.len() {
3 + for <item> in &readings {
|
warning: 1 warning emitted
Iterators are not slower for being higher level. The Rust Book calls
them one of Rust's zero-cost abstractions: in an optimized build, a
chain of iter().filter().map().sum() is typically
compiled to the same kind of loop you would write by hand, often
without the bounds checks that indexing needs. Choose between a loop
and a chain for readability. A long chain with several closures can
be harder to read than a plain for loop with an
if; side effects such as printing or pushing to another
list usually read better in a loop. Clippy and its other lints are
on
Testing, Docs and Tooling.