2. Types, Operators, and Expressions
Variables and constants are the material a program works on; declarations say what they are; operators say what to do with them. This chapter covers the basic types and the rules for combining them, and ends with the one structural idea that separates Rust from C: almost everything is an expression.
2.1 Names and Shadowing
Names are letters, digits, and underscores, and cannot start with a digit. Case
matters. Rust is firm about style and will warn you into line: variables and
functions in snake_case, types in CamelCase, constants in SCREAMING_CASE.
A name can be declared again in the same scope. This is called shadowing:
#![allow(unused)]
fn main() {
let input = " 42 ";
let input = input.trim();
let input: i32 = input.parse().unwrap();
}
Three variables, one name, three types. The earlier ones still exist but can no
longer be reached. This is idiomatic Rust and it is not the same as assignment:
each let makes a new variable, so the value can change type and none of them
has to be mut. Use it when a value is being refined toward its final form, and
not to hold two unrelated things.
2.2 Integers
Rust has signed integers i8, i16, i32, i64, i128 and the unsigned
u8, u16, u32, u64, u128, where the number is the width in bits. It
also has isize and usize, which are as wide as a memory address on the
machine you compile for, 64 bits on anything you are likely to use. usize is
the type of an index and of a length, so it turns up whether or not you go
looking for it.
When you do not say, Rust picks i32. It is a good default: fast everywhere,
big enough for counting things.
Literals may be decimal, hexadecimal, octal, or binary, may contain underscores anywhere for legibility, and may carry the type as a suffix.
#![allow(unused)]
fn main() {
let a = 1_000_000;
let b = 0xff;
let c = 0b1010;
let d = 0o77;
let e = 42u8;
let f = 42_i64;
}
Choosing a width is a real decision and Rust will not make it for you. Use i32
unless you have a reason, u8 for bytes, usize for indices, u64 or i64
when the values might be large. Use the unsigned types to express that a
quantity cannot be negative, as the power function in Chapter 1 did.
Overflow
What happens when a value will not fit is a place where Rust makes a choice C does not. In C, signed overflow is undefined behavior and the compiler is permitted to assume it never happens. In Rust the behavior is specified, and which behavior you get depends on how you compiled:
fn main() {
let mut n: u8 = 250;
for _ in 0..10 {
n += 1;
println!("{n}");
}
}
Built with cargo run, this prints up to 255 and then stops the program:
255
thread 'main' panicked at ch02/src/bin/overflow.rs:4:9:
attempt to add with overflow
Built with cargo run --release, it wraps around to 0 and keeps going. The
checks cost time, so they are on during development and off in optimized builds.
That asymmetry is deliberate and it has a consequence: an overflow bug that only
shows up under real workloads will crash in testing and silently produce garbage
in production, which is still better than C but is not a guarantee.
When overflow is a case you actually have to handle, say so, and the behavior no longer depends on the build:
i32::MAX.checked_add(1) // None
i32::MAX.wrapping_add(1) // -2147483648
i32::MAX.saturating_add(1) // 2147483647
i32::MAX.overflowing_add(1) // (-2147483648, true)
Every arithmetic operator has these four forms. checked_ hands back an
Option that is None on overflow, wrapping_ does modular arithmetic,
saturating_ clamps at the limit, and overflowing_ returns the wrapped value
together with a flag. Every integer type also carries MIN and MAX.
Division
Integer division truncates toward zero and the remainder takes the sign of the
left operand, so -7 / 2 is -3 and -7 % 2 is -1. Division by zero is a
panic, not undefined behavior and not infinity.
2.3 Floating Point
f64 is the default and f32 exists when you need half the space. Literals
with a decimal point or an exponent are floats: 3.0, 1e10, 2.5f32.
The usual floating-point facts apply, since this is IEEE 754 like everywhere
else. 0.1 + 0.2 is 0.30000000000000004. f64::NAN is not equal to itself.
Rust makes that last one visible in the type system rather than leaving it as
folklore: floats implement PartialOrd but not Ord, so a Vec<f64> has no
.sort(), only .sort_by(|a, b| a.partial_cmp(b).unwrap()). When the sorting
method you expected is missing, this is why.
Methods live on the type, so it is (2.0f64).sqrt(), x.abs(), x.floor(),
x.powi(3), x.powf(0.5), x.is_nan(). There is no separate math library to
import.
2.4 Booleans and Characters
bool is true or false and nothing else converts to it. There is no truthy
or falsy: if x where x is an integer does not compile. Write if x != 0.
A char is a Unicode scalar value and occupies four bytes, so 'a', 'é',
'字', and '🦀' are each one char. This is not C’s char. For a single byte
you want u8, and b'A' is the byte literal for 65.
Because a char is a scalar value and a String is UTF-8 bytes, the two
disagree about length in a way you have to be aware of: 'é'.len_utf8() is 2.
Chapter 7 works through what that means for strings, which is the only place it
really bites.
2.5 Conversions
Rust never converts number types implicitly. Not integer to float, not i32 to
i64, not even when the conversion could not possibly lose anything. Three
mechanisms cover the ground.
as converts between any two numeric types and never fails, which means it can
lose information without saying so. 300u32 as u8 is 44, taking the low eight
bits. -1i32 as u8 is 255, reinterpreting the bits. 3.9f64 as i32 is 3,
truncating toward zero, and 1e10f64 as i32 is 2147483647, saturating at the
maximum rather than producing nonsense. Use as when the truncation is what you
meant, most often when indexing or working with bytes.
from and into convert where the conversion cannot lose anything, and are
therefore the ones to reach for by default:
#![allow(unused)]
fn main() {
let count: i32 = 7;
let wide = i64::from(count);
let exact = f64::from(count);
let owned = String::from("hello");
}
try_into converts where it might fail, and hands back a Result:
#![allow(unused)]
fn main() {
let big: u64 = 200;
let small: u8 = big.try_into().expect("does not fit in a byte");
}
Text converts through parse, which also hands back a Result because text is
not to be trusted:
#![allow(unused)]
fn main() {
let n: i32 = "42".parse().unwrap();
let n = "42".parse::<i32>().unwrap();
}
Both lines do the same thing. The second uses the turbofish, ::<T>, to say the
type at the call rather than in the declaration. You will see it whenever there
is no other place for the type to come from.
2.6 Tuples and the Unit
A tuple groups a fixed number of values of different types.
#![allow(unused)]
fn main() {
let point = (3, 4.5, 'x');
println!("{}", point.0);
let (a, b, c) = point;
}
Fields are reached by position with a dot, and a tuple can be taken apart by
pattern in a let. Tuples are how a function returns two things without a
struct being invented for the purpose, worth doing once or twice and not
habitually; by the third field you want a struct and Chapter 6 has one.
The empty tuple () is called the unit. It is the type of an expression that
has no value, and it is what a function returns when its signature has no arrow.
It is a real type and it costs nothing at runtime.
2.7 Constants and Statics
const names a value that is computed at compile time and substituted wherever
it is used:
#![allow(unused)]
fn main() {
const MAX_LINE: usize = 1000;
}
The type is required. A const has no address of its own and no lifetime
concerns, which makes it the right choice nearly always.
static names a value that lives at a fixed address for the whole run of the
program. Read-only statics are ordinary:
#![allow(unused)]
fn main() {
static GREETING: &str = "hello";
}
A mutable static is shared state that any thread could touch, so reading or
writing one requires unsafe, Rust declining to promise something it cannot
check. If you find yourself wanting one, you want a different design; the
standard library has the pieces, and this book does not need them.
2.8 Operators
Arithmetic is + - * / %, with no exponent operator: use x.pow(n) for
integers and x.powf(y) for floats. Comparison is == != < <= > >= and works
only between two values of the same type. Logic is && || !, and && and ||
short-circuit. Bitwise is & | ^ ! << >>, where ! is the complement rather
than C’s ~, and doubles as boolean not; which one it means follows from the
type of its operand.
Assignment is =, and every arithmetic and bitwise operator has a compound form
such as += and <<=. There is no ++ or --, and assignment is not an
expression: a = b = c does not compile and neither does if (x = 1), which
removes a whole family of C bugs by construction.
Precedence, from tightest to loosest:
| Level | Operators |
|---|---|
| 1 | paths ::, method calls, field access, indexing [] |
| 2 | ? |
| 3 | unary - ! * & &mut |
| 4 | as |
| 5 | * / % |
| 6 | + - |
| 7 | << >> |
| 8 | & |
| 9 | ^ |
| 10 | | |
| 11 | == != < > <= >= |
| 12 | && |
| 13 | || |
| 14 | .. ..= |
| 15 | = and the compound assignments |
Two entries are not where a C programmer expects. as binds tighter than
multiplication, so 2 + 3 as f64 converts only the 3. And the bitwise operators
bind tighter than the comparisons, so x & y == 0b1000 means (x & y) == 0b1000, which is what you wanted and what C refuses to give you. Comparisons do
not chain: a < b < c is a type error rather than a surprise.
2.9 Expressions and Statements
Rust distinguishes expressions, which have a value, from statements, which do not. The distinction is easy to state and worth taking seriously, because more things are expressions than you are used to.
A block in braces is an expression whose value is its last expression, with no semicolon on that last line:
#![allow(unused)]
fn main() {
let x = {
let a = 3;
let b = 4;
a * a + b * b
};
}
if is an expression:
#![allow(unused)]
fn main() {
let n = 7;
let size = if n < 10 { "small" } else { "large" };
}
Both arms must have the same type, and there is no ternary operator because this
is the ternary operator. match is an expression in the same way, and Chapter 3
uses it that way constantly.
A semicolon turns an expression into a statement and throws its value away. That
is the rule behind the most common beginner error in Rust: a function whose
final line ends in a semicolon returns () rather than the value you computed,
and the compiler tells you so while pointing at the semicolon.
Certain expressions never produce a value at all, because control leaves before
they finish: return, break, continue, and panic!. Their type is written
! and it fits anywhere, so this works:
#![allow(unused)]
fn main() {
fn show(text: &str) {
let n: i32 = match text.parse() {
Ok(v) => v,
Err(_) => return,
};
println!("{n}");
}
}
The second arm has no i32 to offer, and does not need one.
Exercise 2-1. Print the minimum and maximum of every integer type using
MIN and MAX, and work out from the output which types are signed.
Exercise 2-2. Write a function that takes an i64 and returns its absolute
value without using .abs(). What should it do for i64::MIN? Look at what
.abs() itself does, and at .checked_abs().
Exercise 2-3. Write a function bits(n: u32) -> u32 that counts the one-bits
in a number using & and >>. Compare your answer with n.count_ones().
Exercise 2-4. 0.1 + 0.2 == 0.3 is false. Write a function that compares
two floats to within a tolerance, and find a pair of values for which your
function gives an answer you would call wrong.