Question

Implementing IntoIterator and Iterator for a Rust Struct

rustiterator

Question

How can Iterator and IntoIterator be implemented for this Rust Pixel struct so that its color channels can be iterated over?

struct Pixel {
    r: i8,
    g: i8,
    b: i8,
}

For example, why does this attempted IntoIterator implementation fail with a Sized error, and what should IntoIter be instead?

impl IntoIterator for Pixel {
    type Item = i8;
    type IntoIter = Iterator<Item = Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        [&self.r, &self.g, &self.b].into_iter()
    }
}

Short Answer

You will learn the difference between Rust's Iterator and IntoIterator traits, why an associated iterator type must be a concrete sized type, and how to make a Pixel iterable by value or by reference.

Concept

IntoIterator describes a value that can produce an iterator. It is what makes this syntax possible:

for channel in pixel {
    println!("{channel}");
}

The trait has two associated types:

trait IntoIterator {
    type Item;
    type IntoIter: Iterator<Item = Self::Item>;

    fn into_iter(self) -> Self::IntoIter;
}

IntoIter must be one specific, concrete iterator type. It cannot be written as just this:

Iterator<Item = i8>

That is a trait, not a concrete type. Many different types can implement Iterator<Item = i8>, and a trait by itself does not have a compile-time size. Associated types are Sized by default, so Rust needs to know exactly which iterator struct will be returned.

For a fixed collection of three i8 values, an array is a natural representation. In modern Rust, consuming [self.r, self.g, self.b] and calling .into_iter() returns std::array::IntoIter<i8, 3>.

Usually, the data type implements IntoIterator, while a separate iterator type—such as std::array::IntoIter—implements Iterator. Pixel itself does not need to implement Iterator.

Mental Model

Think of IntoIterator as a vending machine and Iterator as the item dispenser inside it.

  • A Pixel is the vending machine: it can provide channels to iterate through.
  • An iterator is the dispenser: each call to next() provides one channel, then eventually reports that no channels remain.
  • Rust needs to know the exact model of dispenser. Saying only "an iterator" is like saying "some machine" without specifying its size or parts.

std::array::IntoIter<i8, 3> tells Rust exactly which dispenser is being returned: one that owns an array of three i8 values.

Take Quiz

Syntax and Examples

Implement owned iteration by converting the three fields into an array and using the array's iterator:

struct Pixel {
    r: i8,
    g: i8,
    b: i8,
}

impl IntoIterator for Pixel {
    type Item = i8;
    type IntoIter = std::array::IntoIter<i8, 3>;

    fn into_iter(self) -> Self::IntoIter {
        [self.r, self.g, self.b].into_iter()
    }
}

fn main() {
    let pixel = Pixel { r: 10, g: 20, b: 30 };

    for channel in pixel {
        println!("{channel}");
    }
}

Output:

10
20
30

This consumes pixel: its three i8 fields are moved into a temporary array, and the array iterator yields each value. Since i8 implements Copy, copying them would also be inexpensive, but the iteration API still takes ownership of Pixel.

For a reusable, borrowed Pixel, implement IntoIterator for a reference:

impl<'a> IntoIterator for &'a Pixel {
    type Item = &'a i8;
    type IntoIter = std::array::IntoIter<&'a i8, 3>;

    fn into_iter(self) -> Self::IntoIter {
        [&self.r, &self.g, &self.b].into_iter()
    }
}

fn main() {
    let pixel = Pixel { r: 10, g: 20, b: 30 };

    for channel in &pixel {
        println!("{channel}");
    }

    println!("red is still {}", pixel.r);
}

The reference implementation yields &i8 values and leaves the original Pixel available afterward.

Step by Step Execution

Consider this owned implementation:

impl IntoIterator for Pixel {
    type Item = i8;
    type IntoIter = std::array::IntoIter<i8, 3>;

    fn into_iter(self) -> Self::IntoIter {
        [self.r, self.g, self.b].into_iter()
    }
}

And this loop:

let pixel = Pixel { r: 5, g: 15, b: 25 };

for value in pixel {
    println!("{value}");
}

