Skip to content

← Ground Rules: Values, Types, Control Flow step 3 of 24

Easy Primitives

Integer types: widths, signedness and usize

For each i64, say whether it would fit in a u8 without changing value.

pub fn fits_in_u8(values: &[i64]) -> Vec<bool>

[0, 255, 256, -1] becomes [true, true, false, false].

The starter is correct. It fails the gate, and the reason is worth more than the answer.

Rust makes you pick a width

Most languages hand you one integer type and hide the rest. Rust gives you twelve, arranged in a grid you can read off in ten seconds:

bits signed unsigned unsigned max
8 i8 u8 255
16 i16 u16 65 535
32 i32 u32 4 294 967 295
64 i64 u64 18 446 744 073 709 551 615
128 i128 u128 ~3.4 × 10^38
pointer isize usize machine word

i32 is the default: an integer literal with nothing to pin it down becomes i32. i128 and u128 really are 16 bytes each, and they really do work.

Every one of these types carries its own MIN and MAX associated constants — u8::MAX is 255, i32::MIN is -2147483648. Use them. A literal 255 in your source says “the number two hundred and fifty-five”; u8::MAX says “the largest value a byte can hold”, which is what you actually mean, and which stays correct if the type ever changes.

usize is the indexing type, and that explains a lot

usize is as wide as a pointer — 8 bytes on the 64-bit machines that grade this course. It is the type of:

  • .len() on every slice, Vec, String and array;
  • every index you can use with [];
  • every capacity, offset and count in the standard library.

This is why items[i as i32] in the previous problem was E0277 and not a warning: Index is implemented for usize and for ranges, and simply is not implemented for i32. Once you know usize is the indexing type, a whole family of otherwise baffling trait errors becomes one rule.

It is also why for i in 0..v.len() gives you a usize for free, and why casting it back and forth is almost always a sign you should be iterating instead.

Widening is a conversion, not a coercion

Rust will not silently widen. A u8 does not become an i64 because the context wants one. You ask:

let hi = i64::from(u8::MAX);   // 255i64 — always exact, cannot fail

i64::from exists precisely because every u8 value is representable as an i64. Conversions that can lose information do not get a From; they get a TryFrom that returns a Result, which is item 1.11. The type system encodes which conversions are safe, and refuses to let you conflate the two.

The lint you will meet

The starter writes the interval test by hand:

v >= 0 && v <= 255

Clippy’s manual_range_contains fires on that and suggests (0..=255).contains(&v). It is on by default, it is a style lint, and its argument is that a range says “this interval” as one idea rather than as two comparisons a reader has to re-assemble — and that it is much harder to get the inclusive/exclusive ends wrong.

Combine both lessons: build the range out of u8::MIN and u8::MAX, widened to i64, and ask it whether it contains the value.

One more lint, for later

absurd_extreme_comparisons is a correctness lint, denied by default, and it catches things like x >= 0 where x is unsigned — a test that is always true and therefore almost certainly a bug in the reasoning. It will not fire here because our values are i64, but remember it exists the first time you write a bounds check against an unsigned type.