Rust - Numbers & Casting
Overview
Estimated time: 40–50 minutes
Master Rust's numeric types, understand the differences between integer and floating-point types, learn safe and unsafe casting operations, and explore numeric conversions and overflow handling.
Learning Objectives
- Understand all numeric types and their ranges
- Learn safe casting with
as
and conversion traits - Handle numeric overflow and underflow
- Practice with numeric operations and precision
Prerequisites
Integer Types
Signed Integers
Rust provides signed integers of various sizes:
fn main() {
let tiny: i8 = 127; // -128 to 127
let small: i16 = 32_767; // -32,768 to 32,767
let medium: i32 = 2_147_483_647; // Default integer type
let large: i64 = 9_223_372_036_854_775_807;
let huge: i128 = 170_141_183_460_469_231_731_687_303_715_884_105_727;
let arch: isize = 1000; // Platform dependent (32 or 64 bit)
println!("i8: {}, i16: {}, i32: {}", tiny, small, medium);
println!("i64: {}, i128: {}, isize: {}", large, huge, arch);
}
Output:
i8: 127, i16: 32767, i32: 2147483647
i64: 9223372036854775807, i128: 170141183460469231731687303715884105727, isize: 1000
Unsigned Integers
fn main() {
let tiny: u8 = 255; // 0 to 255
let small: u16 = 65_535; // 0 to 65,535
let medium: u32 = 4_294_967_295;
let large: u64 = 18_446_744_073_709_551_615;
let huge: u128 = 340_282_366_920_938_463_463_374_607_431_768_211_455;
let arch: usize = 1000; // Platform dependent, often used for indexing
println!("u8: {}, u16: {}, u32: {}", tiny, small, medium);
println!("u64: {}, u128: {}, usize: {}", large, huge, arch);
}
Floating-Point Types
Basic Floating-Point
fn main() {
let single: f32 = 3.14159; // 32-bit float
let double: f64 = 2.718281828; // 64-bit float (default)
// Scientific notation
let scientific: f64 = 1.23e-4; // 0.000123
let large_sci: f64 = 6.02e23; // Avogadro's number
println!("f32: {}, f64: {}", single, double);
println!("Scientific: {}, Large: {}", scientific, large_sci);
// Special values
let infinity = f64::INFINITY;
let neg_infinity = f64::NEG_INFINITY;
let not_a_number = f64::NAN;
println!("∞: {}, -∞: {}, NaN: {}", infinity, neg_infinity, not_a_number);
}
Output:
f32: 3.14159, f64: 2.718281828
Scientific: 0.000123, Large: 602000000000000000000000
∞: inf, -∞: -inf, NaN: NaN
Type Casting with 'as'
Basic Casting
fn main() {
let integer = 42i32;
let float = 3.14f64;
// Integer to float (safe)
let int_to_float = integer as f64;
// Float to integer (truncates)
let float_to_int = float as i32;
// Unsigned to signed
let unsigned = 255u8;
let signed = unsigned as i8; // Wraps around: -1
println!("Original: {}, as f64: {}", integer, int_to_float);
println!("Original: {}, as i32: {}", float, float_to_int);
println!("u8(255) as i8: {}", signed);
}
Output:
Original: 42, as f64: 42
Original: 3.14, as i32: 3
u8(255) as i8: -1
Potential Data Loss
fn main() {
// Large integer to smaller type
let large = 1000i32;
let small = large as i8; // Wraps: 1000 % 256 = 232, but as signed: -24
// Float precision loss
let precise = 3.999999999999;
let less_precise = precise as f32;
println!("1000 as i8: {}", small);
println!("f64: {}, as f32: {}", precise, less_precise);
// Infinity and NaN
let inf = f64::INFINITY;
let inf_as_int = inf as i32; // Undefined behavior in older Rust, now saturates
println!("Infinity as i32: {}", inf_as_int);
}
Output:
1000 as i8: -24
f64: 3.999999999999, as f32: 4
Infinity as i32: 2147483647
Safe Conversions
TryFrom and TryInto
use std::convert::TryFrom;
fn main() {
let large_number = 1000i32;
// Safe conversion that can fail
match i8::try_from(large_number) {
Ok(small) => println!("Converted successfully: {}", small),
Err(e) => println!("Conversion failed: {}", e),
}
let small_number = 100i32;
match i8::try_from(small_number) {
Ok(small) => println!("Converted successfully: {}", small),
Err(e) => println!("Conversion failed: {}", e),
}
// Using TryInto
let result: Result = large_number.try_into();
match result {
Ok(val) => println!("TryInto success: {}", val),
Err(_) => println!("TryInto failed for {}", large_number),
}
}
Output:
Conversion failed: out of range integral type conversion attempted
Converted successfully: 100
TryInto failed for 1000
Numeric Operations
Basic Arithmetic
fn main() {
let a = 10;
let b = 3;
println!("Addition: {} + {} = {}", a, b, a + b);
println!("Subtraction: {} - {} = {}", a, b, a - b);
println!("Multiplication: {} * {} = {}", a, b, a * b);
println!("Division: {} / {} = {}", a, b, a / b); // Integer division
println!("Remainder: {} % {} = {}", a, b, a % b);
// Floating point division
let x = 10.0;
let y = 3.0;
println!("Float division: {} / {} = {}", x, y, x / y);
}
Output:
Addition: 10 + 3 = 13
Subtraction: 10 - 3 = 7
Multiplication: 10 * 3 = 30
Division: 10 / 3 = 3
Remainder: 10 % 3 = 1
Float division: 10 / 3 = 3.3333333333333335
Overflow Behavior
fn main() {
// In debug mode, this panics
// In release mode, this wraps around
let max_u8 = 255u8;
// Explicit wrapping
let wrapped = max_u8.wrapping_add(1);
println!("255 + 1 (wrapping) = {}", wrapped); // 0
// Checked arithmetic
match max_u8.checked_add(1) {
Some(result) => println!("Result: {}", result),
None => println!("Overflow occurred!"),
}
// Saturating arithmetic
let saturated = max_u8.saturating_add(10);
println!("255 + 10 (saturating) = {}", saturated); // 255
// Overflowing returns tuple
let (result, overflow) = max_u8.overflowing_add(1);
println!("Result: {}, Overflow: {}", result, overflow);
}
Output:
255 + 1 (wrapping) = 0
Overflow occurred!
255 + 10 (saturating) = 255
Result: 0, Overflow: true
Numeric Constants and Methods
Built-in Constants
fn main() {
// Integer constants
println!("i32 MIN: {}", i32::MIN);
println!("i32 MAX: {}", i32::MAX);
println!("u8 MIN: {}", u8::MIN);
println!("u8 MAX: {}", u8::MAX);
// Float constants
println!("f64 MIN: {}", f64::MIN);
println!("f64 MAX: {}", f64::MAX);
println!("f64 EPSILON: {}", f64::EPSILON);
println!("f64 INFINITY: {}", f64::INFINITY);
// Math constants
println!("π: {}", std::f64::consts::PI);
println!("e: {}", std::f64::consts::E);
}
Output:
i32 MIN: -2147483648
i32 MAX: 2147483647
u8 MIN: 0
u8 MAX: 255
f64 MIN: -179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
f64 MAX: 179769313486231570000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
f64 EPSILON: 0.00000000000000022204460492503131
f64 INFINITY: inf
π: 3.141592653589793
e: 2.718281828459045
Useful Methods
fn main() {
let number = -42.7f64;
println!("Original: {}", number);
println!("Absolute: {}", number.abs());
println!("Floor: {}", number.floor());
println!("Ceiling: {}", number.ceil());
println!("Round: {}", number.round());
println!("Truncate: {}", number.trunc());
// Powers and roots
let base = 2.0f64;
println!("2^3 = {}", base.powf(3.0));
println!("√16 = {}", 16.0f64.sqrt());
println!("∛8 = {}", 8.0f64.cbrt());
// Checking special values
let nan = f64::NAN;
println!("Is NaN? {}", nan.is_nan());
println!("Is finite? {}", number.is_finite());
println!("Is infinite? {}", f64::INFINITY.is_infinite());
}
Output:
Original: -42.7
Absolute: 42.7
Floor: -43
Ceiling: -42
Round: -43
Truncate: -42
2^3 = 8
√16 = 4
∛8 = 2
Is NaN? true
Is finite? true
Is infinite? true
Common Pitfalls
Integer Division
fn main() {
// This might not do what you expect
let result = 5 / 2;
println!("5 / 2 = {}", result); // 2, not 2.5!
// For floating-point division
let correct = 5.0 / 2.0;
println!("5.0 / 2.0 = {}", correct); // 2.5
// Or cast first
let also_correct = 5 as f64 / 2 as f64;
println!("5 as f64 / 2 as f64 = {}", also_correct);
}
Floating-Point Comparison
fn main() {
let a = 0.1 + 0.2;
let b = 0.3;
// This might fail due to floating-point precision
println!("0.1 + 0.2 = {}", a);
println!("0.3 = {}", b);
println!("Are they equal? {}", a == b); // Might be false!
// Better comparison
let epsilon = f64::EPSILON;
let are_close = (a - b).abs() < epsilon;
println!("Are they close enough? {}", are_close);
}
Checks for Understanding
Question 1
What happens when you add 1 to the maximum value of u8?
Click to see answer
In debug mode, it panics. In release mode, it wraps around to 0. Use wrapping_add, checked_add, or saturating_add for explicit behavior.
Question 2
Why might this floating-point comparison fail?
if (0.1 + 0.2) == 0.3 {
println!("Equal!");
}
Click to see answer
Floating-point arithmetic has precision limitations. 0.1 + 0.2 might not exactly equal 0.3 due to binary representation. Use epsilon-based comparison instead.