Understanding Data Types in Rust
Dipankar Paul
Posted on March 12, 2024
In Rust, every value has a specific data type that tells the compiler how to work with that value. Rust is a statically typed language, meaning that the type of every variable must be known at compile time. Rust provides several built-in data types, which can be broadly categorized into scalar and compound types. Let's explore these types in more detail.
Scalar Types
A scalar type represents a single value. Rust has four primary scalar types: integers, floating-point numbers, Booleans, and characters. You may recognize these from other programming languages.
Integer Type
- An integer is a number without a fractional component.
- The default integer type is
i32
. - Signed integers:
i8
,i16
,i32
,i64
,i128
,isize
- Unsigned integers:
u8
,u16
,u32
,u64
,u128
,usize
- Each variant can be either signed(+/-) or unsigned(+) and has an explicit size.
Example:
let integer: i32 = 42;
let unsigned_integer: u64 = 100;
- Each signed variant can store numbers from -(2n - 1) to 2n - 1 - 1 inclusive, where n is the number of bits that variant uses.
- Unsigned variants can store numbers from 0 to 2n - 1.
- The
isize
andusize
types depend on the architecture of the computer your program is running on.
Floating-Point Types
-
f32
: 32-bit floating point -
f64
: 64-bit floating point, default.
Example:
let float: f64 = 3.14;
- Floating-point numbers are represented according to the IEEE-754 standard. The f32 type is a single-precision float, and f64 has double precision.
Boolean Type
-
bool
: Represents true or false values. - Booleans are one byte in size.
Example:
let is_true: bool = true;
Character Type
-
char
: Represents a single Unicode character. - We can also store special characters like
$
,@
and&
, etc. using the character type.
Example:
let character: char = 'A';
Notably, we can also store numbers as characters using single quotes. For example,
let num_char: char = '5';
Here
5
is a character not a number.
Compound Types
Compound types can group multiple values into one type. Rust has two primitive compound types: tuples and arrays.
Array Type
- Fixed-size, homogeneous data structure.
- Every element of an array must have the same type.
- Arrays are useful when you want your data allocated on the stack rather than the heap or when you want to ensure you always have a fixed number of elements.
Example:
let a: [i32; 3] = [1, 2, 3];
let first = a[0]; // Accessing array element
let second = a[1];
- Here, i32 is the type of each element. After the semicolon, the number 3 indicates the array contains three elements.
- You can also initialize an array to contain the same value for each element.
Example:
let numbers = [3; 5];
println!("numbers = {:?}", numbers); // [3, 3, 3, 3, 3]
The
println!
macro asks for the debug implementation with the?
format parameter.{}
gives the default output,{:?}
gives the debug output. Types such as integers and strings implement the default output, but arrays only implement the debug output. This means that we must use debug output here.Adding
#
, eg{numbers:#?}
, invokes a “pretty printing” format, which can be easier to read.
Mutable Array in Rust
In Rust, an array is immutable, which means we cannot change its elements once it is created.
However, we can create a mutable array by using the mut keyword before assigning it to a variable.
Example:
fn main() {
let mut numbers: [i32; 5] = [1, 2, 3, 4, 5];
println!("original array = {:?}", array); // [1, 2, 3, 4, 5]
// change the value of the 3rd element in the array
numbers[2] = 0;
println!("changed array = {:?}", numbers); // [1, 2, 0, 4, 5]
}
We changed the element at index 2
(third element) from 3
to 0
. This is possible because we have created the numbers array as mutable.
Note: Values inside an array can only be modified but cannot be deleted because the size of the array is fixed after initialization.
Tuple Type
- Tuples group together elements of different types into one compound type.
- Fixed-size, heterogeneous data structure.
- Oonce declared, they cannot grow or shrink in size.
Example:
let tup: (i32, f64, &str) = (42, 3.14, "tuple");
- We can break down tuples into smaller variables, known as destructuring.
Example:
let (x, y, z) = tup;
println!("The value of y is: {y}"); // prints 3.14
Destructuring a tuple is also known as tuple unpacking.
- We can also access a tuple element directly by using a period (.) followed by the index of the value.
Example:
let first = tup.0; // 42
let second = tup.1; // 3.14
println!("The value of y is: {y}");
The tuple without any values has a special name, unit
. This value and its corresponding type are both written ()
and represent an empty value or an empty return type. Expressions implicitly return the unit
value if they don’t return any other value.
- You can think of it as
void
that can be familiar to you from other programming languages.
Mutable Tuple
In Rust, a tuple is immutable by default, which means we cannot change its elements once it is created.
However, we can create a mutable array by using the mut keyword before assigning it to a variable.
Example:
fn main() {
// initialize a mutable tuple
let mut mountain_heights = ("Everest", 8848, "Fishtail", 6993);
println!("Original tuple = {:?}", mountain_heights);
// prints => ("Everest", 8848, "Fishtail", 6993)
// change 3rd and 4th element of a mutable tuple
mountain_heights.2 = "Lhotse";
mountain_heights.3 = 8516;
println!("Changed tuple = {:?}", mountain_heights);
// prints => ("Everest", 8848, "Lhotse", 8516)
}
Here, we create a mutable tuple named mountain_heights. We then change its 2nd
and 3rd
tuple index.
Note: You can only change the element of a tuple to the same type as when it was created. Changing data types is not allowed after tuple creation.
Some other types
We wiil learn about them in detail later.
String Type
- Represents a sequence of characters.
- The default string type is a string slice
&str
. - Strings can be either string slices or owned strings
String
. - String slices are often used for string literals, while
String
is used for dynamically allocated strings.
Example:
let string_slice: &str = "Hello, Rust!";
let owned_string: String = String::from("Rust is awesome!");
Slice Type
- Represents a view into a portion of a contiguous sequence.
- Slice must reference an existing array or slice.
- Slices are dynamically sized and provide a flexible way to work with arrays.
Example:
let array = [1, 2, 3, 4, 5];
let slice: &[i32] = &array[1..4]; // includes elements from index 1 to 3
// Accessing the elements of the slice
for num in slice {
println!("Number: {}", num);
}
Reference Types
- References allow you to borrow values without taking ownership.
- Reference must be explicitly created by borrowing.
-
&T
: Immutable reference, used for read-only access. -
&mut T
: Mutable reference, used for read-write access.
Example:
let x = 42;
let reference: &i32 = &x;
let mutable_reference: &mut i32 = &mut x;
-
Rules for reference:
- Multiple immutable references to the same data are allowed.
- Only one mutable reference to a piece of data is allowed within a specific scope.
Example:
let x = 10;
let mut y = 10;
// Creating an immutable reference to x
let reference: &i32 = &x;
// Creating a mutable reference to y
let mutable_reference: &mut i32 = &mut y;
// Modifying the value through the mutable reference
*mutable_reference += 5;
// Accessing the value through the reference
println!("Value through immutable reference: {}", *reference); // 10
println!("Value through mutable reference: {}", y); // 15
- The
*
operator is used to dereference the reference and access the underlying value.
Conclusion
Understanding data types in Rust is crucial for writing safe and efficient code. By leveraging the different types provided by Rust, you can express your program's logic more clearly and effectively. Scalars and compounds are the building blocks of Rust programs, and mastering them will set you on the path to becoming a proficient Rust programmer.
Posted on March 12, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.