When your data needs a new shape
You have a Vec of user IDs from an API response. You need a HashMap to look them up by name. Or you have a list of log lines and need a single String to write to a file. Or you have a Vec<u32> and need a Vec<String> for display. Rust doesn't give you a magical to_hashmap() method on every type. Instead, it provides a universal plumbing system that works for any collection. You break the data down, reshape it, and build it back up.
This system relies on two methods: into_iter() and collect(). Together they let you convert between Vec, HashMap, String, HashSet, BTreeMap, and custom types with the same syntax. The pattern is consistent. The power comes from understanding what happens to ownership and how the compiler figures out your target type.
The Iterator plumbing
Rust collections implement the IntoIterator trait. This trait defines how a collection turns into a stream of elements. The method into_iter() consumes the collection and yields its elements. The method collect() takes an iterator and gathers the elements into a new collection.
Think of a recycling plant. into_iter() is the shredder. It takes the whole container and turns it into raw material. collect() is the molding machine. It takes the raw material and forms a new container. You can put filters, sorters, and transformers between the shredder and the mold. The pipeline looks like this:
collection.into_iter() // Shred the collection into elements
.filter(...) // Optional: keep only some elements
.map(...) // Optional: transform each element
.collect() // Mold the stream into a new collection
The collect() method is generic. It doesn't know what type of collection you want. You have to tell the compiler. The compiler uses the type annotation to find the correct implementation of the FromIterator trait. If you don't provide a type, the compiler stops and asks for clarification.
Minimal example: Vec to HashMap
Here is the standard pattern for converting a Vec into a HashMap. The into_iter() method takes ownership of the vector. The map closure transforms each element into a key-value tuple. The collect() method builds the map.
use std::collections::HashMap;
/// Converts a list of IDs into a lookup map where the value is always true.
fn ids_to_lookup(ids: Vec<u32>) -> HashMap<u32, bool> {
// into_iter consumes the Vec. We cannot use `ids` after this line.
// map transforms each u32 into a (u32, bool) tuple.
// collect builds the HashMap because of the return type annotation.
ids.into_iter()
.map(|id| (id, true))
.collect()
}
fn main() {
let ids = vec![101, 202, 303];
let lookup = ids_to_lookup(ids);
// ids is moved here. Trying to use `ids` now would cause E0382.
println!("{:?}", lookup);
}
The type annotation on the function return type drives the conversion. collect() sees that the function returns HashMap<u32, bool>. It looks for impl FromIterator<(u32, bool)> for HashMap<u32, bool>. That implementation exists. The compiler generates code to insert each tuple into the map.
If you remove the return type annotation, the compiler rejects the code with E0283 (type annotations needed). The compiler sees collect() and has no way to guess the target type. It could be a Vec, a HashSet, a String, or a custom struct. You must provide the type.
Trust the type annotation. Without it, the compiler is guessing.
The Borrowing Trap: References vs Values
The most common mistake when converting collections is confusing into_iter() with iter(). These methods yield different types. into_iter() yields owned values. iter() yields references.
If you call iter() on a Vec<T>, the iterator yields &T. If you collect those references, you get a Vec<&T>, not a Vec<T>. This is often not what you want. You end up with a collection of borrows that tie the lifetime of the new collection to the old one.
fn main() {
let numbers = vec![1, 2, 3];
// iter() yields &i32. collect() builds a Vec<&i32>.
let refs: Vec<&i32> = numbers.iter().collect();
// This compiles, but refs borrows from numbers.
// If numbers goes out of scope, refs becomes invalid.
println!("{:?}", refs);
}
If you expected a Vec<i32> but wrote iter().collect(), the compiler catches the mismatch. You get E0308 (mismatched types). The compiler expects i32 but finds &i32.
To get owned values, use into_iter(). This moves the values out of the original vector. The original vector is consumed. You cannot use it after into_iter().
fn main() {
let numbers = vec![1, 2, 3];
// into_iter() yields i32. collect() builds a Vec<i32>.
let owned: Vec<i32> = numbers.into_iter().collect();
// numbers is moved. This line would cause E0382.
// println!("{:?}", numbers);
println!("{:?}", owned);
}
If you need to keep the original collection, you have two options. Use iter().cloned().collect() to create a deep copy. Or use iter().copied().collect() if the elements implement Copy. The cloned() method calls clone() on each reference. The copied() method is a shorthand for cloned() when Copy is available.
Convention aside: copied() is preferred over cloned() for primitive types like i32, u8, or bool. It signals to readers that the operation is cheap and bitwise. cloned() implies a potentially expensive heap allocation.
Convention: Vec::from vs collect
When converting a slice to a Vec, collect() works but is not the fastest option. The Vec::from() method is optimized for this case. Vec::from(slice) allocates memory and copies the data in one step. slice.iter().cloned().collect() goes through the iterator trait overhead and may perform extra allocations if the iterator's size hint is inaccurate.
fn main() {
let slice = [1, 2, 3, 4, 5];
// Fast lane: Vec::from uses optimized allocation.
let fast: Vec<i32> = Vec::from(slice);
// Slower: iter + cloned + collect involves iterator overhead.
let slow: Vec<i32> = slice.iter().cloned().collect();
assert_eq!(fast, slow);
}
Use Vec::from(slice) when you need performance and are converting a slice to a Vec. Use collect() when you are transforming elements or filtering. The overhead of collect() is negligible for small collections or complex pipelines.
Deep Dive: String conversion and trait bounds
Converting to a String is a frequent source of confusion. String implements FromIterator for types that implement AsRef<str>. This means you can collect &str or String items directly.
fn main() {
let words = vec!["hello", "world"];
// &str implements AsRef<str>. This works.
let sentence: String = words.iter().collect();
println!("{}", sentence); // "helloworld"
}
If you try to collect numbers into a String, it fails. i32 does not implement AsRef<str>. The compiler rejects this with E0277 (the trait bound String: FromIterator<i32> is not satisfied).
You must transform the numbers into strings first. The to_string() method or format!() macro produces a String. String implements AsRef<str>, so the collection succeeds.
fn main() {
let numbers = vec![1, 2, 3];
// map converts each i32 to a String.
// collect gathers the Strings into a single String.
let result: String = numbers.iter()
.map(|n| n.to_string())
.collect();
println!("{}", result); // "123"
}
Note that collect() concatenates the strings without separators. If you need separators, use join(). The join() method exists on slices of string-like types.
fn main() {
let words = vec!["hello", "world"];
// join is a method on slices, not an iterator method.
let sentence = words.join(", ");
println!("{}", sentence); // "hello, world"
}
Advanced: Collecting Results
Iterators often yield Result<T, E> when processing fallible operations. Rust provides a special FromIterator implementation for Result. You can collect an iterator of Result<T, E> into a Result<Vec<T>, E>.
This implementation short-circuits. If any element is an Err, collect() returns that error immediately. It stops processing the rest of the iterator. This is efficient and idiomatic.
use std::num::ParseIntError;
/// Parses a list of string numbers into integers.
/// Returns an error if any string is not a valid integer.
fn parse_all(strings: Vec<String>) -> Result<Vec<i32>, ParseIntError> {
// into_iter moves the Strings.
// parse returns Result<i32, ParseIntError>.
// collect gathers Results into Result<Vec<i32>, ParseIntError>.
// If parse fails, collect returns the error and stops.
strings.into_iter()
.map(|s| s.parse::<i32>())
.collect()
}
fn main() {
let good = vec!["1".to_string(), "2".to_string()];
let bad = vec!["1".to_string(), "not_a_number".to_string()];
match parse_all(good) {
Ok(nums) => println!("Parsed: {:?}", nums),
Err(e) => println!("Error: {}", e),
}
match parse_all(bad) {
Ok(nums) => println!("Parsed: {:?}", nums),
Err(e) => println!("Error: {}", e),
}
}
The type annotation Result<Vec<i32>, ParseIntError> is critical. The compiler needs to know that you want a Result containing a Vec, not a Vec containing Results. Without the annotation, you get E0283.
This pattern replaces manual loops with error handling. You don't need to write a for loop that checks each result and breaks on error. collect() handles the control flow.
Pitfalls and compiler errors
When converting collections, watch for these common errors.
E0283 (type annotations needed): This happens when collect() cannot infer the target type. The compiler sees collect() and doesn't know whether to build a Vec, a HashMap, or a String. Provide a type annotation on the variable or use turbofish syntax collect::<Type>().
E0277 (trait bound not satisfied): This happens when the iterator's element type doesn't match what the target collection expects. For example, collecting i32 into a String fails because i32 doesn't implement AsRef<str>. Transform the elements first.
E0382 (use of moved value): This happens when you use into_iter() and then try to use the original collection. into_iter() consumes the collection. If you need the original data, use iter() or iter().cloned().
E0599 (no method named into_iter found): This can happen if you have a reference to a collection and call into_iter() without dereferencing. (&vec).into_iter() yields references, not values. To move values out of a reference, you need to clone or use methods that work on references. Usually, this error indicates a logic error. You probably want iter() instead.
E0308 (mismatched types): This happens when the type annotation doesn't match what collect() produces. For example, annotating a Vec<i32> but collecting iter() which produces Vec<&i32>. Fix the annotation or change the iterator method.
Decision matrix
Choose the right method based on ownership and performance needs.
Use into_iter() when you want to move ownership of elements out of the collection and don't need the original collection afterward. Use into_iter() for converting Vec to HashMap, Vec to String, or any transformation that consumes the source.
Use iter() when you only need to read elements and must keep the original collection alive. Use iter() when building a collection of references or when the target type accepts references.
Use iter().cloned() or iter().copied() when you need owned values but must keep the original collection. Use copied() for types that implement Copy like integers and booleans. Use cloned() for heap-allocated types like String or Vec.
Use collect() when you want to build a new collection from an iterator. Use collect() with a type annotation to specify the target type. Use collect() for converting iterators to Vec, HashMap, String, Result, or custom types.
Use Vec::from(slice) when converting a slice to a Vec for performance. Use Vec::from() instead of iter().cloned().collect() when no transformation is needed.
Use join() when converting a collection of strings to a single string with separators. Use join() on slices instead of collect() when you need delimiters.