2.5 Data Types and Annotations

Rust is a statically typed language, meaning the type of every variable must be known at compile time. The compiler can often infer the type, but you can also provide explicit type annotations. Once assigned, a variable’s type cannot change.

2.5.1 Primitive Data Types

Rust offers a standard set of primitive types:

  • Integers: Signed (i8, i16, i32, i64, i128, isize) and unsigned (u8, u16, u32, u64, u128, usize). The number indicates the bit width. isize and usize are pointer-sized integers (like ptrdiff_t and size_t in C).
  • Floating-Point: f32 (single-precision) and f64 (double-precision).
  • Boolean: bool (can be true or false).
  • Character: char represents a Unicode scalar value (4 bytes), capable of holding characters like ‘a’, ‘國’, or ‘😂’. This contrasts with C’s char, which is typically a single byte.

2.5.2 References

In addition to value types like i32, Rust also supports references—safe, managed pointers that refer to data stored elsewhere in memory. Similar to C pointers, references hold the address of a value, introducing a level of indirection.

Rust references can be either immutable or mutable, allowing temporary access to data without transferring ownership or making a copy. This is especially useful for passing data to functions efficiently.

To create a reference, Rust uses the & operator for immutable access and &mut for mutable access. The * operator can be used to access (dereference) the value behind a reference, although in many cases this happens implicitly.

References are covered in more depth in Chapter 5. Chapter 6 will explore them in full detail, as part of the discussion on Ownership, Borrowing, and Memory Management.

Below is a short example demonstrating how to pass a mutable reference to a function:

fn inc(i: &mut i32) {
    *i += 1;
} 

fn main() {
    let mut v = 0;
    inc(&mut v);
    println!("{v}"); // 1
    let r = &mut v;
    inc(r);
    println!("{}", *r); // 2
}

2.5.3 Type Inference

The compiler can often deduce the type based on the assigned value and context.

fn main() {
    let answer = 42;     // Type i32 inferred by default for integers
    let pi = 3.14159; // Type f64 inferred by default for floats
    let active = true;   // Type bool inferred
    println!("answer: {}, pi: {}, active: {}", answer, pi, active);
}

2.5.4 Explicit Type Annotation

Use a colon : after the variable name to specify the type explicitly, which is necessary when the compiler needs guidance or you want a non-default type (e.g., f32 instead of f64).

fn main() {
    let count: u8 = 10; // Explicitly typed as an 8-bit unsigned integer
    let temperature: f32 = 21.5; // Explicitly typed as a 32-bit float
    println!("count: {}, temperature: {}", count, temperature);
}

2.5.5 Comparison with C

In C, basic types like int can have platform-dependent sizes. C99 introduced fixed-width integer types in <stdint.h> (e.g., int32_t, uint8_t), which correspond directly to Rust’s integer types. C lacks built-in type inference like Rust’s.