Execution proceeds as follows:

  1. for value in pixel is approximately expanded by Rust into a call to IntoIterator::into_iter(pixel).
  2. pixel is moved into into_iter, because the method accepts self by value.
  3. The method creates [5, 15, 25] from self.r, self.g, and self.b.
  4. .into_iter() creates std::array::IntoIter<i8, 3>.
  5. The loop repeatedly calls next() on that iterator.
  6. next() returns Some(5), then Some(15), then Some(25).
  7. Finally, it returns None, so the loop ends.
  8. The original pixel variable cannot be used afterward because it was consumed.

Real World Use Cases

Iterating over fields is useful when a struct represents a small fixed-size group of values:

  • Colors: process RGB, RGBA, or CMYK channel values uniformly.
  • Coordinates: iterate over x, y, and z values when calculating bounds or serializing data.
  • Small numeric vectors: apply validation, scaling, clamping, or aggregation to a fixed number of components.
  • Protocol values: validate several fixed fields against the same rule.
  • Data export: turn a domain type into a sequence for CSV rows, binary encoders, or statistics.

For example, validating an RGB pixel:

let pixel = Pixel { r: 12, g: -4, b: 100 };
let all_non_negative = pixel.into_iter().all(|channel| channel >= 0);

assert!(!all_non_negative);

For more complicated data, it is often clearer to expose an explicit method such as channels() rather than make the whole type iterable.

Take Quiz

Real Codebase Usage

In real Rust projects, types commonly provide several ways to access their contents:

  • IntoIterator for Pixel for consuming the value with for value in pixel.
  • IntoIterator for &Pixel for read-only traversal with for value in &pixel.
  • IntoIterator for &mut Pixel when callers should modify every field.
  • A named method such as channels() when iteration is a helpful capability but not the primary meaning of the type.

A practical API can support all three ownership modes:

impl<'a> IntoIterator for &'a mut Pixel {
    type Item = &'a mut i8;
    type IntoIter = std::array::IntoIter<&'a mut i8, 3>;

    fn into_iter(self) -> Self::IntoIter {
        [&mut self.r, &mut self.g, &mut self.b].into_iter()
    }
}

fn brighten(pixel: &mut Pixel, amount: i8) {
    for channel in pixel {
        *channel = channel.saturating_add(amount);
    }
}

This pattern avoids repetitive field-by-field code. Notice that a mutable iteration item is &mut i8, so it must be dereferenced with *channel before assigning to it.

For a struct with many fields, fields that can be added later, or data that is naturally variable-length, storing the values in an array or Vec is often a better design than manually building an iterator from individual fields.

Common Mistakes

Using a trait where a concrete iterator type is required

This does not work:

// Broken: `Iterator<Item = i8>` is a trait, not a concrete sized type.
type IntoIter = Iterator<Item = i8>;

Use the exact returned type instead:

type IntoIter = std::array::IntoIter<i8, 3>;

Returning references while declaring owned items

This declaration promises i8 items:

type Item = i8;

But this expression produces &i8 items:

[&self.r, &self.g, &self.b].into_iter()

The item type and returned iterator must agree. Either iterate by value:

[self.r, self.g, self.b].into_iter()

or implement IntoIterator for &Pixel and declare type Item = &'a i8.

Borrowing fields from a consumed self

In an owned into_iter(self) method, returning references to self.r, self.g, and self.b would be invalid for non-Copy fields. self is dropped when the method ends, so references to its fields would dangle. Return owned values for the owned implementation.

Implementing Iterator directly on Pixel without iteration state

An iterator needs to remember which item comes next. This cannot work correctly without extra state:

// Incomplete idea: where is the current position stored?
impl Iterator for Pixel {
    type Item = i8;

    fn next(&mut self) -> Option<Self::Item> {
        // Cannot know whether r, g, or b should be returned next.
        todo!()
    }
}

Use an array iterator, or create a separate iterator struct containing a position such as index: usize.

Forgetting that owned iteration consumes the value

let pixel = Pixel { r: 1, g: 2, b: 3 };
let values: Vec<i8> = pixel.into_iter().collect();

