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

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.


← PreviousNext →