Rust · Beginner
Data Types in Rust
In short: Every Rust value has a type fixed at compile time. Scalars are integers (i32, u64 and so on), floats (f32, f64), bool and char; tuples and arrays group values. Numeric types never convert implicitly, so mixing them needs an explicit as cast.
Types are fixed and explicit
Rust is statically typed: the compiler knows the type of every value before the program runs, and it never changes. Most of the time you do not write the types, because the compiler infers them from literals and from how a value is used, but they are always there. When two types meet in an expression they must match exactly. Adding an i32 to an i64 is an error, not a silent widening, and dividing an integer by a float is an error too. The fix is an explicit conversion, usually with the as keyword, and the rule exists so that a program's arithmetic means what it says on every platform.
Integers come in signed and unsigned flavours of several widths: i8, i16, i32, i64, i128 and their unsigned partners u8 to u128. isize and usize match the machine's pointer width and are what Rust uses for indexes and lengths, which is why .len() returns a usize. An unconstrained integer literal defaults to i32. Literals may carry a suffix (255u8) and underscores for readability (4_000_000_000). Integer division truncates toward zero, so 17 / 5 is 3, and % gives the remainder. If an operation overflows its type, a debug build panics and a release build wraps around; when overflow is a real possibility, use methods such as checked_add or saturating_add to say what you want.
Floats are f32 and f64; a literal like 2.5 is an f64. bool is true or false and nothing else: Rust has no idea of a truthy number. char is a single Unicode scalar value written in single quotes, four bytes wide, so 'é' is one char even though it occupies two bytes inside a string. Text lives in two types, the borrowed string slice &str that a literal has, and the owned, growable String; the ownership lesson explains the relationship between them.
Tuples group a fixed number of values of possibly different types: ("Bed 7", 2.5, true). You reach the parts with .0, .1 and so on, or destructure the whole tuple into names with let (name, width, sunny) = plot;. The empty tuple () is the type of a function that returns nothing. Arrays group a fixed number of values of one type, [i32; 5], stored inline with the length as part of the type, so an array cannot grow. Indexing starts at 0, and an index past the end stops the program with a panic rather than reading memory it should not. For a list that grows, use Vec, covered in collections and iterators.
Input arrives as text, so reading a number means parsing. "42".parse::<u32>() converts a string to a u32 and returns a Result, because the text may not be a number; in these lessons .unwrap() accepts the value and stops the program on bad input. Trim the line first, since a stray space makes parsing fail.
Syntax
let count = 3; // i32 by default
let big: u64 = 4_000_000_000; // the annotation picks the type
let ratio = 2.5; // f64 by default
let single: f32 = 2.5;
let sunny: bool = true;
let grade: char = 'B'; // single quotes for char
let name: &str = "Bed 7"; // double quotes for text
let plot: (&str, f64, bool) = ("Bed 7", 2.5, true);
let width = plot.1; // 2.5
let temps: [i32; 3] = [18, 21, 19];
let first = temps[0];
let zeros = [0; 8]; // eight zeros
let whole = 7 / 2; // 3: integer division truncates
let exact = 7 as f64 / 2.0; // 3.5: convert first
let parsed: u32 = "42".trim().parse().unwrap();A type annotation follows a colon after the name. as converts between numeric types, and parse() turns text into whichever number type the annotation asks for.
Integers, floats and the as cast
packets as f64 is what makes the multiplication on the fourth line legal.
fn main() {
let packets: u32 = 7;
let grams_each: f64 = 12.5;
let total_grams = packets as f64 * grams_each;
println!("Seed weight: {} g", total_grams);
println!("Seed weight: {:.2} g", total_grams);
let seeds = 17;
let rows = 5;
println!("Seeds per row: {}", seeds / rows);
println!("Left over: {}", seeds % rows);
println!("Exact: {}", seeds as f64 / rows as f64);
let big: u64 = 4_000_000_000;
let small: u8 = 255;
println!("{} and {}", big, small);
}Output
Seed weight: 87.5 g Seed weight: 87.50 g Seeds per row: 3 Left over: 2 Exact: 3.4 4000000000 and 255
packets is a u32 and grams_each an f64; multiplying them directly would be an error, so packets as f64 converts the count first. {} prints the shortest text that represents the float exactly, while {:.2} fixes two decimal places. seeds / rows is integer division and drops the fraction; % keeps it as a remainder, and converting both operands with as f64 gives the exact quotient. The last two lines show a u64 holding a value too large for i32 and a u8 at its maximum of 255.
char, bool and tuples
Two characters, one comparison and a three-part tuple that is taken apart in two ways.
fn main() {
let grade = 'B';
let accent = 'é';
println!("{} takes {} byte(s), {} takes {}", grade, grade.len_utf8(), accent, accent.len_utf8());
println!("Is a letter: {}", grade.is_alphabetic());
let stock = 3;
let low = stock < 5;
println!("Low stock: {}", low);
let entry = ("Harbour Lights", 1998, 4.5);
let (title, year, rating) = entry;
println!("{} ({}) rated {}", title, year, rating);
println!("First field: {}", entry.0);
}Output
B takes 1 byte(s), é takes 2 Is a letter: true Low stock: true Harbour Lights (1998) rated 4.5 First field: Harbour Lights
A char is one Unicode character, and len_utf8() reports how many bytes it needs when stored in a string: one for B, two for é. stock < 5 produces a bool, which prints as true. The tuple entry mixes a string slice, an integer and a float; let (title, year, rating) = entry; destructures it into three variables in one step, and entry.0 reads a single field by position.
Arrays and numbers read from input
The two input lines are 4 and 1.5; each is parsed into the type its annotation names.
use std::io::BufRead;
fn main() {
let mut lines = std::io::stdin().lock().lines();
let beds: u32 = lines.next().unwrap().unwrap().trim().parse().unwrap();
let side: f64 = lines.next().unwrap().unwrap().trim().parse().unwrap();
println!("{} beds of {} m", beds, side);
let temps: [i32; 5] = [18, 21, 19, 24, 22];
let mut sum = 0;
for t in temps {
sum += t;
}
println!("Readings: {}", temps.len());
println!("Average: {:.1}", sum as f64 / temps.len() as f64);
let zeros = [0; 3];
println!("{:?} {:?}", temps, zeros);
}Input given to the program: 4 ↵ 1.5
Output
4 beds of 1.5 m Readings: 5 Average: 20.8 [18, 21, 19, 24, 22] [0, 0, 0]
The annotations on beds and side tell parse() which type to produce, and trim() guards against stray spaces. temps is an array of exactly five i32 values; for t in temps visits each one, temps.len() returns a usize, and both operands are cast to f64 before the division so the average keeps its fraction. [0; 3] builds an array of three zeros. Arrays print with {:?}, the Debug format, since they have no plain {} representation.
Common mistakes
Mixing integer types in one expression
Why it goes wrong: Rust has no implicit numeric promotion.
a + bwith ani32and ani64is error E0277, cannot addi64toi32, because the compiler will not guess which width you meant.Fix: Convert explicitly with
asor, for a widening that cannot lose data,i64::from(a).Rust · fixlet a: i32 = 3; let b: i64 = 4; let sum = i64::from(a) + b;Writing a char in double quotes
Why it goes wrong:
"B"is a&str, not achar, solet c: char = "B";is a type mismatch (E0308). The compiler's help text points at the quotes.Fix: Use single quotes for a single character:
'B'.Expecting integer division to produce a fraction
Why it goes wrong:
7 / 2is3because both operands are integers and the result is truncated toward zero. Nothing warns you; the program simply prints a whole number.Fix: Convert to a float before dividing when you need the fraction:
7 as f64 / 2.0.Casting a float to an integer and expecting rounding
Why it goes wrong:
astruncates toward zero:3.99 as i32is3and-5.9 as i32is-5. Values outside the target range saturate at its limits, so300.7 as u8is255.Fix: Call
.round(),.floor()or.ceil()on the float first, then cast.Rust · fixlet boxes = (3.99_f64).round() as i32; // 4
Rust's primitive types
| Type | Holds | Literal example |
|---|---|---|
i8 to i128, isize | signed integers of 8 to 128 bits, or pointer width | -40, 12i64 |
u8 to u128, usize | unsigned integers; usize is used for indexes and lengths | 255u8, 4_000 |
f32, f64 | floating-point numbers; literals default to f64 | 2.5, 1e3 |
bool | true or false | stock < 5 |
char | one Unicode scalar value, 4 bytes | 'B', 'é' |
(A, B, C) | a fixed group of values of any types | ("Bed 7", 2.5, true) |
[T; N] | exactly N values of one type, fixed length | [18, 21, 19], [0; 8] |
&str, String | text: a borrowed slice, or an owned growable string | "Bed 7", String::from("Bed 7") |
Where you use this
A greenhouse controller reads sensor readings as text, and the types decide what the program can do with them. Temperatures parse into f64 because they carry a fraction; the number of beds parses into u32 because a count cannot be negative, and if someone types -3 the parse fails instead of producing nonsense. A fixed set of five sensors fits an array, since the hardware will not grow overnight, and the average is computed after converting the sum to f64. Choosing u32 over i32 and f64 over u32 at the declaration is how you tell the compiler which mistakes to catch for you.
let beds: u32 = line.trim().parse().unwrap();
let readings: [f64; 5] = [18.5, 21.0, 19.5, 24.0, 22.5];
let mean = readings.iter().sum::<f64>() / readings.len() as f64;Key points
- Every value has one type fixed at compile time; the compiler infers it when the value makes it obvious.
- Integer types differ in width and sign; an unconstrained literal is
i32, a float literal isf64, lengths and indexes areusize. - Numeric types never convert implicitly; use
as, orFrom, to convert. - Integer division truncates toward zero and
%gives the remainder. boolis onlytrueorfalse;charis one Unicode character in single quotes.- Tuples hold a fixed set of mixed types; arrays hold a fixed number of one type.
line.trim().parse::<T>()converts input text to a number and returns aResult.
Try it yourself
The program reads a number of tiles and a tile side length in metres and prints them as text. Parse tiles as a u32 and side as an f64, then add a line that prints Area: followed by tiles times side squared, with one decimal place.
use std::io::BufRead;
fn main() {
let mut lines = std::io::stdin().lock().lines();
let tiles = lines.next().unwrap().unwrap();
let side = lines.next().unwrap().unwrap();
println!("{} tiles of side {}", tiles.trim(), side.trim());
}
6 ↵ 0.56 tiles of side 0.5
Area: 1.5use std::io::BufRead;
fn main() {
let mut lines = std::io::stdin().lock().lines();
let tiles: u32 = lines.next().unwrap().unwrap().trim().parse().unwrap();
let side: f64 = lines.next().unwrap().unwrap().trim().parse().unwrap();
println!("{} tiles of side {}", tiles, side);
println!("Area: {:.1}", tiles as f64 * side * side);
}
Practise this
Related lessons
- Variables and Mutability in RustWhy Rust variables are immutable by default, when to write let mut, how shadowing with let creates a new variable, and how const differs from let.7 min
- Control Flow in Rust: if, loop, while and forHow Rust branches and repeats: if as an expression, loop with break values, while, for over ranges and arrays, continue, and labelled breaks.9 min
Frequently asked questions
Which integer type should I use in Rust by default?
i32 is the default for a reason: it is fast on every platform and large enough for most counts. Use u32 or u64 when a value can never be negative and you want the compiler to reject negatives at parse time, i64 or u64 for sums that may exceed about two billion, and usize whenever the value indexes an array or a Vec, because that is the type indexing requires.
How do I convert a String or &str to a number in Rust?
Call parse() and tell it the target type, either through an annotation on the variable, let n: i32 = text.trim().parse().unwrap();, or with the turbofish form text.trim().parse::<i32>(). parse() returns a Result because the text may not be a valid number; unwrap() takes the value or stops the program, and the error handling lesson shows how to react gracefully instead.
What is the difference between an array and a Vec in Rust?
An array [T; N] has a length that is part of its type and fixed at compile time; it is stored inline, often on the stack, and cannot grow. A Vec<T> stores its elements on the heap and can push and pop at run time. Use an array for a small, fixed collection such as the days of the week, and a Vec for anything whose size depends on input.
How this page was checked. Every program on it was run with rustc 1.94 at build time by the publishing checks, and the output shown is what it printed. Running Rust inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with rustc 1.94 locally.