Skip to content
Learn Rust
Theme

Numbers and Types

Basics · Lesson 9

Every number type has an exact size, and Rust does not convert between them unless you write the conversion.

Coming from Goi32, i64 and f64 are your int32, int64 and float64, and x as i64 is int64(x). There is no plain int: whole-number literals default to i32, and lengths and indexes use usize.

Coming from Kotlini32, i64 and f64 are your Int, Long and Double, with as in place of toLong() or toDouble(). Kotlin lets you write Int + Long and Int * Double and gives the wider type; Rust has no such operators, so you write the conversion yourself. u8 and u32 are your UByte and UInt.

Coming from PythonA Python int grows as big as it needs to, but Rust integers have a fixed size like i32 or u64. Going past the limit panics in a debug build (cargo run) and wraps around in a release build (cargo run --release), and an i32 and an f64 won't mix until you convert them. 7 / 2 is 3 here, not 3.5.

Coming from TypeScriptYour number is always a 64-bit float, which is Rust's f64. Rust also has separate integer types like i32 and u64, and they never mix with each other or with floats, the same way number and bigint don't mix.

Coming from Javai32, i64, f64 and bool are int, long, double and boolean, and Rust adds unsigned types like u32 and u8, which Java does not have. Rust does not widen for you, so int * double style mixing needs an explicit conversion. Overflow wraps silently in Java; in a Rust debug build it panics.

Coming from C#i32, i64, u32, f64 are int, long, uint, double. C# widens int to long or double on its own; Rust widens nothing, so you write the conversion every time (i64::from(x) when nothing can be lost, as when you mean to cut).

Coming from Ci32 and i64 are int32_t and int64_t with the size fixed on every platform, and there is no plain int whose size varies. Rust does no implicit conversions between number types and no integer promotions, so mixing types needs a conversion, and overflow panics in debug builds where signed overflow in C is undefined behaviour.

Coming from C++i32, u64, f64 map to std::int32_t, std::uint64_t, double, and as is close to your static_cast. Rust has no implicit conversions between number types and no integer promotions, so even i32 + i64 does not compile without a cast.

The quick comparison

