Day 6a: Rust Integers – Signed vs Unsigned, Type Sizes, and Basic Operations
Note: Integers are the backbone of Rust's numeric data types, so I’ll be doing a deep dive over the next three days. We’ll cover everything you need to know about integers, from the basics to advanced topics like overflow handling, performance optimization, and practical use cases. Let’s begin with today’s post, Day 6a, which covers the fundamental differences between signed and unsigned integers, their sizes, and basic operations.
Introduction
Rust provides various types of integers to help you balance memory usage, performance, and safety. In this post, we’ll explore the core integer types, discuss their signed and unsigned variants, and explain the differences in memory usage. We’ll also cover basic arithmetic and bitwise operations, which are fundamental when working with integers.
1. Integer Types in Rust
Rust offers a variety of integer types, categorized into signed and unsigned types. The key difference between them is whether they support negative numbers.
a. Signed Integers
Signed integers can store both positive and negative numbers. For example, i32
can store values from -2,147,483,648
to 2,147,483,647
. Signed integers use the leftmost bit to store the sign (0 for positive, 1 for negative), reducing the overall range of values they can represent.
Example:
fn main() {
let x: i32 = -108;
println!("Signed integer: {}", x); // Output: -108
}
b. Unsigned Integers
Unsigned integers only store non-negative numbers. For example, u32
can store values from 0
to 4,294,967,295
. Since unsigned integers don't need to reserve a bit for the sign, they can store larger numbers than signed integers of the same size.
Example:
fn main() {
let x: u32 = 108;
println!("Unsigned integer: {}", x); // Output: 108
}
2. Integer Sizes and Memory Considerations
Each integer type has a different memory footprint, depending on the number of bits it uses. The more bits, the larger the range of numbers it can store. Rust allows you to select an integer type based on your memory or performance needs.
Type | Size | Range (Signed) | Range (Unsigned) |
---|---|---|---|
i8 | 8-bit | -128 to 127 | N/A |
u8 | 8-bit | N/A | 0 to 255 |
i16 | 16-bit | -32,768 to 32,767 | N/A |
u16 | 16-bit | N/A | 0 to 65,535 |
i32 | 32-bit | -2,147,483,648 to 2,147,483,647 | N/A |
u32 | 32-bit | N/A | 0 to 4,294,967,295 |
i64 | 64-bit | -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 | N/A |
u64 | 64-bit | N/A | 0 to 18,446,744,073,709,551,615 |
i128 | 128-bit | Extremely large range | N/A |
u128 | 128-bit | N/A | Extremely large range |
Memory Considerations
When choosing an integer type, consider the memory and performance trade-offs. For example, while using i64
provides a larger range of values, it also consumes more memory than an i32
. In systems with constrained memory, smaller types like i8
and u8
can help reduce memory usage, though you’ll need to ensure they provide enough range for your values.
3. Signed vs Unsigned: Key Differences
The main distinction between signed and unsigned integers is whether or not they support negative values. This affects the range of values they can store. Choosing between signed and unsigned integers depends on your application:
- Signed integers are useful when negative values are expected, such as in mathematical computations involving loss or debt.
- Unsigned integers are more appropriate for scenarios where only positive numbers make sense, such as array indexing or counting elements.
Example:
fn main() {
let signed_num: i32 = -500;
let unsigned_num: u32 = 500;
println!("Signed: {}, Unsigned: {}", signed_num, unsigned_num);
}
4. Basic Arithmetic Operations
Rust supports basic arithmetic operations like addition, subtraction, multiplication, and division for integers. These operations are crucial for tasks ranging from simple calculations to complex systems programming.
Operator | Description |
---|---|
+ | Addition |
- | Subtraction |
* | Multiplication |
/ | Division |
% | Modulus (remainder) |
Example:
fn main() {
let a: i32 = 10;
let b: i32 = 3;
println!("Addition: {}", a + b); // Output: 13
println!("Subtraction: {}", a - b); // Output: 7
println!("Multiplication: {}", a * b); // Output: 30
println!("Division: {}", a / b); // Output: 3
println!("Modulus: {}", a % b); // Output: 1
}
One key point to note is that dividing integers truncates the result, meaning that dividing 5
by 2
results in 2
(not 2.5
as with floating-point numbers).
5. Bitwise Operations
Bitwise operations are useful for low-level programming and manipulating individual bits of integers. Rust supports several bitwise operators that allow you to perform AND, OR, XOR, left shifts, and right shifts directly on integer values.
Operator | Description |
---|---|
& | AND |
| | OR |
^ | XOR |
<< | Left shift |
>> | Right shift |
Example:
fn main() {
let x: u8 = 0b1010; // Binary literal for 10
let y: u8 = 0b1108; // Binary literal for 12
println!("x & y: {:b}", x & y); // AND operation, Output: 1080
println!("x | y: {:b}", x | y); // OR operation, Output: 1110
println!("x ^ y: {:b}", x ^ y); // XOR operation, Output: 0110
println!("x << 1: {:b}", x << 1); // Left shift, Output: 10108
println!("y >> 2: {:b}", y >> 2); // Right shift, Output: 11
}
Conclusion for Day 6a
In today’s post, we introduced Rust’s integer types, discussed signed vs unsigned integers, and explored how to choose the correct integer type for performance and memory efficiency. We also covered basic arithmetic and bitwise operations, which are essential tools when working with integers in low-level systems programming.
In the next post, Day 6b, we’ll dive deeper into how Rust handles integer overflow, how to use checked and wrapping arithmetic, and how to optimize integer performance.