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
Pixelis 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.
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:
for value in pixelis approximately expanded by Rust into a call toIntoIterator::into_iter(pixel).pixelis moved intointo_iter, because the method acceptsselfby value.- The method creates
[5, 15, 25]fromself.r,self.g, andself.b. .into_iter()createsstd::array::IntoIter<i8, 3>.- The loop repeatedly calls
next()on that iterator. next()returnsSome(5), thenSome(15), thenSome(25).- Finally, it returns
None, so the loop ends. - The original
pixelvariable 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, andzvalues 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.
Real Codebase Usage
In real Rust projects, types commonly provide several ways to access their contents:
IntoIterator for Pixelfor consuming the value withfor value in pixel.IntoIterator for &Pixelfor read-only traversal withfor value in &pixel.IntoIterator for &mut Pixelwhen 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
| Approach | Loop syntax | Item type | Does it consume Pixel? | Best use |
|---|---|---|---|---|
IntoIterator for Pixel | for x in pixel | i8 | Yes | The caller no longer needs the pixel. |
IntoIterator for &Pixel | for x in &pixel | &i8 | No | Read values while retaining the pixel. |
IntoIterator for &mut Pixel | for x in &mut pixel | &mut i8 | No | Change every channel in place. |
channels() method | for x in pixel.channels() | Chosen by the method | Depends on design | Make the API's intent explicit. |
Iterator and IntoIterator have different responsibilities:
| Trait | Implemented by | Main method | Purpose |
|---|---|---|---|
Iterator | The iterator object | next(&mut self) | Produces items one at a time. |
IntoIterator | The iterable value | into_iter(self) | Creates an iterator object. |
For this Pixel, std::array::IntoIter is the iterator object. The Pixel simply creates it.
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
IntoItermust be a concrete type that implementsIterator.Itemmust 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
selfvalue 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>.
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.
Keep learning
Related questions
Absolute Values in Rust: Using the abs() Method
Learn how to calculate absolute values in modern Rust with abs(), handle signed integer overflow, and choose safer alternatives.
Accepting Iterators in Rust Functions with impl Iterator
Learn how to write Rust functions that accept HashMap iterators using generic bounds or impl Iterator, avoiding trait object and lifetime errors.
Access Enum Variant Values in Rust with Pattern Matching
Learn how to extract and print values stored in Rust enum variants using match and if let pattern matching, with practical examples.