// println!("{}", pixel.r); // Broken: `pixel` was moved.

Use (&pixel).into_iter() or for value in &pixel when the value must remain available.

Comparisons

ApproachLoop syntaxItem typeDoes it consume Pixel?Best use
IntoIterator for Pixelfor x in pixeli8YesThe caller no longer needs the pixel.
IntoIterator for &Pixelfor x in &pixel&i8NoRead values while retaining the pixel.
IntoIterator for &mut Pixelfor x in &mut pixel&mut i8NoChange every channel in place.
channels() methodfor x in pixel.channels()Chosen by the methodDepends on designMake the API's intent explicit.

Iterator and IntoIterator have different responsibilities:

TraitImplemented byMain methodPurpose
IteratorThe iterator objectnext(&mut self)Produces items one at a time.
IntoIteratorThe iterable valueinto_iter(self)Creates an iterator object.

For this Pixel, std::array::IntoIter is the iterator object. The Pixel simply creates it.

Take Quiz

Cheat Sheet

struct Pixel {
    r: i8,
    g: i8,
    b: i8,
}

Iterate by value

impl IntoIterator for Pixel {
    type Item = i8;
    type IntoIter = std::array::IntoIter<i8, 3>;

    fn into_iter(self) -> Self::IntoIter {
        [self.r, self.g, self.b].into_iter()
    }
}
  • Usage: for value in pixel { ... }
  • Item: i8
  • Consumes pixel

Iterate by shared reference

impl<'a> IntoIterator for &'a Pixel {
    type Item = &'a i8;
    type IntoIter = std::array::IntoIter<&'a i8, 3>;

    fn into_iter(self) -> Self::IntoIter {
        [&self.r, &self.g, &self.b].into_iter()
    }
}
  • Usage: for value in &pixel { ... }
  • Item: &i8
  • Does not consume pixel

Rules

  • IntoIter must be a concrete type that implements Iterator.
  • Item must match exactly what the iterator yields.
  • Use [a, b, c].into_iter() for a fixed sequence of owned values in modern Rust.
  • Do not return references to fields of a self value that is being consumed.

FAQ

Why can't I use Iterator<Item = i8> as IntoIter?

Because it is a trait, not one concrete type. Rust must know the exact size and layout of an associated type. Use a concrete implementation such as std::array::IntoIter<i8, 3>.

Do I need to implement both Iterator and IntoIterator for Pixel?

Usually no. Implement IntoIterator for Pixel and let a dedicated iterator type implement Iterator. Here, the dedicated type is std::array::IntoIter<i8, 3>.

Why does the reference version use a lifetime such as 'a?

The yielded references must not outlive the Pixel they refer to. The lifetime connects &'a Pixel to each yielded &'a i8.

Can I use for value in pixel after implementing IntoIterator?

Yes. A for loop automatically calls IntoIterator::into_iter on its input.

What order should the fields be returned in?

Choose and document a stable order. RGB is conventionally r, then g, then b, so use [self.r, self.g, self.b].

Should I store channels in an array instead?

Often, yes. If indexed access and iteration are central to the type, struct Pixel([i8; 3]); or a named [i8; 3] field can simplify the implementation.

Can I use Box<dyn Iterator<Item = i8>>?

Yes, a boxed trait object is a concrete sized type, but it allocates and is unnecessary for this fixed-size example. Prefer std::array::IntoIter<i8, 3>.

Take Quiz

Mini Project

Description

Create a small RGB utility that supports iterating over a Pixel by value and by mutable reference. This mirrors image-processing code, where channel values are read for calculations and modified for adjustments.

Goal

Implement iterable RGB channels and brighten every channel safely without exceeding the i8 range.

Requirements

Implement IntoIterator for Pixel so it yields i8 values in red, green, blue order. Implement IntoIterator for &mut Pixel so it yields mutable channel references. Write a brighten function that increases every channel by a supplied amount. Use saturating arithmetic so values do not overflow. Demonstrate collecting channels and then modifying a second pixel.

Take Quiz

Keep learning

Related questions

View more