The integer told the float, “You’re being irrational.”
The float replied, “At least I have a point.”
In Rust, numbers don't always get along. Numeric types are kept distinct, and Rust won't let you mix them without an explicit conversion. The types themselves are familiar:
let byte: u8 = 255; // a single byte, holds 0 to 255
let b: u32 = 42; // unsigned, so it can't go negative
let a: i32 = -42; // signed 32-bit, the everyday default
let big: i64 = 9_000_000_000; // 64-bit, for numbers too large for i32
let i: usize = 0; // the type for sizes and indices
let price: f64 = 19.99; // floating point (f32 is the smaller one)
These are the numeric types you'll encounter most often. What makes Rust different is how strictly it treats them.
Languages handle integer overflow differently: Java wraps, Python's integers grow to hold the result, and C wraps unsigned arithmetic but leaves signed overflow undefined. Rust rejects overflow it detects at compile time by default. At runtime, overflow panics in debug builds and wraps in release builds by default.
let hp: u8 = 200;
let bonus: u8 = 100;
// The compiler detects this overflow and rejects the addition.
// If overflow occurs at runtime, debug builds panic and release builds wrap by
// default.
let total = hp + bonus;
Storing that sum in an unsigned 8-bit integer in C would give 44 instead of 300. Imagine you play Diablo and pick up a bonus item and your health bar suddenly drops from 200 to 44.
Rust's integer methods let you choose what happens when a result does not fit:
a.saturating_add(b) clamps at the maximum, so 255 stays 255.a.checked_add(b) returns None on overflow, so you can handle it yourself.a.wrapping_add(b) opts back into wraparound, for the times you actually want
it.If you want the health bar to stop at 255, use saturating_add.
I prefer to choose the overflow behavior explicitly rather than rely on the release default. These methods also give you the same behavior in debug and release builds.
Rust never mixes numeric types for you. u32 + i32 won't compile, and you
can't multiply a u32 by an f64 either. You convert explicitly: as performs
a cast that may lose information, .into() handles infallible conversions, and
.try_into() returns a Result when a conversion can fail.
let count: u32 = 42;
let price: f64 = 19.99;
// Convert count to f64 before multiplying.
let total = price * count as f64;
Parsing a string can fail because the input might not be a number at all, so
parse hands back a Result.
let parsed = "123".parse::<u32>();
The ::<u32> tells parse which number type you want. The result still
represents either success or failure, rather than a bare u32.
We'll talk about Result later. For now, .unwrap_or(fallback) extracts a
successful value or supplies the fallback if parsing failed. Choose that
fallback to match the behavior your caller needs.
A u8 holds 0 to 255. Add 100 to 200 and a debug build panics, while a release
build wraps to 44. Neither is what you want from a health bar.
saturating_add is the fix. It does the addition but clamps at the type's
maximum instead of overflowing, so stacking buffs tops out at 255 rather than
wrapping around.
Vim user? Open Settings in the top bar and turn on Vim Keybindings to use Vim in the exercise editors. Your choice is saved in this browser.
Useful from the Standard Library
u8::saturating_addreturns the sum, or the type's maximum if the true sum wouldn't fit.
current + gain would panic (debug) or wrap (release) once the sum
passes 255.saturating_add on the integer types: it clamps at the maximum
instead of overflowing.
/// Adds `gain` health to `current`, capping at `u8::MAX` (255) instead of
/// overflowing.
///
/// `saturating_add` performs the addition but clamps the result at the type's
/// maximum, so a big stack of buffs tops out at 255 rather than wrapping around
/// to a small number (which is what a plain `+` would do in a release build).
fn add_health(current: u8, gain: u8) -> u8 {
current.saturating_add(gain)
}
#[test]
fn test_add_health() {
assert_eq!(add_health(100, 50), 150);
assert_eq!(add_health(200, 100), 255);
assert_eq!(add_health(255, 1), 255);
assert_eq!(add_health(0, 0), 0);
}
Rust never converts between numeric types implicitly. If you want to multiply a
u32 by an f64, you have to convert one of them first. You can use as for
that.
The function takes base damage as a u32 and an f64 bonus percentage, then
returns the final damage as a u32. A bonus of 50.0 means adding half of the
base damage again, whether it came from a critical hit, equipment, or some other
modifier. Keep the calculation in f64 so the fractional percentage is not
lost, then convert the final damage back to u32.
Converting the final value back to u32 truncates the fractional part toward
zero. That matches games which use whole HP, so 8.085 damage becomes 8, not
9.
Useful Resources
asis the cast operator.1.7_f64 as u32is1, not2, because the cast truncates.
base is u32 and bonus_percent is f64, so you can't multiply them
directly. Cast base with as f64 first.u32 for the return value, use a plain as u32 cast. That
truncates the fractional part (drops any fractional HP), so the 15.5%
example returns 8.
/// Applies a damage bonus to a base damage value.
///
/// The bonus is a percentage *added on top* of the base, so `50.0` means
/// "+50%": a base of `100` becomes `150`. Think of it as a critical-hit bonus,
/// an equipment buff, or any other damage modifier expressed as a percentage.
/// Fractional HP is dropped (truncated toward zero), which is what most games
/// do; half-HP doesn't exist.
fn damage_with_bonus(base: u32, bonus_percent: f64) -> u32 {
let multiplier = 1.0 + bonus_percent / 100.0;
(base as f64 * multiplier) as u32
}
#[test]
fn test_damage_with_bonus() {
// No bonus: damage is unchanged.
assert_eq!(damage_with_bonus(100, 0.0), 100);
// Classic +50% bonus (e.g. a critical hit).
assert_eq!(damage_with_bonus(100, 50.0), 150);
// +100% = double damage.
assert_eq!(damage_with_bonus(80, 100.0), 160);
// Fractional HP truncates toward zero: 7 + 7 * 0.155 = 8.085 → 8, not 9.
assert_eq!(damage_with_bonus(7, 15.5), 8);
// 8.75 still truncates to 8; rounding would give 9.
assert_eq!(damage_with_bonus(7, 25.0), 8);
}
str::parse turns text into the type you ask for. It returns a Result because
the input might not be valid for that type.
Returning 0 on failure is a bad idea in real code because it makes valid
input "0" indistinguishable from garbage. In Rust, Option and Result
preserve the distinction between valid zero and invalid input. Use Result when
a missing value is an error you need to explain. Here, I'll keep error handling
out of the exercise: return 0 if the string isn't a valid number.
u32 can't be negative, so "-5".parse::<u32>() fails and you should return
0 for it too.
Useful from the Standard Library
str::parseturns a string into a type you choose. It returns aResultbecause the input might not be valid.Result::unwrap_orhands back the value onOk, or the fallback you give it onErr. Useful for the "just give me a number" path here.
parse needs a target type. You can specify it with ::<u32>, or let the
function's return type guide inference once the parsed value is returned.
/// Parses a string into a positive integer. Returns the number if valid, 0 if
/// invalid.
fn parse_positive_integer(input: &str) -> u32 {
input.parse().unwrap_or(0)
}
#[test]
fn test_parse_positive_integer() {
assert_eq!(parse_positive_integer("123"), 123);
assert_eq!(parse_positive_integer("0"), 0);
assert_eq!(parse_positive_integer("invalid"), 0);
assert_eq!(parse_positive_integer("-5"), 0);
}
You chose how to handle overflow, converted numeric types explicitly, and used a fallback when parsing failed.
What We Learned
- If you add two numbers and the result doesn't fit the type, Rust panics in debug builds and wraps in release builds. When overflow is possible, pick the behavior you want:
saturating_add(clamp),checked_add(return anOption), orwrapping_add(wrap around).- Rust never mixes numeric types for you. If the types don't match, you must convert them explicitly.
ascasts may lose information;.into()handles infallible conversions, while.try_into()returns aResultwhen a conversion can fail.as u32on a float truncates toward zero (1.7 as u32is1);f64::roundrounds to the nearest integer. We used truncation for the damage example.str::parse()turns text into a value of the type you ask for. It returns aResult, and.unwrap_or(...)supplies a fallback when parsing fails.
Extra practice to explore at your own pace. These chapters do not count toward course progress.