Rust - Data Types
Rust - Data Types
Section titled “Rust - Data Types”A type system defines the different categories of values a programming language supports. Rust is a statically typed language, which means it must know the types of all variables at compile time. This powerful feature allows the compiler to verify code correctness, prevent a wide range of bugs, and provide excellent support for tooling like auto-completion and refactoring.
While you must be explicit about types in function signatures and structs, the compiler can often infer the type of a variable from its initial value. This gives you the best of both worlds: strong type safety without unnecessary verbosity.
Declaring Variables with let
Section titled “Declaring Variables with let”In Rust, you declare variables with the let keyword. By default, variables are immutable, which is a key Rust feature that promotes safer, more predictable code. To create a mutable variable, you use the let mut keyword.
fn main() { // Type is inferred by the compiler let company = "TechCorp"; // &str (string slice) let rating = 4.5; // f64 (floating-point number) let is_profitable = true; // bool (boolean) let logo = '🦀'; // char (Unicode character)
// You can also explicitly annotate the type let mut project_count: u32 = 10; // u32 (unsigned 32-bit integer) project_count += 1;
println!("Company: {}", company); println!("Rating: {}", rating); println!("Is Profitable? {}", is_profitable); println!("Logo: {}", logo); println!("Current Projects: {}", project_count);}In the example above, company, rating, is_profitable, and logo are immutable. project_count is mutable and its type u32 is explicitly declared. The println! macro uses {} placeholders, which are replaced by the variable values during execution.
You can check your code for correctness without compiling it by running cargo check in your terminal. To compile and run, use cargo run.
The output of the code is:
Company: TechCorpRating: 4.5Is Profitable? trueLogo: 🦀Current Projects: 11Scalar Types
Section titled “Scalar Types”A scalar type represents a single value. Rust has four primary scalar types:
- Integers (e.g.,
5,-10) - Floating-point numbers (e.g.,
3.14,99.9) - Booleans (e.g.,
true,false) - Characters (e.g.,
'a','%','π')
Integers
Section titled “Integers”An integer is a number without a fractional component. They can be signed (can be negative, zero, or positive) or unsigned (can only be zero or positive). Here are the built-in integer types:
| Size | Signed | Unsigned |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| Arch | isize | usize |
The isize and usize types depend on the architecture of the computer your program is running on: 32 bits on a 32-bit architecture and 64 bits on a 64-bit architecture. They are primarily used for indexing collections.
Best Practice: The default integer type is i32, which is generally a good choice for speed and compatibility. Use usize when indexing collections. Use other integer types only when you have a specific need, such as saving memory or interfacing with an external system that has fixed-size types.
Integer Overflow
Section titled “Integer Overflow”Integer overflow occurs when you try to assign a value to a variable that is outside the type’s valid range (e.g., assigning 256 to a u8, which can only hold 0 to 255). Rust’s behavior depends on the build profile:
- Debug Mode: Rust checks for integer overflow. If it occurs, the program will panic and exit immediately. This helps you find bugs during development.
- Release Mode: Rust does not check for overflow. Instead, it performs two’s complement wrapping. For example,
256in au8becomes0,257becomes1, and so on.
If you need to handle overflow explicitly, the standard library provides methods:
fn main() { let a: u8 = 255;
// checked_* methods return an Option<T> let checked_add = a.checked_add(1); // Returns None, because it would overflow let checked_add_ok = a.checked_add(0); // Returns Some(255)
// wrapping_* methods perform two's complement wrapping let wrapping_add = a.wrapping_add(1); // Returns 0
// saturating_* methods return the type's maximum or minimum value let saturating_add = a.saturating_add(1); // Returns 255
println!("Checked add (255 + 1): {:?}", checked_add); println!("Checked add (255 + 0): {:?}", checked_add_ok); println!("Wrapping add (255 + 1): {}", wrapping_add); println!("Saturating add (255 + 1): {}", saturating_add);}Relying on implicit wrapping can lead to unexpected bugs. It’s best practice to use the checked_* methods and handle the None case when you expect an operation might overflow.
Floating-Point Numbers
Section titled “Floating-Point Numbers”Rust has two floating-point types: f32 (single-precision) and f64 (double-precision). The default type is f64 because on modern CPUs, it’s roughly the same speed as f32 but is capable of more precision.
fn main() { let pi = 3.14159; // f64 by default let interest_rate: f32 = 0.05; // Explicitly f32
println!("Pi is approximately {}", pi); println!("Interest Rate is {}", interest_rate);
// For readability, you can use underscores in numeric literals let large_number = 1_000_000.555_001; println!("A large number: {}", large_number);}Important: Rust does not perform implicit type casting between numeric types. The following code will not compile:
// This will fail to compile!fn main() { let my_integer: i32 = 10; // error[E0308]: mismatched types // let my_float: f64 = my_integer;}
// To fix it, you must use an explicit cast with the `as` keyword:fn fixed() { let my_integer: i32 = 10; let my_float: f64 = my_integer as f64; // This is correct println!("Float value: {}", my_float);}Booleans and Characters
Section titled “Booleans and Characters”The boolean type, bool, has two possible values: true and false. It’s most often used in conditional logic like if expressions.
The character type, char, is Rust’s most primitive alphabetic type. It’s specified with single quotes. Importantly, char represents a Unicode Scalar Value, which means it can represent much more than just ASCII. A char is 4 bytes in size and can represent letters, numbers, symbols, and even emojis.
fn main() { let is_learning_rust: bool = true; println!("Are we learning Rust? {}", is_learning_rust);
let letter_a = 'a'; let checkmark = '✅'; println!("A simple letter: {}", letter_a); println!("A checkmark emoji: {}", checkmark);}