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
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}");}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)}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")}17 17.5 true
a: int = 7 # Python ints have no fixed sizeb: int = 10price: float = 2.5ok: bool = True
total = a + bcost = a * price # int * float gives a float automaticallyprint(f"{total} {cost} {str(ok).lower()}")17 17.5 true
const a: number = 7; // every ordinary number is a 64-bit floatconst b: bigint = 10n; // bigint: whole numbers of any sizeconst price: number = 2.5;const ok: boolean = true;
const total = BigInt(a) + b; // number and bigint don't mix: convert firstconst cost = a * price;console.log(`${total} ${cost} ${ok}`);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); }}17 17.5 true
int a = 7; // 32-bit signed integerlong b = 10; // 64-bit signed integerdouble price = 2.5; // 64-bit floatbool ok = true;
long total = a + b; // int widens to long on its owndouble cost = a * price; // and to double tooConsole.WriteLine($"{total} {cost} {ok.ToString().ToLower()}");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;}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";}17 17.5 true
What's different
- Integer names say their size:
i8 i16 i32 i64are signed,u8 u16 u32 u64are unsigned.usizeis 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 picksi32. Floats default tof64. ascan quietly cut bits off (300_i32 as u8is 44). When a conversion cannot lose data, usei64::from(a). When it might fail, useu8::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 withchecked_add,wrapping_addorsaturating_add. Floats never panic:1.0 / 0.0isinf, andNaNis not equal to itself.
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.fn main() { let a: i32 = 7; let price: f64 = 2.5; let cost = a * price; println!("{cost}");}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)}17.5
a: int = 7price: float = 2.5cost = a * priceprint(cost)17.5
const a: number = 7;const price: number = 2.5;const cost = a * price;console.log(cost);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); }}17.5
int a = 7;double price = 2.5;var cost = a * price;Console.WriteLine(cost);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;}17.5
#include <iostream>
int main() { int a = 7; double price = 2.5; auto cost = a * price; std::cout << cost << "\n";}17.5
What does the compiler say? Guess first, then open.
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)
Go refuses this too: multiplying an int32 by a float64 is a compile error, so you write float64(a) first.
Kotlin runs this and prints 17.5. An Int times a Double quietly becomes a Double, so you never write the conversion.
Python runs this and prints 17.5. An int times a float quietly becomes a float, so you never write the conversion.
Prints 17.5. TypeScript has one number type for both whole numbers and decimals, so there is nothing to convert.
Java quietly widens the int to a double, so this prints 17.5. Rust wants you to write that conversion yourself.
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.
C quietly turns a into a double before multiplying, so this prints 17.5. Rust wants you to write that conversion yourself.
C++ quietly converts a to a double before multiplying, so this prints 17.5. Rust wants you to write that conversion.
Rust treats every conversion as something you must write down.
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.
fn main() { let a: i32 = 7; let price: f64 = 2.5; let cost = f64::from(a) * price; println!("{cost}");}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:
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 );}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:
fn main() { let scores = [90, 85, 77]; let i: i32 = 1; println!("{}", scores[i]);}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:
fn main() { let count: u32 = 7; let index = usize::from(count); println!("{index}");}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:
fn main() { let big = 3_000_000_000; println!("{big}");}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.
fn main() { let price: f64 = 2.5; println!("{}", price * 2);}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):
fn main() { let mut level: u8 = 250; for step in 1..=10 { level += 1; println!("step {step}: level {level}"); }}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:
fn main() { let level: u8 = 255 + 1; println!("{level}");}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.
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));}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:
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);}-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:
fn main() { println!("{}", 300 as u8);}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:
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}");}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: 2try_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:
fn main() { let big: i32 = 300; let small = u8::from(big); println!("{small}");}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:
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));}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:
fn main() { let mut readings = vec![2.5, 0.5, 1.5]; readings.sort(); println!("{readings:?}");}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):
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:?}");}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:
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));}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:
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));}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:
fn main() { let flag = true; let n: i32 = flag; println!("{n}");}error[E0308]: mismatched types --> src/main.rs:3:18 | 3 | let n: i32 = flag; | --- ^^^^ expected `i32`, found `bool` | | | expected due to this
fn main() { let wins = 3; println!("{}", wins + true);}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:
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);}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: 172Text 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:
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}");}"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: 8080Spaces 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:
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);}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.)
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);}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:
fn main() { let balance: i32 = -1; let limit: u32 = 1; println!("under the limit: {}", balance < limit);}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.
/// `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
/// `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");}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.
/// 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
/// 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");}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.
/// `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
/// `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");}all checks passed
Read more
- The Rust Book, 3.2 Data Types.
- Rust by Example: Primitives, Literals and operators and Casting.
- The Rust Reference: numeric casts and overflow.
- The standard library docs:
i32(with thechecked_,wrapping_,saturating_andoverflowing_families),f64,char,usize,FromandTryFrom. - Reading compiler errors: E0277 and E0308 from this page.
Learn Rust is an independent project, not affiliated with the Rust Project or the Rust Foundation. Code and text are under the MIT licence.Source on GitHub.