🚀
15. Iterators

Iterators

  • Iterators are provided by the standard library
  • They can be implemented for any data structures
fn main() {
    let v1 = vec![1, 2, 3];
 
    let v1_iter = v1.iter();
 
    for val in v1_iter {
        println!("Got: {}", val);
    }
}
$ cargo run
 
Got: 1
Got: 2
Got: 3
  • The iter method produces an iterator over immutable references

  • All Data Structures implement the Iterator trait, which is defined in the standard library

pub trait Iterator {
    type Item;
 
    fn next(&mut self) -> Option<Self::Item>;
}
  • Item is an associated type, which means that it's a type that's associated with the Iterator trait

  • &mut self means that the method takes a mutable reference to self, because calling next changes the internal state of the iterator

#[test]
fn iterator_demonstration() {
    let v1 = vec![1, 2, 3];
 
    let mut v1_iter = v1.iter();
 
    assert_eq!(v1_iter.next(), Some(&1));
    assert_eq!(v1_iter.next(), Some(&2));
    assert_eq!(v1_iter.next(), Some(&3));
    assert_eq!(v1_iter.next(), None);
}
$ cargo test
 
running 1 test
test iterator_demonstration ... ok
 
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
  • The iter method produces an iterator over immutable references
  • The iter_mt method produces an iterator over mutable references
  • The into_iter method takes ownership of the data and returns an iterator that takes ownership of the values

Adapter Methods

  • Adapter methods allow you to change an iterator into a different kind of iterator
#[test]
fn iterator_increment() {
    let v1 = vec![1, 2, 3];
 
    let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
 
    assert_eq!(v2, vec![2, 3, 4]);
}
$ cargo test
 
running 3 tests
test iterator_demonstration ... ok
test iterator_increment ... ok
test iterator_sum ... ok
 
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}
 
fn shoes_in_my_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}

The struct derives two traits:

PartialEq : Allows two Shoe instances to be compared using == and != . Debug : Allows the struct to be formatted using the {:?} or {:#?} formatting in debug output, which is useful for printing the struct for debugging purposes.

#[test]
fn filters_by_size() {
    let shoes = vec![
        Shoe { size: 10, style: String::from("sneaker") },
        Shoe { size: 13, style: String::from("sandal") },
        Shoe { size: 10, style: String::from("boot") },
    ];
 
    let in_my_size = shoes_in_my_size(shoes, 10);
 
    assert_eq!(
        in_my_size,
        vec![
            Shoe { size: 10, style: String::from("sneaker") },
            Shoe { size: 10, style: String::from("boot") },
        ]
    );
}
$ cargo test
 
running 4 tests
test tests::filters_by_size ... ok
test tests::iterator_demonstration ... ok
test tests::iterator_increment ... ok
test tests::iterator_sum ... ok
 
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Implementing the Iterator Trait

  • Implementing the Iterator trait on your own data structures allows you to use all the methods provided by the standard library
struct Counter {
    count: u32,
}
 
impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}
impl Iterator for Counter {
    type Item = u32;
 
    fn next(&mut self) -> Option<Self::Item> {
        self.count += 1;
 
        if self.count < 6 {
            Some(self.count)
        } else {
            None
        }
    }
}
#[test]
fn calling_next_directly() {
    let mut counter = Counter::new();
 
    assert_eq!(counter.next(), Some(1));
    assert_eq!(counter.next(), Some(2));
    assert_eq!(counter.next(), Some(3));
    assert_eq!(counter.next(), Some(4));
    assert_eq!(counter.next(), Some(5));
    assert_eq!(counter.next(), None);
}
$ cargo test
 
running 5 tests
test tests::calling_next_directly ... ok
test tests::filters_by_size ... ok
test tests::iterator_demonstration ... ok
test tests::iterator_increment ... ok
test tests::iterator_sum ... ok
 
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
#[test]
fn using_other_iterator_trait_methods() {
    let sum: u32 = Counter::new().zip(Counter::new().skip(1))
        .map(|(a, b)| a * b)
        .filter(|x| x % 3 == 0)
        .sum();
    assert_eq!(18, sum);
}
$ cargo tests
 
running 6 tests
test tests::calling_next_directly ... ok
test tests::filters_by_size ... ok
test tests::iterator_increment ... ok
test tests::iterator_demonstration ... ok
test tests::iterator_sum ... ok
test tests::using_other_iterator_trait_methods ... ok
 
test result: ok. 6 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
  • The zip method takes two iterators and creates an iterator of pairs
  • The skip method skips the first value and returns the rest
  • The map method calls a closure on each element to change it
  • The filter method uses a closure to decide which elements to keep
  • The sum method adds up all the elements of the iterator
Calculation:

    = [1 * 2] + [2 * 3] + [3 * 4] + [4 * 5]
    = 2 + 6 + 12 + 20
    Filtering out the values that are not divisible by 3 ...
    = 6 + 12
    = 40

Removing Clone Using Iterator

pub struct Config {
    pub query: String,
    pub filename: String,
    pub case_sensitive: bool,
}
 
impl Config {
    pub fn new(args: &[String]) -> Result<Config, &str> {
        if args.len() < 3 {
            return Err("Not enough arguments");
        }
 
        let query = args[1].clone();
        let filename = args[2].clone();
        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
 
        Ok(Config { query, filename, case_sensitive })
    }
}
  • Cloning string are inefficient

  • env::args() returns an iterator that produces a series of strings, where each string is a command-line argument

  • Instead of taking ownership of a String array, we can use iterator to keep ownership of the item.

let args: env::Args = env::args();
 
let config: Config = Config::new(args).unwrap_or_else(|err| {
    eprintln!("Problem parsing arguments: {}", err);
    process::exit(1);
});
  • env::Args is an iterator that produces a series of strings, where each string is a command-line argument
impl Config {
    pub fn new(mut args: env::Args) -> Result<Config, &str> {
        if args.len() < 3 {
            return Err("Not enough arguments");
        }
 
        let query = args[1].clone();
        let filename = args[2].clone();
 
        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
 
        Ok(Config { query, filename, case_sensitive })
    }
}
  • missing lifetime specifier this function's return type contains a borrowed value

  • since we want the lifetime of the result type to exist through out the program we can use 'static lifetime specifier

impl Config {
    pub fn new(mut args: env::Args) -> Result<Config, &'static str> {
 
        args.next(); // skip the path of the program
 
        let query = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a query string"),
        };
 
        let filename = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a filename"),
        };
 
        let case_sensitive = env::var("CASE_INSENSITIVE").is_err();
 
        Ok(Config { query, filename, case_sensitive })
    }
}

Using Iterator Adapter

pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
 
    let mut results = Vec::new();
 
    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }
 
    results
}
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    contents.lines()
        .filter(|line| line.contains(query))
        .collect()
}
  • The lines method returns an iterator over the lines of a string
  • The filter method adapts an iterator, creating a new iterator that only contains elements that satisfy the predicate
  • The collect method consumes the iterator and collects the resulting values into a collection data type

Loops vs Iterators

  • Loops and Iterator have pretty much the same performance, because of the Zero Cost Abstraction principle ( which is using high-level abstractions without incurring runtime overhead )

© 2024 Driptanil Datta.All rights reserved

Made with Love ❤️

Last updated on Mon Oct 20 2025