Why this matters
Measurements, money estimates, physics and graphics need fractional numbers; every decision a program makes comes down to a true-or-false value; and text is built from characters. These three scalar types each have a surprise that catches beginners: floats are not exact, && may skip its right side, and a Rust char is not a byte. Learn the surprises now and they will not cost you an afternoon later.
Floating-point numbers
Rust has two float types: f64 (8 bytes, about 15-16 significant decimal digits, the default) and f32 (4 bytes, about 7 digits, used where memory matters, as in graphics). Both follow the IEEE 754 standard that every modern CPU implements, so they behave the same as floats in C, Python or JavaScript.
fn main() {
let x = 7.0 / 2.0;
let y: f32 = 1.0 / 3.0;
let z = 1.0 / 3.0;
println!("{x} {y} {z}");
println!("{}", 2.0_f64.sqrt());
println!("{} {} {}", 2.7_f64.floor(), 2.5_f64.round(), (-2.7_f64).abs());
println!("{:.3}", std::f64::consts::PI);
}
3.5 0.33333334 0.3333333333333333
1.4142135623730951
2 3 2.7
3.142
Float literals need a dot (7.0, or 7. or the suffix 7f64); 7 alone is an integer, and Rust never mixes the two automatically. Math functions are methods: x.sqrt(), x.powi(3), x.sin(), x.max(y).
What is inside an f64
An f64 is scientific notation in base 2: a sign bit, an 11-bit exponent and a 52-bit fraction (the "mantissa"):
1 bit 11 bits 52 bits
+------+-----------+----------------------------+
| sign | exponent | fraction | value = (-1)^sign * 1.fraction * 2^(exponent-1023)
+------+-----------+----------------------------+
With 52 bits of fraction, only numbers that are sums of powers of two (with a limited span) are exact. 0.5, 0.25 and 0.375 are exact. 0.1 is 0.000110011001100... in binary, repeating forever, so it is rounded:
fn main() {
println!("{}", 0.1 + 0.2);
println!("{}", 0.1 + 0.2 == 0.3);
println!("{:.20}", 0.1);
let close = (0.1_f64 + 0.2 - 0.3).abs() < 1e-9;
println!("{close}");
}
0.30000000000000004
false
0.10000000000000000555
true
Rules that follow from this:
- Do not compare floats with
==after arithmetic. Compare with a tolerance. - Do not use floats for money. Count cents in an integer (
i64) instead. - Format with a precision (
{:.2}) when printing for people.
Special values
IEEE 754 has values for results that are not ordinary numbers:
fn main() {
let inf = 1.0 / 0.0;
let nan = 0.0_f64 / 0.0;
println!("{inf} {} {nan}", -inf);
println!("{}", nan == nan);
println!("{}", nan.is_nan());
}
inf -inf NaN
false
true
Float division by zero does not panic (unlike integers); it gives infinity. NaN ("not a number") results from undefined operations and is not equal to anything, not even itself. Use is_nan() to test for it. This is also why floats cannot be sorted with the plain sort method or used as keys in some collections, which you will meet in module 9.
Booleans
bool has two values, true and false, and occupies one byte. Comparisons produce bools:
| operator | meaning |
|---|---|
== != | equal, not equal |
< <= > >= | ordering |
&& | and |
|| | or |
! | not |
fn main() {
let age = 34;
let member = true;
let discount = age >= 65 || member;
let adult_non_member = age >= 18 && !member;
println!("{discount} {adult_non_member}");
println!("{}", true as i32 + true as i32);
}
true false
2
&& and || short-circuit: if the left side already decides the answer, the right side is not evaluated at all. In b != 0 && a / b > 2, the division never runs when b is zero. Order your conditions so the cheap or protective test comes first.
Rust has no "truthiness": if 1 { ... } is a type error. Conditions must be bool, so write if count != 0.
Characters
A char is a single Unicode scalar value: any character from any script, plus symbols and emoji. It is always 4 bytes, because it holds the character's code point, a number up to 0x10FFFF.
fn main() {
let letter = 'R';
let accented = 'é';
let heart = '♥';
println!("{letter} {accented} {heart}");
println!("{} {} {}", letter as u32, accented as u32, heart as u32);
println!("{}", std::mem::size_of::<char>());
println!("{} {}", letter.is_alphabetic(), '7'.to_digit(10).unwrap());
println!("{}", 'a'.to_ascii_uppercase());
}
R é ♥
82 233 9829
4
true 7
A
Note the single quotes: 'R' is a char, "R" is a string (text of any length). This matters: in a string, Rust stores characters in UTF-8, where each takes 1 to 4 bytes, so a string is not an array of chars. Module 6 explains this properly. For now: one character, single quotes, 4 bytes, holds a Unicode code point.
Casting with as
as converts between primitive types. It always succeeds, which means it sometimes changes the value:
fn main() {
println!("{}", 3.99_f64 as i32);
println!("{}", -3.99_f64 as i32);
println!("{}", 1e20_f64 as i32);
println!("{}", 300_i32 as u8);
println!("{}", -1_i32 as u32);
println!("{}", 65_u8 as char);
println!("{}", 'A' as u8);
println!("{}", 7 as f64 / 2.0);
}
3
-3
2147483647
44
4294967295
A
65
3.5
- float to integer: truncates toward zero, saturates at the type's limits, NaN becomes 0
- integer to smaller integer: keeps the low bits (300 = 0x12C, low byte 0x2C = 44)
- signed to unsigned of the same size: same bits, reinterpreted (-1 is all ones)
u8tocharandcharto integer: by code point
Use as when you know the value fits or when truncation is what you want. When it might not fit, the checked conversions u8::try_from(x) and friends (module 8) are the safe choice.
Common mistakes
Mixing integers and floats:
fn main() {
let c = 37.0;
let f = c * 9 / 5 + 32;
println!("{f}");
}
error[E0277]: cannot multiply `{float}` by `{integer}`
--> main.rs:3:15
|
3 | let f = c * 9 / 5 + 32;
| ^ no implementation for `{float} * {integer}`
|
= help: the trait `Mul<{integer}>` is not implemented for `{float}`
= help: the following other types implement trait `Mul<Rhs>`:
`&f128` implements `Mul<f128>`
`&f128` implements `Mul`
`&f16` implements `Mul<f16>`
`&f16` implements `Mul`
`&f32` implements `Mul<f32>`
`&f32` implements `Mul`
`&f64` implements `Mul<f64>`
`&f64` implements `Mul`
and 57 others
Write 9.0, 5.0, 32.0, or convert integer variables with as f64.
Double quotes for a char: let c: char = "a"; is E0308, expected char, found &str.
Comparing floats with == gives false for values that "should" be equal. Use a tolerance.
Assuming as checks: big as u8 silently drops high bits. If that is not what you want, it is a bug the compiler cannot see.
Recap
f64(default) andf32are IEEE 754 binary floats: fast, approximate. Compare with a tolerance; count money in integers.- Float division by zero gives
inf; undefined results giveNaN, which equals nothing. boolistrue/false;&&and||short-circuit; conditions must be real bools.charis a 4-byte Unicode scalar value in single quotes.asconverts primitive types without checks: truncation, saturation and bit reinterpretation are all possible.
Going further: print f64::EPSILON, f64::MAX and 0.1_f32 as f64, and explain the last one. Search for "What Every Computer Scientist Should Know About Floating-Point Arithmetic" for the deep version.