Skip to content
Select theme

How to use the range syntax

Short answer: .. excludes its end value, ..= includes it. 0..3 is 0, 1, 2 and 0..=3 is 0, 1, 2, 3. They’re also two different types, Range<i32> and RangeInclusive<i32>, so one can’t stand in for the other.

In src/main.rs:

cargo run prints:

0
1
2
--
0
1
2
3

Same start, same end value written in the same place, one character apart in the middle. .. is half-open: its end is the first value that isn’t included. ..= is closed, so its end is the last value you get.

Neither form is loop syntax. Both build a value you can hold on to:

exclusive: 2 up to 6
inclusive: 2 up to 6
4 items / 5 items
holds 6? false / true

exclusive is a Range<i32>, and it keeps its ends in the fields start and end. inclusive is a RangeInclusive<i32>, which keeps them private: start() and end() are methods there. Getting that wrong is a compile error rather than something silent:

cargo build prints:

error[E0616]: field `end` of struct `std::ops::RangeInclusive` is private
--> src/main.rs:3:30
|
3 | println!("{}", inclusive.end);
| ^^^ private field
|
help: a method `end` also exists, call it with parentheses
|
3 | println!("{}", inclusive.end());
| ++
For more information about this error, try `rustc --explain E0616`.

Both forms answer count(), because both are iterators, and both answer contains().

The names aren’t interchangeable, and not only in a signature you write yourself:

cargo build prints:

error[E0308]: mismatched types
--> src/main.rs:2:35
|
2 | let r: std::ops::Range<i32> = 2..=6;
| -------------------- ^^^^^ expected `Range<i32>`, found `RangeInclusive<{integer}>`
| |
| expected due to this
|
= note: expected struct `std::ops::Range<i32>`
found struct `std::ops::RangeInclusive<{integer}>`
For more information about this error, try `rustc --explain E0308`.

A function that takes a Range<i32> won’t accept 2..=6. Swapping .. for ..= changes the type of the expression, so it isn’t a flag you can flip at the call site. The {integer} in the note is rustc’s stand-in for a literal whose type isn’t pinned down yet; annotate r as RangeInclusive<i32> and it goes away.

Half-open ranges line up with lengths, because the last valid index is one below the length:

60 over 3 items

values.len() is 3 and the last index is 2, so 0..values.len() visits every item with nothing to adjust. The inclusive version of the same loop would have to say 0..=values.len() - 1, and that subtraction panics on an empty collection — attempt to subtract with overflow, because a usize won’t go below zero. The half-open form needs no correction: 0..0 is simply empty.

3..3 is 0
3..0 is 0
3..=3 is 1
[3, 2, 1]

3..3 holds nothing, since 3 is excluded. 3..=3 holds exactly one value. 3..0 is empty as well, not an error: a range doesn’t run backwards, so the loop stops before it starts, and a countdown needs .rev() on an upward range.

Slicing is less forgiving. A backwards range used as an index panics — slice index starts at 2 but ends at 0 — and so does an end past the length: &items[0..5] on four items reports range end index 5 out of range for slice of length 4. The same range that quietly does nothing in a loop is a hard failure in an index.

Drop one end and you still have a range. That’s why slicing looks the way it does:

["b", "c"]
["a", "b"]
["c", "d"]
["a", "b", "c", "d"]
4

&items[..2] runs from the beginning, &items[2..] runs to the end, and .. on its own is everything, so &items[0..items.len()] and &items[..] are the same slice. Same half-open rule, with a missing end meaning “no end”: 0.. is that idea in a loop, a range that starts at zero and never names where to stop.