Comparing two numbers of different types¶
Level: 101 → 201 · working knowledge
One line: < and == take the same number type on both sides, so comparing an i32 with a u16 means converting one of them first. Pick a type that holds every value of both, convert with From, and the comparison can't lie. as compiles for every pair and gives wrong answers for some.
fn main() {
let a: i32 = 10;
let b: u16 = 100;
println!("{}", a < i32::from(b)); // true
println!("{}", a < b.into()); // true
println!("{}", i64::from(-1_i32) < i64::from(3_000_000_000_u32)); // true
}
The refusal¶
Without the conversion, rustc stops at the comparison:
error[E0308]: mismatched types
--> mixed_types.rs:4:24
|
4 | println!("{}", a < b);
| - ^ expected `i32`, found `u16`
| |
| expected because this is `i32`
|
help: you can convert a `u16` to an `i32`
|
4 | println!("{}", a < b.into());
| +++++++
The error is a type mismatch, not a missing trait. < calls PartialOrd::partial_cmp ↗, and the trait does take a type parameter for the right-hand side, PartialOrd<Rhs = Self>. std implements it for i32 with Rhs = i32 and nothing else, so the compiler knows exactly what b should have been. The comparison is refused because std has no i32 < u16 impl. The language has no general rule against comparing different types: std does compare String == &str and Vec<i32> == [i32; 3], because it implements those pairs (the claims page runs both).
The suggested fix is b.into(), not b as i32, and the difference matters. .into() compiles only when a lossless From exists. When it doesn't, you get an error instead of a wrong answer:
error[E0277]: the trait bound `i32: From<u32>` is not satisfied
--> no_common_from.rs:4:26
|
4 | println!("{}", a < b.into());
| ^^^^ the trait `From<u32>` is not implemented for `i32`
|
= help: `i32` implements trait `From<T>`:
From<bool>
From<i16>
From<i8>
From<u16>
From<u8>
= note: required for `u32` to implement `Into<i32>`
That list is the lesson in miniature. An i32 can take any u16 or u8, because their whole range fits. It can't take a u32, whose top half doesn't fit, and no amount of casting changes that.
Pick the type that holds both¶
| the two sides | compare in | write |
|---|---|---|
u16 and i32 |
i32 |
a < i32::from(b) |
i32 and u32 |
i64 |
i64::from(a) < i64::from(b) |
i64 and u64 |
i128 |
i128::from(a) < i128::from(b) |
usize and u64 |
u64, fallibly |
u64::try_from(len) — std has no From either way, since usize has no fixed width |
i128 and u128 |
nothing wider | a < 0 \|\| a.cast_unsigned() < b |
i32 and f64 |
f64 |
f64::from(a) < x — every i32 is exact in an f64 |
i64 and f64 |
none exists | compare the whole part and the fraction separately (section 4 below) |
The rule behind every row: the target type must hold every value of both types, sign included. Width alone isn't enough. u64 is wider than i32 and still has no place for -1.
What the program prints¶
Verified output of comparing_two_number_types.rs — regenerated by tools/run_examples.py, never hand-typed.
1. THE WORKING ANSWER: CONVERT ONE SIDE, LOSSLESSLY
a: i32 = 10, b: u16 = 100
a < i32::from(b) : true
a < b.into() : true
a.cmp(&i32::from(b)) : Less
i32::from(u16) compiles only because every u16 fits in an i32.
2. PICK THE TYPE THAT HOLDS BOTH RANGES
u16 and i32 i32 -1 < 65535 : true
i32 and u32 i64 -1 < 3000000000 : true
i64 and u64 i128 -1 < 18446744073709551615 : true
usize and u64 u64 3 > 2 : true
no From either way, so u64::try_from(len) = Ok(3)
i128 and u128 none -1 < 1 : true
no wider type exists: test the sign, then compare as u128
3. THE SAME COMPARISONS WITH `as`, AND WHERE THEY LIE
-1i32 as u32 < 1u32 : false -1 became 4294967295
-1i32 as u64 < 1u64 : false a WIDER type, and -1 became 18446744073709551615
-1i32 < 3_000_000_000 as i32 : false 3000000000 became -1294967296
widening is safe when the TARGET holds every value of the source:
width is not enough, the sign has to fit too. From only exists
where it does, so i32::from(b) cannot make this mistake.
4. INTEGER AND FLOAT: f64 HOLDS EVERY i32, NOT EVERY i64
f64::from(i32::MAX) = 2147483647 exact
n = 9007199254740993 (2^53 + 1), f = 9007199254740992.0 (2^53)
n as f64 == f : true <- two different numbers, 'equal'
n as f64 > f : false
cmp_i64_f64(n, f) : Some(Greater) <- exact
cmp_i64_f64(3, 3.5) : Some(Less)
cmp_i64_f64(-3, -3.5) : Some(Greater)
cmp_i64_f64(i64::MAX, 2^63): Some(Less)
cmp_i64_f64(0, NAN) : None
5. try_into() FOR A CONVERSION THAT CANNOT FAIL
b.try_into() as i32 has type core::result::Result<i32, core::convert::Infallible>
Infallible has no values, so that unwrap() can never panic.
the conversion that CAN fail is the other direction:
u16::try_from(70_000i32) = Err(TryFromIntError(PosOverflow))
u16::try_from(-1i32) = Err(TryFromIntError(NegOverflow))
The as versions, and where they lie¶
Section 3 is the trap. Each as line compiles without a warning and prints false for a comparison that is true:
-1i32 as u32 < 1u32: the same bits read as unsigned, so-1becomes 4294967295.-1i32 as u64 < 1u64: casting to a larger type doesn't help, because the sign still has nowhere to go. Advice to "cast the smaller type to the larger one" is right only when the larger type also covers the smaller one's sign.3_000_000_000u32 as i32: the unsigned value doesn't fit, so it wraps to -1294967296 and-1 <it is false.
Casting with as has the full table of what as does. Clippy's pedantic group catches the pattern: cast_lossless says a u16 as i32 "can be expressed infallibly using From", and cast_sign_loss warns that i32 as u32 "may lose the sign of the value". Strict clippy switches them on.
Integers against floats¶
f64 holds every i32 exactly (f64::from exists), but not every i64: from 2⁵³ up, neighbouring floats are 2 or more apart. So (2⁵³ + 1) as f64 rounds to 2⁵³, and section 4 shows two different numbers comparing equal. No common type holds both, so the program's cmp_i64_f64 splits the float instead: reject NaN, answer directly when the float is beyond i64's range, then compare the whole part as integers and let the fraction break a tie. That's what Python does for every int < float, without being asked.
try_into() when nothing can fail¶
b.try_into().unwrap() on a u16 headed for an i32 compiles and works, but its error type is Infallible ↗ (section 5). That's an enum with no values, so the unwrap() guards against something that can't happen. Use i32::from(b) when the conversion can't fail, and save try_from for the direction that can: u16::try_from(-1_i32) is Err(TryFromIntError(NegOverflow)).
If you are coming from another language¶
Python. Python has no refusal here, and needs none. Its integers have no width, and int < float compares the two exact values: 2**53 + 1 == float(2**53 + 1) is False, where Rust's as f64 says equal. So everything section 2 does by hand, Python does for you. What Python keeps, like Rust, is the refusal for unrelated types: 1 < "2" raises TypeError. What transfers back is the idea that a comparison should be between values, with the conversion chosen so no value is lost. See comparing an int with a float ↗ in the Python library.
C. C compiles every mixed comparison and converts first, by rules you have to know. int < unsigned short works, because the unsigned short is promoted to int. That's C's integer promotion, and where the word "promotion" comes from. But -1 < 1u is false: the usual arithmetic conversions turn the int into an unsigned int, and -1 becomes UINT_MAX. That's section 3's first as line, applied by the compiler without being asked, with -Wsign-compare as the only warning. Rust refuses the comparison C gets wrong and makes you choose the type. See comparing signed and unsigned ↗ in the C library.
ABAP. ABAP compares mixed numeric operands in the highest-ranked of their types: decimal floating point, then F, then P, then INT8, then I. So IF lv_int < lv_packed compares in P and is exact, while a P against an F compares in F, where a 31-digit packed number can round. That's the i64 against f64 row above. The habit that transfers is knowing which type the comparison happens in. What changes is that Rust never picks it for you, so the rounding case can't happen without an as you wrote yourself. (Not machine-checked; CI cannot run ABAP. Check the comparison-type rules in your release's documentation.)
See also¶
- What Rust in Action §2.3.3 says about comparing numbers, run: the book section this page came from, claim by claim
- Casting with
as: the four silent losses behind section 3 TryFromandTryInto: the conversions that can fail- What a float actually stores: why 2⁵³ + 1 has no
f64, and whyf64has noOrd - What
i128is exact about: the type row three compares in - The comparison traits:
PartialEq<Rhs>andPartialOrd<Rhs>, and whyString == &strcompiles - Machine numbers ↗ in the math library: the finite set an
f64is a member of
Po polsku¶
Porównanie (comparison) dwóch liczb różnych typów — i32 z u16 — w Ruscie się nie kompiluje: error[E0308]: mismatched types. To nie jest zakaz języka, tylko brak implementacji. Operator < to cecha (trait) PartialOrd<Rhs>, a biblioteka standardowa implementuje ją dla liczb tylko wtedy, gdy po obu stronach stoi ten sam typ. Dla innych par, np. String == &str, implementacje istnieją i porównanie działa.
Rozwiązanie to wybrać typ wspólny, który mieści wszystkie wartości obu typów, łącznie ze znakiem, i przekonwertować bezstratnie przez From: i32::from(b), a dla pary i32 i u32 — i64::from po obu stronach. Rzutowanie as kompiluje się zawsze i czasem kłamie: -1i32 as u64 < 1u64 daje false, bo „większy typ” nie ma miejsca na minus. Polskie materiały o C nazywają to promocją typów (integer promotion), i to w C -1 < 1u też daje false. Python porównuje int z float dokładnie i nie potrzebuje żadnej konwersji.
Szukaj po polsku: porównywanie liczb różnych typów · rzutowanie typów · promocja typów w C · rust E0308 mismatched types · rust compare i32 u32 · rust clippy cast_lossless