Rustsrc/main.rsRun ↗
fn main() {
let a: i32 = 7; // 32-bit signed integer
let b: i64 = 10; // 64-bit signed integer
let price: f64 = 2.5; // 64-bit float
let ok: bool = true;
let total = a as i64 + b; // convert with `as`
let cost = a as f64 * price;
println!("{total} {cost} {ok}");
}
$ cargo run
17 17.5 true
package main
import "fmt"
func main() {
var a int32 = 7
var b int64 = 10
price := 2.5
ok := true
total := int64(a) + b
cost := float64(a) * price
fmt.Println(total, cost, ok)
}
$ go run main.go
17 17.5 true
fun main() {
val a: Int = 7
val b: Long = 10
val price = 2.5
val ok = true
val total = a + b // Kotlin widens Int to Long for you
val cost = a * price // and Int to Double
println("$total $cost $ok")
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar
17 17.5 true
a: int = 7 # Python ints have no fixed size
b: int = 10
price: float = 2.5
ok: bool = True
total = a + b
cost = a * price # int * float gives a float automatically
print(f"{total} {cost} {str(ok).lower()}")
$ python3 main.py
17 17.5 true
const a: number = 7; // every ordinary number is a 64-bit float
const b: bigint = 10n; // bigint: whole numbers of any size
const price: number = 2.5;
const ok: boolean = true;
const total = BigInt(a) + b; // number and bigint don't mix: convert first
const cost = a * price;
console.log(`${total} ${cost} ${ok}`);
$ tsc --strict --module nodenext --types node main.ts && node main.js
17 17.5 true
public class Main {
public static void main(String[] args) {
int a = 7; // 32-bit signed integer
long b = 10; // 64-bit signed integer
double price = 2.5; // 64-bit float
boolean ok = true;
long total = a + b; // Java widens int to long for you
double cost = a * price; // and int to double
System.out.println(total + " " + cost + " " + ok);
}
}
$ javac Main.java && java Main
17 17.5 true
int a = 7; // 32-bit signed integer
long b = 10; // 64-bit signed integer
double price = 2.5; // 64-bit float
bool ok = true;
long total = a + b; // int widens to long on its own
double cost = a * price; // and to double too
Console.WriteLine($"{total} {cost} {ok.ToString().ToLower()}");
$ dotnet run
17 17.5 true
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
int main(void) {
int32_t a = 7; // 32-bit signed integer
int64_t b = 10; // 64-bit signed integer
double price = 2.5; // 64-bit float
bool ok = true;
int64_t total = (int64_t)a + b; // a cast, like Rust's `as`
double cost = (double)a * price;
printf("%lld %g %s\n", (long long)total, cost, ok ? "true" : "false");
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main
17 17.5 true
#include <cstdint>
#include <iostream>
int main() {
std::int32_t a = 7; // 32-bit signed integer
std::int64_t b = 10; // 64-bit signed integer
double price = 2.5; // 64-bit float
bool ok = true;
auto total = static_cast<std::int64_t>(a) + b; // explicit conversion
auto cost = static_cast<double>(a) * price;
std::cout << total << " " << cost << " " << std::boolalpha << ok << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main
17 17.5 true

What's different

  • Integer names say their size: i8 i16 i32 i64 are signed, u8 u16 u32 u64 are unsigned. usize is the type used for lengths and indexes. It is 64 bits on a 64-bit machine.
  • If you write let x = 5; with no type, Rust picks i32. Floats default to f64.
  • as can quietly cut bits off (300_i32 as u8 is 44). When a conversion cannot lose data, use i64::from(a). When it might fail, use u8::try_from(big), which returns a Result.
  • Going past the end of an integer is a panic in a debug build (cargo run) and a wrap-around in a release build (cargo run --release). Say what you mean with checked_add, wrapping_add or saturating_add. Floats never panic: 1.0 / 0.0 is inf, and NaN is not equal to itself.
Gotcha

Mixing an integer and a float

Looks fine: in Kotlin, Python, TypeScript, Java, C#, C and C++ it runs fine. Rust won't compile it.

Go refuses this too. Same idea, different words.Works in Kotlin. In Rust it's just a rule.Works in Python. In Rust it's just a rule.Works in TypeScript. In Rust it's just a rule.Works in Java. In Rust it's just a rule.Works in C#. In Rust it's just a rule.Works in C. In Rust it's just a rule.Works in C++. In Rust it's just a rule.
Rustsrc/main.rs✗ won't compile
fn main() {
let a: i32 = 7;
let price: f64 = 2.5;
let cost = a * price;
println!("{cost}");
}
Go✗ refuses too

Go refuses this too: multiplying an int32 by a float64 is a compile error, so you write float64(a) first.

package main
import "fmt"
func main() {
var a int32 = 7
price := 2.5
cost := a * price
fmt.Println(cost)
}
# command-line-arguments
./main.go:8:10: invalid operation: a * price (mismatched types int32 and float64)
fun main() {
val a: Int = 7
val price: Double = 2.5
val cost = a * price
println(cost)
}
$ kotlinc Main.kt -include-runtime -d main.jar && java -jar main.jar✓ runs
17.5
a: int = 7
price: float = 2.5
cost = a * price
print(cost)
$ python3 main.py✓ runs
17.5
const a: number = 7;
const price: number = 2.5;
const cost = a * price;
console.log(cost);
$ tsc --strict --module nodenext --types node main.ts && node main.js✓ runs
17.5
public class Main {
public static void main(String[] args) {
int a = 7;
double price = 2.5;
var cost = a * price;
System.out.println(cost);
}
}
$ javac Main.java && java Main✓ runs
17.5
int a = 7;
double price = 2.5;
var cost = a * price;
Console.WriteLine(cost);
$ dotnet run✓ runs
17.5
#include <stdio.h>
int main(void) {
int a = 7;
double price = 2.5;
double cost = a * price;
printf("%g\n", cost);
return 0;
}
$ gcc -Wall -Wextra main.c -o main && ./main✓ runs
17.5
#include <iostream>
int main() {
int a = 7;
double price = 2.5;
auto cost = a * price;
std::cout << cost << "\n";
}
$ g++ -std=c++20 -Wall -Wextra main.cpp -o main && ./main✓ runs
17.5
What does the compiler say? Guess first, then open.
$ cargo buildWhat is E0277?
error[E0277]: cannot multiply `i32` by `f64`
 --> src/main.rs:4:18
  |
4 |     let cost = a * price;
  |                  ^ no implementation for `i32 * f64`
  |
  = help: the trait `Mul<f64>` is not implemented for `i32`
  ... (notes pointing into the standard library trimmed)
What happens in Go

Go refuses this too: multiplying an int32 by a float64 is a compile error, so you write float64(a) first.

What happens in Kotlin

Kotlin runs this and prints 17.5. An Int times a Double quietly becomes a Double, so you never write the conversion.

What happens in Python

Python runs this and prints 17.5. An int times a float quietly becomes a float, so you never write the conversion.

What happens in TypeScript

Prints 17.5. TypeScript has one number type for both whole numbers and decimals, so there is nothing to convert.

What happens in Java

Java quietly widens the int to a double, so this prints 17.5. Rust wants you to write that conversion yourself.

What happens in C#

C# quietly converts the int to a double here and prints 17.5. That one is exact, but C# applies the same silent conversion from int to float and from long to double, where digits are lost. Rust wants every one of them written down.

What happens in C

C quietly turns a into a double before multiplying, so this prints 17.5. Rust wants you to write that conversion yourself.

What happens in C++

C++ quietly converts a to a double before multiplying, so this prints 17.5. Rust wants you to write that conversion.

What Rust sees

Rust treats every conversion as something you must write down.

Why Rust says no

Some conversions lose information. A big i64 does not fit in an i32, and a large integer cannot be stored exactly in an f32. This particular one, i32 to f64, is exact, but the rule is by type, not by value. Making you write the conversion means every place where something could be lost is visible in the code.

The fix

Compiles and runs.

Rust, fixedsrc/main.rsRun ↗
fn main() {
let a: i32 = 7;
let price: f64 = 2.5;
let cost = f64::from(a) * price;
println!("{cost}");
}
$ cargo run
17.5

The toolkit

Rust’s number types have fixed sizes (usize follows the target), and a number changes type only when you write the conversion. This page covers the types, what happens at their edges (overflow, NaN, negative remainders), and how to convert on purpose. Declarations and types shows how literals, as and the turbofish are spelled; this page covers what they do. Outputs come from Linux on 64-bit ARM with Rust 1.99.1; the text says where a fact depends on the platform.

The integer types

i is signed (it can be negative), u is unsigned (zero and up), and the number is the bit count. This program prints the size (size_of) and range (MIN and MAX) of each:

Rustsrc/main.rsRun ↗
use std::mem::size_of;
fn main() {
println!(
"i8 {:>2} byte {} to {}",
size_of::<i8>(),
i8::MIN,
i8::MAX
);
println!(
"i16 {:>2} bytes {} to {}",
size_of::<i16>(),
i16::MIN,
i16::MAX
);
println!(
"i32 {:>2} bytes {} to {}",
size_of::<i32>(),
i32::MIN,
i32::MAX
);
println!(
"i64 {:>2} bytes {} to {}",
size_of::<i64>(),
i64::MIN,
i64::MAX
);
println!(
"i128 {:>2} bytes {} to {}",
size_of::<i128>(),
i128::MIN,
i128::MAX
);
println!(
"u8 {:>2} byte {} to {}",
size_of::<u8>(),
u8::MIN,
u8::MAX
);
println!(
"u16 {:>2} bytes {} to {}",
size_of::<u16>(),
u16::MIN,
u16::MAX
);
println!(
"u32 {:>2} bytes {} to {}",
size_of::<u32>(),
u32::MIN,
u32::MAX
);
println!(
"u64 {:>2} bytes {} to {}",
size_of::<u64>(),
u64::MIN,
u64::MAX
);
println!(
"u128 {:>2} bytes {} to {}",
size_of::<u128>(),
u128::MIN,
u128::MAX
);
println!(
"isize {:>2} bytes {} to {}",
size_of::<isize>(),
isize::MIN,
isize::MAX
);
println!(
"usize {:>2} bytes {} to {}",
size_of::<usize>(),
usize::MIN,
usize::MAX
);
}
$ cargo run
i8     1 byte  -128 to 127
i16    2 bytes -32768 to 32767
i32    4 bytes -2147483648 to 2147483647
i64    8 bytes -9223372036854775808 to 9223372036854775807
i128  16 bytes -170141183460469231731687303715884105728 to 170141183460469231731687303715884105727
u8     1 byte  0 to 255
u16    2 bytes 0 to 65535
u32    4 bytes 0 to 4294967295
u64    8 bytes 0 to 18446744073709551615
u128  16 bytes 0 to 340282366920938463463374607431768211455
isize  8 bytes -9223372036854775808 to 9223372036854775807
usize  8 bytes 0 to 18446744073709551615
Bits Signed Unsigned Note
8 i8 u8 u8 is the byte
16 i16 u16
32 i32 u32 i32 is the default integer
64 i64 u64
128 i128 u128 see big numbers
pointer width isize usize lengths and positions

usize and isize are as wide as a memory address: the std docs say 4 bytes on a 32-bit target and 8 on a 64-bit one, and the output above shows 8 here.

Indexes are usize

A length can never be negative, and it must be able to name any position in memory, so lengths and positions are usize. Indexing with another integer type does not compile:

Rustsrc/main.rs✗ won't compile
fn main() {
let scores = [90, 85, 77];
let i: i32 = 1;
println!("{}", scores[i]);
}
$ cargo buildWhat is E0277?
error[E0277]: the type `[{integer}]` cannot be indexed by `i32`
 --> src/main.rs:4:27
  |
4 |     println!("{}", scores[i]);
  |                           ^ slice indices are of type `usize` or ranges of `usize`
  |
  = help: the trait `SliceIndex<[{integer}]>` is not implemented for `i32`
  ... (notes pointing into the standard library trimmed)

That is error E0277 (error codes). Convert first (scores[i as usize], or try_from below). Because usize is unsigned, items.len() - 1 on an empty list overflows (the panic). Its width differs between targets, so there is no From<u32> for it:

Rustsrc/main.rs✗ won't compile
fn main() {
let count: u32 = 7;
let index = usize::from(count);
println!("{index}");
}
$ cargo buildWhat is E0277?
error[E0277]: the trait bound `usize: From<u32>` is not satisfied
 --> src/main.rs:3:17
  |
3 |     let index = usize::from(count);
  |                 ^^^^^ the trait `From<u32>` is not implemented for `usize`
  ... (notes pointing into the standard library trimmed)

The usize docs list From for u8, u16 and bool, and TryFrom for u32 and the wider types.

Literals take their type from use

A number with no suffix takes its type from its use, and becomes i32 (integer) or f64 (float) only when nothing decides (the type is often optional). A literal that cannot fit the type it ends up with is an error, not a wrap:

Rustsrc/main.rs✗ won't compile
fn main() {
let big = 3_000_000_000;
println!("{big}");
}
$ cargo build
error: literal out of range for `i32`
 --> src/main.rs:2:15
  |
2 |     let big = 3_000_000_000;
  |               ^^^^^^^^^^^^^
  |
  = note: the literal `3_000_000_000` does not fit into the type `i32` whose range is `-2147483648..=2147483647`
  = help: consider using the type `u32` instead
  = note: `#[deny(overflowing_literals)]` on by default

An integer literal never turns into a float either: price * 2 needs 2.0.

Rustsrc/main.rs✗ won't compile
fn main() {
let price: f64 = 2.5;
println!("{}", price * 2);
}
$ cargo buildWhat is E0277?
error[E0277]: cannot multiply `f64` by `{integer}`
 --> src/main.rs:3:26
  |
3 |     println!("{}", price * 2);
  |                          ^ no implementation for `f64 * {integer}`
  |
  = help: the trait `Mul<{integer}>` is not implemented for `f64`
help: consider using a floating-point literal by writing it with `.0`
  |
3 |     println!("{}", price * 2.0);
  |                             ++

  ... (notes pointing into the standard library trimmed)

Overflow: a panic in debug, a wrap in release

Adding past the top of a type is overflow. Rust does not leave it undefined. In a debug build (the default for cargo run, cargo build and cargo test, and what this page’s runner makes) the program stops with a panic (an error that ends the program; Errors):

Rustsrc/main.rs! panics at runtimeRun ↗
fn main() {
let mut level: u8 = 250;
for step in 1..=10 {
level += 1;
println!("step {step}: level {level}");
}
}
$ cargo run
step 1: level 251
step 2: level 252
step 3: level 253
step 4: level 254
step 5: level 255

thread 'main' (1) panicked at src/main.rs:4:9:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

A release build (cargo build --release, the optimised one, profiles) is different. The Rust Book says that in release mode Rust “does not include checks for integer overflow that cause panics” and instead “performs two’s complement wrapping”, so 256 in a u8 becomes 0. Try it with cargo run --release: this page’s runner builds only in debug. What decides is the overflow-checks setting, not the build’s name: overflow-checks = true under [profile.release] in Cargo.toml (Cargo profiles), or -C overflow-checks=on for plain rustc, brings the panic back. Division is the exception: i32::MIN / -1 panics in every build (the Reference says these checks stay on even when overflow-checks is off). The Book calls relying on the wrap “an error”: decide what you want, and write it.

When the compiler can see the overflow without running the program, it refuses to build:

Rustsrc/main.rs✗ won't compile
fn main() {
let level: u8 = 255 + 1;
println!("{level}");
}
$ cargo build
error: this arithmetic operation will overflow
 --> src/main.rs:2:21
  |
2 |     let level: u8 = 255 + 1;
  |                     ^^^^^^^ attempt to compute `u8::MAX + 1_u8`, which would overflow
  |
  = note: `#[deny(arithmetic_overflow)]` on by default

Four families of methods say what you want, on every integer type:

Method On overflow 250u8 plus 10
checked_add returns None None
wrapping_add wraps around 4
saturating_add stops at the limit 255
overflowing_add returns the wrapped value and a flag (4, true)

(Option and Tuples cover the return types.) The same names exist for subtraction, multiplication and more (checked_sub, saturating_mul, checked_pow, wrapping_neg), and the last lines show why abs has them: i32::MIN has no positive counterpart.

Rustsrc/main.rsRun ↗
fn main() {
let level: u8 = 250;
println!("checked_add(10): {:?}", level.checked_add(10));
println!("checked_add(5): {:?}", level.checked_add(5));
println!("wrapping_add(10): {}", level.wrapping_add(10));
println!("saturating_add(10): {}", level.saturating_add(10));
println!("overflowing_add(10): {:?}", level.overflowing_add(10));
// The same families exist for -, *, /, pow and neg.
println!("0u8.saturating_sub(1): {}", 0u8.saturating_sub(1));
println!("i32::MIN.checked_abs(): {:?}", i32::MIN.checked_abs());
println!("i32::MIN.wrapping_abs(): {}", i32::MIN.wrapping_abs());
println!("i32::MIN.unsigned_abs(): {}", i32::MIN.unsigned_abs());
println!("10i32.checked_pow(10): {:?}", 10i32.checked_pow(10));
}
$ cargo run
checked_add(10): None
checked_add(5): Some(255)
wrapping_add(10): 4
saturating_add(10): 255
overflowing_add(10): (4, true)
0u8.saturating_sub(1): 0
i32::MIN.checked_abs(): None
i32::MIN.wrapping_abs(): -2147483648
i32::MIN.unsigned_abs(): 2147483648
10i32.checked_pow(10): None

Use checked_* when overflow is a bug you want to report, saturating_* for health points or a progress bar, and wrapping_* for hashes, counters meant to wrap, and fixed-width register models.

as: casts that never fail

as (spelling) never fails, so it can lose information without saying so. These are its rules for integers (the “sign bit” is explained under two’s complement), and check-casts.rs asserts every row:

Kind You write Result Rule
Narrowing 300_i32 as u8 44 keeps the low bits
Narrowing 70_000_i32 as i16 4464 keeps the low bits
Same size, other sign -1_i32 as u32 4294967295 same bits, read the other way
Same size, other sign 200_u8 as i8 -56 same bits, read the other way
Widening unsigned 255_u8 as i32 255 the new high bits are 0
Widening signed -1_i8 as i32 -1 the new high bits copy the sign bit
Widening signed to unsigned -1_i8 as u32 4294967295 sign bit copied, then read as unsigned
char to integer '€' as u32 8364 the Unicode code point
char to u8 '€' as u8 172 keeps the low 8 bits (0xAC)
bool to integer true as i32 1 false is 0
u8 to char 97_u8 as char 'a' only u8 can become a char

A float cut to an integer rounds toward zero, saturates at the integer type’s limits, and turns NaN into 0. An integer turned into a float rounds to the nearest value the float can hold:

Rustsrc/main.rsRun ↗
fn main() {
// Floats to integers: cut toward zero, saturate at the ends, NaN becomes 0.
println!("-2.9 as i32: {}", -2.9_f64 as i32);
println!("1e10 as i32: {}", 1e10_f64 as i32);
println!("-1e10 as i32: {}", -1e10_f64 as i32);
println!("300.7 as u8: {}", 300.7_f64 as u8);
println!("-5.0 as u8: {}", -5.0_f64 as u8);
println!("NaN as i32: {}", f64::NAN as i32);
println!("inf as i64: {}", f64::INFINITY as i64);
// Integers to floats: rounds to the nearest value the float can hold.
println!("16777216 as f32: {}", 16_777_216_i32 as f32);
println!("16777217 as f32: {}", 16_777_217_i32 as f32);
println!("i64::MAX as f64: {}", i64::MAX as f64);
println!("u64::MAX as f32: {}", u64::MAX as f32);
}
$ cargo run
-2.9 as i32: -2
1e10 as i32: 2147483647
-1e10 as i32: -2147483648
300.7 as u8: 255
-5.0 as u8: 0
NaN as i32: 0
inf as i64: 9223372036854775807
16777216 as f32: 16777216
16777217 as f32: 16777216
i64::MAX as f64: 9223372036854776000
u64::MAX as f32: 18446744000000000000

16777217 needs 25 bits, and an f32 has 24 bits of precision, so it lands on 16777216. A literal that does not fit the target of its own cast is rejected:

Rustsrc/main.rs✗ won't compile
fn main() {
println!("{}", 300 as u8);
}
$ cargo build
error: literal out of range for `u8`
 --> src/main.rs:2:20
  |
2 |     println!("{}", 300 as u8);
  |                    ^^^
  |
  = note: the literal `300` does not fit into the type `u8` whose range is `0..=255`
  = note: `#[deny(overflowing_literals)]` on by default

From and TryFrom: conversions that say what they do

as accepts everything. The two conversion traits (a trait is a set of methods a type can offer; Traits, later) are stricter. From between number types exists only where no value can be lost, so it cannot fail. TryFrom returns a Result (Ok(value) or Err(reason); Errors), so a value that does not fit is an Err you handle:

Rustsrc/main.rsRun ↗
fn main() {
// From: only where no value can be lost.
let small: i32 = 7;
let wide = i64::from(small);
let as_float = f64::from(small);
println!("i64::from(7i32): {wide}");
println!("f64::from(7i32): {as_float}");
println!("u32::from('a'): {}", u32::from('a'));
println!("u8::from(true): {}", u8::from(true));
// TryFrom: a Result, so a value that does not fit is an Err.
println!("u8::try_from(200i32): {:?}", u8::try_from(200_i32));
println!("u8::try_from(300i32): {:?}", u8::try_from(300_i32));
println!("u8::try_from(-1i32): {:?}", u8::try_from(-1_i32));
println!("usize::try_from(-1i32): {:?}", usize::try_from(-1_i32));
println!("i8::try_from(-128i64): {:?}", i8::try_from(-128_i64));
println!("char::try_from(65u32): {:?}", char::try_from(65_u32));
println!(
"char::try_from(0xD800u32): {:?}",
char::try_from(0xD800_u32).is_err()
);
// .ok() turns the Result into an Option: Ok(v) is Some(v), any Err is None.
println!("u8::try_from(200i32).ok(): {:?}", u8::try_from(200_i32).ok());
println!("u8::try_from(300i32).ok(): {:?}", u8::try_from(300_i32).ok());
// try_into is the same call from the other side; the target type says which.
let index: i32 = 2;
let as_usize: usize = index.try_into().unwrap();
println!("2i32 into usize: {as_usize}");
}
$ cargo run
i64::from(7i32): 7
f64::from(7i32): 7
u32::from('a'): 97
u8::from(true): 1
u8::try_from(200i32): Ok(200)
u8::try_from(300i32): Err(TryFromIntError(PosOverflow))
u8::try_from(-1i32): Err(TryFromIntError(NegOverflow))
usize::try_from(-1i32): Err(TryFromIntError(NegOverflow))
i8::try_from(-128i64): Ok(-128)
char::try_from(65u32): Ok('A')
char::try_from(0xD800u32): true
u8::try_from(200i32).ok(): Some(200)
u8::try_from(300i32).ok(): None
2i32 into usize: 2

try_into is the same call from the value’s side; the target type (here usize, from the annotation) picks the conversion. unwrap takes the value out of an Ok and panics on an Err. .ok() turns Ok(v) into Some(v) and any Err into None, an Option (a value that may be missing; Option). Asking From for a conversion that can lose data is an error:

Rustsrc/main.rs✗ won't compile
fn main() {
let big: i32 = 300;
let small = u8::from(big);
println!("{small}");
}
$ cargo buildWhat is E0277?
error[E0277]: the trait bound `u8: From<i32>` is not satisfied
 --> src/main.rs:3:17
  |
3 |     let small = u8::from(big);
  |                 ^^ the trait `From<i32>` is not implemented for `u8`
  ... (notes pointing into the standard library trimmed)

Use i64::from(x) (or x.into()) when the value always fits, u8::try_from(x) (or x.try_into()) when it might not and you must know, and x as u8 when you want the bits cut. For a position or length from a signed value, use usize::try_from(x).

Floats: NaN, infinity and comparing

f64 is the default 64-bit IEEE-754 float; f32 is the 32-bit one. Float division by zero is not an error, and NaN (“not a number”) is not equal to anything, itself included:

Rustsrc/main.rsRun ↗
fn main() {
println!(
"f32 size: {}, f64 size: {}",
size_of::<f32>(),
size_of::<f64>()
);
println!("f32::MAX: {:e}", f32::MAX);
println!("f64::MAX: {:e}", f64::MAX);
println!("f64::EPSILON: {:e}", f64::EPSILON);
println!("0.1 + 0.2: {}", 0.1 + 0.2);
println!("0.1 + 0.2 == 0.3: {}", 0.1 + 0.2 == 0.3);
println!("close enough: {}", (0.1_f64 + 0.2 - 0.3).abs() < 1e-9);
let zero = 0.0_f64;
println!("1.0 / 0.0: {}", 1.0 / zero);
println!("-1.0 / 0.0: {}", -1.0 / zero);
println!("0.0 / 0.0: {}", zero / zero);
let nan = f64::NAN;
println!("NaN == NaN: {}", nan == nan);
println!("NaN < 1.0: {}", nan < 1.0);
println!("NaN > 1.0: {}", nan > 1.0);
println!("NaN.is_nan(): {}", nan.is_nan());
println!("1.0.partial_cmp(&2.0): {:?}", 1.0_f64.partial_cmp(&2.0));
println!("1.0.partial_cmp(&NaN): {:?}", 1.0_f64.partial_cmp(&nan));
println!("1.0.total_cmp(&NaN): {:?}", 1.0_f64.total_cmp(&nan));
println!("0.0 == -0.0: {}", 0.0_f64 == -0.0);
println!("0.0.total_cmp(&-0.0): {:?}", 0.0_f64.total_cmp(&-0.0));
}
$ cargo run
f32 size: 4, f64 size: 8
f32::MAX: 3.4028235e38
f64::MAX: 1.7976931348623157e308
f64::EPSILON: 2.220446049250313e-16
0.1 + 0.2: 0.30000000000000004
0.1 + 0.2 == 0.3: false
close enough: true
1.0 / 0.0: inf
-1.0 / 0.0: -inf
0.0 / 0.0: NaN
NaN == NaN: false
NaN < 1.0: false
NaN > 1.0: false
NaN.is_nan(): true
1.0.partial_cmp(&2.0): Some(Less)
1.0.partial_cmp(&NaN): None
1.0.total_cmp(&NaN): Less
0.0 == -0.0: true
0.0.total_cmp(&-0.0): Greater

partial_cmp returns an Option because floats do not always have an order: None for NaN. total_cmp gives every float a place (negative zero before positive zero, a positive NaN after everything else), so it can sort. Plain sort() is refused on floats:

Rustsrc/main.rs✗ won't compile
fn main() {
let mut readings = vec![2.5, 0.5, 1.5];
readings.sort();
println!("{readings:?}");
}
$ cargo buildWhat is E0277?
error[E0277]: the trait bound `{float}: Ord` is not satisfied
 --> src/main.rs:3:14
  |
3 |     readings.sort();
  |              ^^^^ the trait `Ord` is not implemented for `{float}`
  |
  = help: the following other types implement trait `Ord`:
            i128
            i16
            i32
            i64
            i8
            isize
            u128
            u16
          and 4 others
  ... (notes pointing into the standard library trimmed)

E0277. sort_by takes a closure (a small function written inline: Closures) and the list is a Vec (Vectors):

Rustsrc/main.rsRun ↗
fn main() {
let mut readings = vec![2.5, f64::NAN, 0.5, -1.0];
readings.sort_by(|a, b| a.total_cmp(b));
println!("sorted: {readings:?}");
}
$ cargo run
sorted: [-1.0, 0.5, 2.5, NaN]

Compare floats that came from arithmetic with a tolerance, as close enough does above. Writing x == 0.3 is legal and is almost always wrong.

Division and remainder

Integer / cuts toward zero, and % takes the sign of the left side. For an index that has to stay in range, rem_euclid always gives a result of zero or more:

Rustsrc/main.rsRun ↗
fn main() {
println!("7 / 2: {}", 7 / 2);
println!("7.0 / 2.0: {}", 7.0 / 2.0);
println!("7 as f64 / 2.0: {}", 7 as f64 / 2.0);
println!("-7 / 2: {}", -7 / 2);
println!("-7 % 2: {}", -7 % 2);
println!("7 % -2: {}", 7 % -2);
println!("(-7).div_euclid(2): {}", (-7_i32).div_euclid(2));
println!("(-7).rem_euclid(2): {}", (-7_i32).rem_euclid(2));
println!("-7 % 5: {}", -7 % 5);
println!("(-7).rem_euclid(5): {}", (-7_i32).rem_euclid(5));
// A wrapped index: step back one place in a ring of 5 slots.
let slot = 0_i32;
println!("(slot - 1) % 5: {}", (slot - 1) % 5);
println!("(slot - 1).rem_euclid(5): {}", (slot - 1).rem_euclid(5));
}
$ cargo run
7 / 2: 3
7.0 / 2.0: 3.5
7 as f64 / 2.0: 3.5
-7 / 2: -3
-7 % 2: -1
7 % -2: 1
(-7).div_euclid(2): -4
(-7).rem_euclid(2): 1
-7 % 5: -2
(-7).rem_euclid(5): 3
(slot - 1) % 5: -1
(slot - 1).rem_euclid(5): 4

7 / 2 is 3: two integers divide to an integer, so convert one side first for 3.5. Dividing an integer by zero panics, as does i32::MIN / -1 (see overflow); checked_div returns None for both (check-division.rs asserts it). The zero case:

Rustsrc/main.rs! panics at runtimeRun ↗
fn average(total: i32, count: i32) -> i32 {
total / count
}
fn main() {
println!("average of 10 over 2: {}", average(10, 2));
println!("average of 10 over 0: {}", average(10, 0));
}
$ cargo run
average of 10 over 2: 5

thread 'main' (1) panicked at src/main.rs:2:5:
attempt to divide by zero
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Method calls bind tighter than unary minus, so -7_i32.rem_euclid(2) is -1: put the negative number in parentheses, as division.rs does (check-division.rs asserts it).

bool and char

A bool is true or false, one byte. It is not a number, so there is no if count (What Rust lacks) and no wins + true:

Rustsrc/main.rs✗ won't compile
fn main() {
let flag = true;
let n: i32 = flag;
println!("{n}");
}
$ cargo buildWhat is E0308?
error[E0308]: mismatched types
 --> src/main.rs:3:18
  |
3 |     let n: i32 = flag;
  |            ---   ^^^^ expected `i32`, found `bool`
  |            |
  |            expected due to this
Rustsrc/main.rs✗ won't compile
fn main() {
let wins = 3;
println!("{}", wins + true);
}
$ cargo buildWhat is E0277?
error[E0277]: cannot add `bool` to `{integer}`
 --> src/main.rs:3:25
  |
3 |     println!("{}", wins + true);
  |                         ^ no implementation for `{integer} + bool`
  |
  = help: the trait `Add<bool>` is not implemented for `{integer}`
  = help: the following other types implement trait `Add<Rhs>`:
            `&f128` implements `Add<f128>`
            `&f128` implements `Add`
            `&f16` implements `Add<f16>`
            `&f16` implements `Add`
            `&f32` implements `Add<f32>`
            `&f32` implements `Add`
            `&f64` implements `Add<f64>`
            `&f64` implements `Add`
          and 56 others

A char is one Unicode scalar value: any code point except the surrogates U+D800 to U+DFFF, 4 bytes wide (a char is not a byte); char::from_u32 returns None for numbers that are not one. A u8 is a plain number, written as a byte literal (b'a') for ASCII:

Rustsrc/main.rsRun ↗
fn main() {
println!("size of bool: {}", size_of::<bool>());
println!("size of char: {}", size_of::<char>());
println!("char::MAX: {:?} = U+{:X}", char::MAX, char::MAX as u32);
println!("char::from_u32(0x41): {:?}", char::from_u32(0x41));
println!("char::from_u32(0xD800): {:?}", char::from_u32(0xD800));
println!("char::from_u32(1114112): {:?}", char::from_u32(1_114_112));
// Three characters, three lengths in UTF-8.
for c in ['a', 'é', '€', '🦀'] {
println!(
"{c} is U+{:04X}, {} byte(s) in UTF-8",
c as u32,
c.len_utf8()
);
}
// A u8 is a number that can be written as a byte literal.
let letter = b'a';
println!("b'a': {letter}");
println!("b'a' + 1: {}", letter + 1);
println!("as a char: {}", (letter + 1) as char);
println!("'7'.to_digit(10): {:?}", '7'.to_digit(10));
println!("'x'.to_digit(10): {:?}", 'x'.to_digit(10));
println!("char::from_digit(7, 10): {:?}", char::from_digit(7, 10));
println!("'€' as u8: {}", '€' as u8);
}
$ cargo run
size of bool: 1
size of char: 4
char::MAX: '\u{10ffff}' = U+10FFFF
char::from_u32(0x41): Some('A')
char::from_u32(0xD800): None
char::from_u32(1114112): None
a is U+0061, 1 byte(s) in UTF-8
é is U+00E9, 2 byte(s) in UTF-8
€ is U+20AC, 3 byte(s) in UTF-8
🦀 is U+1F980, 4 byte(s) in UTF-8
b'a': 97
b'a' + 1: 98
as a char: b
'7'.to_digit(10): Some(7)
'x'.to_digit(10): None
char::from_digit(7, 10): Some('7')
'€' as u8: 172

Text is stored as UTF-8 bytes and does not index by char: Strings.

Parsing numbers from text

as does not accept text. parse does, and it returns a Result, because text may not be a number. The type comes from ::<i32> (turbofish) or from where the value goes:

Rustsrc/main.rsRun ↗
fn main() {
println!("\"42\": {:?}", "42".parse::<i32>());
println!("\"-5\" to u8: {:?}", "-5".parse::<u8>());
println!("\"300\" to u8: {:?}", "300".parse::<u8>());
println!("\"4x\": {:?}", "4x".parse::<i32>());
println!("\"\": {:?}", "".parse::<i32>());
println!("\" 42\": {:?}", " 42".parse::<i32>());
println!("\" 42\".trim(): {:?}", " 42".trim().parse::<i32>());
println!("\"+7\": {:?}", "+7".parse::<i32>());
println!("\"2.5\" to f64: {:?}", "2.5".parse::<f64>());
println!("\"2.5\" to i32: {:?}", "2.5".parse::<i32>());
println!("\"NaN\" to f64: {:?}", "NaN".parse::<f64>());
println!("\"true\": {:?}", "true".parse::<bool>());
// The type can come from where the value goes.
let n: u16 = "8080".parse().unwrap();
println!("port: {n}");
}
$ cargo run
"42": Ok(42)
"-5" to u8: Err(ParseIntError { kind: InvalidDigit })
"300" to u8: Err(ParseIntError { kind: PosOverflow })
"4x": Err(ParseIntError { kind: InvalidDigit })
"": Err(ParseIntError { kind: Empty })
" 42": Err(ParseIntError { kind: InvalidDigit })
" 42".trim(): Ok(42)
"+7": Ok(7)
"2.5" to f64: Ok(2.5)
"2.5" to i32: Err(ParseIntError { kind: InvalidDigit })
"NaN" to f64: Ok(NaN)
"true": Ok(true)
port: 8080

Spaces are not skipped (call trim() first). Errors shows how to handle the Err.

Big numbers are not built in

The largest built-in integer is the 128-bit one. Past that std has no big-number type; the num-bigint crate (a library from the Rust package registry; docs) provides one. checked_mul finds the edge:

Rustsrc/main.rsRun ↗
fn main() {
let mut factorial: u128 = 1;
for n in 1..=40_u128 {
match factorial.checked_mul(n) {
Some(next) => factorial = next,
None => {
println!(
"{n}! does not fit in a u128; {} is the last that does",
n - 1
);
break;
}
}
}
println!("{factorial}");
println!("u128::MAX: {}", u128::MAX);
}
$ cargo run
35! does not fit in a u128; 34 is the last that does
295232799039604140847618609643520000000
u128::MAX: 340282366920938463463374607431768211455

Your language, in Rust

Pick your language in “Coming from” at the top of the page. Most Rust cells in these tables are asserted by check-forlang.rs; the compile errors are the snippets above.

Going deeper

Two’s complement in one example

Rust stores signed integers in two’s complement: the top bit counts as a negative weight. Negating is “flip every bit, add one”, and everything in the casts table follows. ({:08b} and {:#x} are in format strings.)

Rustsrc/main.rsRun ↗
fn main() {
let x: i8 = 5;
println!("x: {:08b}", x);
println!("-x: {:08b}", -x);
println!("!x + 1: {:08b}", !x + 1);
println!("-1_i8: {:08b}", -1_i8);
println!("i8::MIN: {:08b} = {}", i8::MIN, i8::MIN);
println!("i8::MAX: {:08b} = {}", i8::MAX, i8::MAX);
// The same bits read as unsigned, and the other way round.
println!("-x as u8: {:08b} = {}", -x as u8, -x as u8);
println!("200_u8 as i8: {:08b} = {}", 200_u8 as i8, 200_u8 as i8);
// Adding one to the largest value carries into the sign bit.
println!("i8::MAX.wrapping_add(1): {}", i8::MAX.wrapping_add(1));
println!("255u8.wrapping_add(1): {}", 255_u8.wrapping_add(1));
// Narrowing keeps the low bits; hex shows which.
let wide: i32 = 0xA1B2C;
println!("wide: {:#x}", wide);
println!("as u16: {:#x}", wide as u16);
println!("as u8: {:#x}", wide as u8);
println!("300: {:#b}", 300_i32);
println!("300_i32 as u8: {:#010b} -> {}", 300_i32 as u8, 300_i32 as u8);
}
$ cargo run
x:             00000101
-x:            11111011
!x + 1:        11111011
-1_i8:         11111111
i8::MIN:       10000000 = -128
i8::MAX:       01111111 = 127
-x as u8:      11111011 = 251
200_u8 as i8:  11001000 = -56
i8::MAX.wrapping_add(1): -128
255u8.wrapping_add(1):   0
wide:          0xa1b2c
as u16:        0x1b2c
as u8:         0x2c
300:           0b100101100
300_i32 as u8: 0b00101100 -> 44

Read it top to bottom. ! flips every bit of an integer (Rust has no ~; on a bool it is logical not), so -x and !x + 1 print the same bits. -x as u8 keeps those bits and reads them as 251; 200_u8 as i8 keeps 11001000 and reads it as -56. Adding one to 127 carries into the top bit and gives -128; adding one to 255 in a u8 carries out of the byte and leaves 0. Narrowing drops the high bits: 0xA1B2C as a u8 is 0x2c, and 300 is 0b100101100, which loses its top bit and leaves 0b00101100, which is 44.

No implicit widening

Going from i32 to i64 loses nothing, so why does Rust ask for i64::from? Because the rule is by type, not by value. A language that converts silently decides pair by pair which conversions are safe, and the decision is invisible at the call site. Java and C# convert int to float on their own. C and C++ go furthest (their programs are under “Coming from”): u8 + u8 is promoted to int, so 200 + 100 is 300, and storing that back into a u8 gives 44; and -1 < 1u is false, because the signed side becomes unsigned. Rust has neither: u8 + u8 is u8 (What Rust lacks shows i32 + i64), and a mixed comparison does not compile:

Rustsrc/main.rs✗ won't compile
fn main() {
let balance: i32 = -1;
let limit: u32 = 1;
println!("under the limit: {}", balance < limit);
}
$ cargo buildWhat is E0308?
error[E0308]: mismatched types
 --> src/main.rs:4:47
  |
4 |     println!("under the limit: {}", balance < limit);
  |                                     -------   ^^^^^ expected `i32`, found `u32`
  |                                     |
  |                                     expected because this is `i32`
  |
help: you can convert a `u32` to an `i32` and panic if the converted value doesn't fit
  |
4 |     println!("under the limit: {}", balance < limit.try_into().unwrap());
  |                                                    ++++++++++++++++++++

E0308. The help suggests converting limit with try_into().unwrap(), which panics only if limit does not fit in an i32. Which side to convert, and what happens on failure, is your decision. Every place a number changes type is searchable, and each is From (cannot lose), TryFrom (tells you) or as (you chose to cut). The cost is real: code that mixes usize counts with i32 values has more conversions than in a language that converts for you.

Practice

Try it

Open each exercise in the Playground, replace the todo!(), and run it until it prints all checks passed. Two return an Option: Some(x) is “here is the answer” and None is “no answer” (Option, later).

A safe percentage

percent(part, total) returns part * 100 / total, rounded down, as a u32. It is None when total is 0 or when the answer does not fit in a u32. Doing the arithmetic in u32 overflows for large inputs, so widen first with u64::from, and use u32::try_from for the result, with .ok() to get the Option. The check with 50 million of 100 million is the one people get wrong.

Starterexercises/percent.rs
/// `part * 100 / total`, rounded down. `None` when `total` is 0 or the answer
/// does not fit in a `u32`.
fn percent(part: u32, total: u32) -> Option<u32> {
todo!("widen to u64 before multiplying; u32::try_from(...).ok() for the answer")
}
fn main() {
assert_eq!(percent(1, 4), Some(25));
assert_eq!(percent(1, 3), Some(33));
assert_eq!(percent(0, 7), Some(0));
assert_eq!(percent(150, 100), Some(150));
// 50_000_000 * 100 does not fit in a u32.
assert_eq!(percent(50_000_000, 100_000_000), Some(50));
assert_eq!(percent(u32::MAX, u32::MAX), Some(100));
// Division by zero is None, not a panic.
assert_eq!(percent(5, 0), None);
// The answer itself is too big for a u32.
assert_eq!(percent(u32::MAX, 1), None);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/percent.solution.rsRun ↗
/// `part * 100 / total`, rounded down. `None` when `total` is 0 or the answer
/// does not fit in a `u32`.
fn percent(part: u32, total: u32) -> Option<u32> {
if total == 0 {
return None;
}
let scaled = u64::from(part) * 100 / u64::from(total);
u32::try_from(scaled).ok()
}
fn main() {
assert_eq!(percent(1, 4), Some(25));
assert_eq!(percent(1, 3), Some(33));
assert_eq!(percent(0, 7), Some(0));
assert_eq!(percent(150, 100), Some(150));
// 50_000_000 * 100 does not fit in a u32.
assert_eq!(percent(50_000_000, 100_000_000), Some(50));
assert_eq!(percent(u32::MAX, u32::MAX), Some(100));
// Division by zero is None, not a panic.
assert_eq!(percent(5, 0), None);
// The answer itself is too big for a u32.
assert_eq!(percent(u32::MAX, 1), None);
println!("all checks passed");
}
$ cargo run
all checks passed

Digit sum of any integer

digit_sum(n) adds the decimal digits of an i64, ignoring the sign: -1234 gives 10. Loop with % and /. i64::MIN has no positive twin, so n.abs() would overflow: use unsigned_abs(), which returns a u64. A digit is below 10, so turning it into a u32 with as cannot cut anything.

Starterexercises/digit-sum.rs
/// The sum of the decimal digits of `n`, ignoring the sign.
fn digit_sum(n: i64) -> u32 {
todo!("take the absolute value as a u64 (unsigned_abs), then use % 10 and / 10 in a loop; a digit is below 10, so `as u32` cannot cut anything")
}
fn main() {
assert_eq!(digit_sum(0), 0);
assert_eq!(digit_sum(7), 7);
assert_eq!(digit_sum(1234), 10);
assert_eq!(digit_sum(-1234), 10);
assert_eq!(digit_sum(1_000_000), 1);
// i64::MIN is -9223372036854775808; its absolute value does not fit in an i64.
assert_eq!(digit_sum(i64::MIN), 89);
assert_eq!(digit_sum(i64::MAX), 88);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/digit-sum.solution.rsRun ↗
/// The sum of the decimal digits of `n`, ignoring the sign.
fn digit_sum(n: i64) -> u32 {
let mut rest = n.unsigned_abs();
let mut sum = 0;
while rest > 0 {
// A digit is below 10, so this `as` cannot cut anything.
sum += (rest % 10) as u32;
rest /= 10;
}
sum
}
fn main() {
assert_eq!(digit_sum(0), 0);
assert_eq!(digit_sum(7), 7);
assert_eq!(digit_sum(1234), 10);
assert_eq!(digit_sum(-1234), 10);
assert_eq!(digit_sum(1_000_000), 1);
// i64::MIN is -9223372036854775808; its absolute value does not fit in an i64.
assert_eq!(digit_sum(i64::MIN), 89);
assert_eq!(digit_sum(i64::MAX), 88);
println!("all checks passed");
}
$ cargo run
all checks passed

A checked index

checked_index(i, len) turns an i32 into a slot number (a u8) in a table of len slots, or None when i is negative or past the end. Use u8::try_from(i), and .ok() to get an Option. i as u8 is the shortcut to avoid: it turns 256 into 0 and 300 into 44, and the checks include both.

Starterexercises/checked-index.rs
/// `Some(slot)` when `i` names a slot in a table of `len` slots (numbered from
/// 0), otherwise `None`.
fn checked_index(i: i32, len: u8) -> Option<u8> {
todo!("u8::try_from(i) is an Err when i does not fit in a u8; .ok() makes it an Option")
}
fn main() {
assert_eq!(checked_index(0, 3), Some(0));
assert_eq!(checked_index(2, 3), Some(2));
assert_eq!(checked_index(3, 3), None);
assert_eq!(checked_index(-1, 3), None);
assert_eq!(checked_index(i32::MIN, 3), None);
assert_eq!(checked_index(i32::MAX, 3), None);
// An empty table has no valid slot.
assert_eq!(checked_index(0, 0), None);
// `i as u8` would wrap 256 to 0 and 300 to 44, and say Some.
assert_eq!(checked_index(256, 10), None);
assert_eq!(checked_index(300, 100), None);
assert_eq!(checked_index(254, 255), Some(254));
assert_eq!(checked_index(255, 255), None);
println!("all checks passed");
}

Open in the Rust Playground ↗Run it first: it stops at todo!() with not yet implemented and a hint, and the compiler warns that the parameter is unused. Both are expected. Replace the todo!() with your code and run it again until it prints all checks passed.

Show a solution
One solutionexercises/checked-index.solution.rsRun ↗
/// `Some(slot)` when `i` names a slot in a table of `len` slots (numbered from
/// 0), otherwise `None`.
fn checked_index(i: i32, len: u8) -> Option<u8> {
match u8::try_from(i).ok() {
Some(slot) if slot < len => Some(slot),
_ => None,
}
}
fn main() {
assert_eq!(checked_index(0, 3), Some(0));
assert_eq!(checked_index(2, 3), Some(2));
assert_eq!(checked_index(3, 3), None);
assert_eq!(checked_index(-1, 3), None);
assert_eq!(checked_index(i32::MIN, 3), None);
assert_eq!(checked_index(i32::MAX, 3), None);
// An empty table has no valid slot.
assert_eq!(checked_index(0, 0), None);
// `i as u8` would wrap 256 to 0 and 300 to 44, and say Some.
assert_eq!(checked_index(256, 10), None);
assert_eq!(checked_index(300, 100), None);
assert_eq!(checked_index(254, 255), Some(254));
assert_eq!(checked_index(255, 255), None);
println!("all checks passed");
}
$ cargo run
all checks passed

Read more