Range Operators - .. and ..=
Range operators (.. and ..=) construct lightweight range structs from the std::ops module. Ranges are used throughout Rust for two primary purposes: generating sequences of values in for loops and iterators, and slicing contiguous collections (&slice[start..end]).
The Six Range Forms in Rust
Depending on whether a start bound, an end bound, or both are provided, .. and ..= produce six distinct types in std::ops:
| Syntax | Type (std::ops::...) |
Bounds | Meaning in Iteration (for) |
Meaning in Slicing (&s[...]) |
|---|---|---|---|---|
start..end |
Range<Idx> |
Half-open [start, end) |
Yields start up to end - 1 |
Elements from index start up to end - 1 |
start..=end |
RangeInclusive<Idx> |
Closed [start, end] |
Yields start through end (inclusive) |
Elements from index start through end |
start.. |
RangeFrom<Idx> |
Lower-bounded [start, ∞) |
Infinite iterator starting at start (use .take(n)!) |
Elements from index start to the end of the slice |
..end |
RangeTo<Idx> |
Upper-bounded [0, end) |
Not iterable (no starting point) | Elements from index 0 up to end - 1 |
..=end |
RangeToInclusive<Idx> |
Upper-bounded [0, end] |
Not iterable (no starting point) | Elements from index 0 through end |
.. |
RangeFull |
Unbounded (-∞, +∞) |
Not iterable | The entire slice (0..s.len()) |
fn main() {
let half_open = 1..5; // std::ops::Range<i32>
let inclusive = 1..=5; // std::ops::RangeInclusive<i32>
let from_ten = 10..; // std::ops::RangeFrom<i32>
let up_to_four = ..4; // std::ops::RangeTo<usize>
let up_to_inc = ..=4; // std::ops::RangeToInclusive<usize>
let full = ..; // std::ops::RangeFull
println!("half_open: {half_open:?} (start={}, end={})", half_open.start, half_open.end);
println!("inclusive: {inclusive:?} (start={}, end={})", inclusive.start(), inclusive.end());
println!("from_ten: {from_ten:?} (first 3: {:?})", from_ten.take(3).collect::<Vec<_>>());
println!("Slicing ranges: {up_to_four:?}, {up_to_inc:?}, {full:?}");
}
half_open: 1..5 (start=1, end=5)
inclusive: 1..=5 (start=1, end=5)
from_ten: 10.. (first 3: [10, 11, 12])
Slicing ranges: ..4, ..=4, ..
Slicing Arrays, Vectors, and Strings with Ranges
All six range forms implement SliceIndex, allowing you to extract borrowed sub-slices from arrays, vectors, and strings:
Array: let data = ['a', 'b', 'c', 'd', 'e'];
Indices: 0 1 2 3 4
&data[1..4] --> ['b', 'c', 'd'] (indices 1, 2, 3)
&data[1..=4] --> ['b', 'c', 'd', 'e'] (indices 1, 2, 3, 4)
&data[..3] --> ['a', 'b', 'c'] (indices 0, 1, 2)
&data[..=3] --> ['a', 'b', 'c', 'd'] (indices 0, 1, 2, 3)
&data[2..] --> ['c', 'd', 'e'] (indices 2, 3, 4)
&data[..] --> ['a', 'b', 'c', 'd', 'e'](all elements)
fn main() {
let data = ['a', 'b', 'c', 'd', 'e'];
println!("&data[1..4] = {:?}", &data[1..4]);
println!("&data[1..=4] = {:?}", &data[1..=4]);
println!("&data[..3] = {:?}", &data[..3]);
println!("&data[..=3] = {:?}", &data[..=3]);
println!("&data[2..] = {:?}", &data[2..]);
println!("&data[..] = {:?}", &data[..]);
}
&data[1..4] = ['b', 'c', 'd']
&data[1..=4] = ['b', 'c', 'd', 'e']
&data[..3] = ['a', 'b', 'c']
&data[..=3] = ['a', 'b', 'c', 'd']
&data[2..] = ['c', 'd', 'e']
&data[..] = ['a', 'b', 'c', 'd', 'e']
Checking Membership with .contains(&value)
Every range type provides a .contains(&item) method that checks whether a value falls inside the range bounds in $O(1)$ time (comparing against start and end without iterating through the range!):
fn main() {
let working_hours = 9..17; // 9..=16
let percentage = 0.0..=100.0; // Works on f64 too!
let lowercase = 'a'..='z'; // Works on char!
println!("Is 12 in 9..17? {}", working_hours.contains(&12));
println!("Is 17 in 9..17? {}", working_hours.contains(&17));
println!("Is 99.5 in 0..=100? {}", percentage.contains(&99.5));
println!("Is 'm' lowercase? {}", lowercase.contains(&'m'));
println!("Is (5..2).is_empty()? {}", (5..2).is_empty());
}
Is 12 in 9..17? true
Is 17 in 9..17? false
Is 99.5 in 0..=100? true
Is 'm' lowercase? true
Is (5..2).is_empty()? true
Range Patterns in match and if let
In pattern matching (match, if let, matches!), inclusive ranges (start..=end) and open-ended ranges (start.. or ..=end / ..end) match values directly:
fn classify_http_status(code: u16) -> &'static str {
match code {
100..=199 => "Informational",
200..=299 => "Success",
300..=399 => "Redirection",
400..=499 => "Client Error",
500..=599 => "Server Error",
_ => "Non-standard Status",
}
}
fn main() {
println!("204: {}", classify_http_status(204));
println!("404: {}", classify_http_status(404));
}
204: Success
404: Client Error
Note: Inside a
matchpattern, bounded ranges must use the inclusive..=syntax (100..=199) rather than half-open100..200, so the reader never has to guess whether the upper endpoint of a pattern is included.
Operator Precedence of .. and ..=
The range operators .. and ..= have very low precedence (lower than arithmetic +, -, *, /, bit shifts <<, comparisons, and method calls .):
1 + 2..5 + 5is parsed as(1 + 2)..(5 + 5)(3..10), which is convenient when computing bounds.- However,
0..5.len()or(0..5)with method calls requires parentheses(0..5).rev()because.binds much tighter than..:
// ❌ does not compile
fn main() {
for i in 0..5.rev() {
println!("{i}");
}
}
error[E0599]: no method named `rev` found for type `{integer}` in the current scope
--> src/main.rs:2:19
|
2 | for i in 0..5.rev() {
| ^^^ method not found in `{integer}`
error: aborting due to 1 previous error
Always wrap the range in parentheses (0..5).rev() before calling iterator methods on it.