Rust Types, Operators and Expressions¶
Rust types determine which values and operations a program can represent. This lesson expands the small classifier from lesson 2: choose units, distinguish integer from floating-point arithmetic, and return values from blocks without silently losing information.
Prerequisites and outcome¶
Complete variables and control flow. You should be able to explain why a numeric conversion is needed and distinguish a statement from an expression. No hardware readings or external crates are required.
Choose Rust types before choosing operators¶
| Type family | Examples | Important distinction |
|---|---|---|
| Signed integers | i8, i16, i32, i64, i128, isize | Can represent negative values |
| Unsigned integers | u8, u16, u32, u64, u128, usize | Cannot represent negative values |
| Floating point | f32, f64 | Approximate many real-number values |
| Boolean and character | bool, char | A char is a Unicode scalar value, not a byte or whole string |
| Tuple and array | (i32, bool), [i32; 3] | Tuples can mix types; arrays have one element type and fixed length |
The bit width is part of an integer type's range. usize and isize follow the target pointer width; they are not portable synonyms for u64 and i64. Unsuffixed integers commonly default to i32 and floats to f64 when other constraints do not determine the type.
For example, u8 holds 0 through 255, while i8 holds -128 through 127. Choose a signed type for readings that may be negative, not merely because today's fixture is positive. A type annotation constrains a binding; a literal suffix such as 255_u8 constrains that literal. Decimal 16, hexadecimal 0x10, binary 0b1_0000 and octal 0o20 express the same integer value.
A tuple such as (3_usize, 46.7_f64) groups different types and has positional fields (report.0, report.1). An array such as [0_i32; 3] contains three elements of the same type. Array indices start at zero, so a three-element array has valid indices 0, 1 and 2. Its length is part of its type.
See the official data types chapter. The primitive types reference is the route to more precise rules.
Build a typed fixture report¶
Replace src/main.rs with:
Expected program output:
None means the checked addition cannot represent its result. It is an Option value, explained in lesson 9. Debug formatting {:?} lets us inspect it without handling it as a normal numeric result.
Expressions produce values¶
Arithmetic, comparisons, function calls and many blocks are expressions. A block's final expression supplies its value; a semicolon discards that value. A block without a returned value has type (), called unit. Functions without an explicit return type also return unit.
In celsius, the parameter's type fixes the input unit's representation and -> f64 fixes the result type. A function's signature does not encode the physical unit itself: the name millidegrees and documentation still matter. The compiler would accept a wrongly scaled i32, so tests must check the conversion rule.
The let label = { ... }; example uses this distinction: the inner if supplies the label, while the outer semicolon completes the declaration. return value; exits a function explicitly, so it is different from merely adding a semicolon to its final expression.
For a precise description of the final operand, see block expressions. Avoid the shortcut “every semicolon makes a function return unit”: a return statement already transfers control.
The never type ! describes computations that do not return normally, such as panic!. It is different from unit: a function returning () finishes with a value, while a diverging computation never supplies one. We revisit this distinction with error handling.
Arithmetic, conversions and overflow¶
Integer division truncates toward zero; converting the numerator first preserves the fractional result. Operators such as +, -, *, / and % operate on compatible operand types. Comparisons produce bool; && and || short-circuit Boolean expressions. Bitwise operators such as & and | are different operations, not substitutes for short-circuit conditions.
| Operation | Example | Result or interpretation |
|---|---|---|
| Remainder | -7_i32 % 3 |
-1; not a nonnegative modulo operation |
| Comparison | 46_700_i32 < 60_000 |
true |
| Logical negation | !true |
false |
| Short-circuit condition | false && expensive_check() |
Does not call the right-hand function |
| Integer bit mask | 0b1100_u8 & 0b1010 |
0b1000 |
| Left shift | 1_u8 << 3 |
8 |
Use parentheses to show intent: (2 + 3) * 4 is 20, while 2 + 3 * 4 is 14. Division or remainder by zero panics for integers, regardless of overflow-check settings. Do not apply ordinary unsigned wrapping assumptions to division or invalid shifts.
Rust does not automatically mix integer and floating-point operands. f64::from provides a supported conversion for our i32. An explicit as cast is not an input validator: narrowing can discard bits, and some numeric conversions lose precision. Use checked conversions such as TryFrom when a value might not fit; we introduce their error handling later.
Do not rely on plain addition to implement a deliberate wrapping policy. With overflow checks enabled, overflowing integer arithmetic panics; ordinary release profiles normally disable those checks. Cargo profiles can change that setting. Use checked_add, wrapping_add or saturating_add to state the intended policy explicitly. See operator expressions.
Deliberately failing example¶
In a separate scratch project, replace src/main.rs with this complete program:
Run cargo check. It should reject the out-of-range literal, not produce a wrapped count. This is a compile-time range error, distinct from arithmetic overflow at runtime. Do not leave this program in the working project.
Exercises and troubleshooting¶
- Convert the third fixture: expect
-0.5, not zero. Compare with integer division of-500 / 1_000. - Add a fourth array element without updating
[i32; 3]: expect an array-length type error. Then fix the annotation. - Add a semicolon after the inner
ifexpression: the block now yields unit, which cannot be printed with the label's display format. Restore it.
If you see incompatible numeric types, inspect the operands and units before adding casts. If indexing panics, check the index against the array length; compiling does not guarantee every runtime index is valid.
Verification and next step¶
On October 10, 2026, this lesson was verified on a Raspberry Pi 4B with 64-bit user space, kernel 6.18.50+rpt-rpi-v8, Rust and Cargo 1.99.0, and edition 2024. In an isolated Cargo project, check, run, all 6 tests and the formatting check passed. Output matched the example in debug and release builds. The deliberately failing program and the array-length and semicolon exercises were checked separately and rejected for the expected reasons. No live sensor or performance verification is claimed.
Continue with ownership, moves, Copy and Clone.