Day 5: Variables
Welcome back fellow Rustaceans🦀!
In the last article, we learned how to print custom data in Rust using the Debug and Display traits. Today, we're diving into the fundamentals: variables! Variables are the containers that store your data.
In Rust, we use the let
keyword to declare a variable. Here's the basic structure:
let my_variable: data_type = value;
let
: Tells Rust we're creating a variable.my_variable
: The name you choose for your variable.data_type
: The kind of data the variable will hold (more on this soon!).value
: The initial data you assign to the variable.
One key difference between strongly typed languages like C and loosely typed languages like Python lies in how you declare variables. In strongly typed languages like C, you must explicitly define the data type of each variable. Loosely typed languages, however, can infer the data type based on how the variable is used. Rust, while being strongly typed and compiled, adopts a slightly different approach. Let's try this example
In the above example, we declare a variable a
of type unsigned int 32 (u32)
and assign it a value of 1000. As expected, when we run the code, the value of 'a' is printed.
Now, let's remove the :u32
from the let
statement, rebuild, and re-run our code. Surprisingly, it still works!
This shows that even though Rust is a statically typed language, it can often infer the type of a variable based on the value you give it. If you have Rust Analyzer installed, you'll notice it highlights the type of a
as i32
. That's because Rust defaults to a signed 32-bit integer for whole numbers.
In most cases, the Rust compiler can figure out the variable type based on how it's used. However, there are times when it can't, such as with function arguments (more on that later). In those situations, you'll need to specify the type explicitly.
Data Types
Rust provides a variety of data types to suit different needs. Here's a closer look at some common ones, along with their sizes and ranges:
One key difference you'll notice compared to C is how Rust handles characters. Unlike C, Rust's char
type is not limited to a single byte. Instead, it represents a single Unicode Scalar Value. This means Rust's characters support the vast range of characters, symbols, and emojis used across different languages and writing systems. Using UTF-8 encoding ensures your Rust programs can handle text from around the world!
Pointer Size
The size of a pointer can vary depending on your computer's architecture. For instance:
- On a 32-bit system, pointers typically occupy 4 bytes
- On a 64-bit system, pointers often take up 8 bytes
Rust employs usize
and isize
to abstract away architecture-specific memory details, making your code more portable.
Data Type | Description | Purpose |
---|---|---|
usize |
Unsigned integer, size equivalent to a pointer on the system | Indexing arrays, collections, memory operations |
isize |
Signed integer, size equivalent to a pointer on the system | Similar to usize , but allows negative values |
These pointer-sized types offer advantages when working with memory addresses or memory-related operations. They can be equivalent to u32/u64
and i32/i64
depending on whether you're on a 32-bit or 64-bit system. They ensure your code is adaptable to different architectures without needing to explicitly specify the pointer size. This promotes code portability and simplifies memory management tasks.
Finding Size of a type
Sometimes, knowing the exact size a data type takes up in memory is important. If you're coming from a C background, the concept of sizeof
might be familiar. Rust's standard library offers a similar functionality with std::mem::size_of.
Let's check out the syntax of std::mem::size_of
according to the Rust documentation https://doc.rust-lang.org/std/mem/fn.size_of.html
pub const fn size_of<T>() -> usize
This syntax might seem a bit unfamiliar if you're coming from a C background.
std::mem::size_of
is a generic function. Generics in Rust allows you to write code that can work with multiple different data types without sacrificing type safety. We will learn about generics in much more detail later. For now, you can think of them as blueprints for functions where you can plug in specific data types later.
However, unlike functions with regular arguments, the Rust compiler sometimes needs extra help figuring out the specific type you want to use with a generic. This is where the turbo fish syntax (::<>
) comes in. It's a way to explicitly tell the compiler which concrete type to fill into a generic placeholder. For example, size_of::<bool>
specifies that you want the size of a bool
type specifically.
Let's print the size of all the types we have learnt so far.
use std::mem::size_of;
fn main() {
println!("Data type sizes in Rust:");
println!("bool: {} bytes", size_of::<bool>());
println!("char: {} bytes", size_of::<char>());
println!("i8: {} bytes", size_of::<i8>());
println!("i16: {} bytes", size_of::<i16>());
println!("i32: {} bytes", size_of::<i32>());
println!("i64: {} bytes", size_of::<i64>());
println!("u8: {} bytes", size_of::<u8>());
println!("u16: {} bytes", size_of::<u16>());
println!("u32: {} bytes", size_of::<u32>());
println!("u64: {} bytes", size_of::<u64>());
println!("f32: {} bytes", size_of::<f32>());
println!("f64: {} bytes", size_of::<f64>());
println!("usize: {} bytes", size_of::<usize>());
println!("isize: {} bytes", size_of::<isize>());
}
Conclusion
In today's article, we learned about different data types in Rust. We explored how Rust's chars offer more flexibility than C, and how we can use usize and isize to streamline working with pointers. In our upcoming articles, we'll delve into the core concepts of Rust and explore how they differ from C in areas like pointers, argument parsing, and more. If you'd like to strengthen your understanding of these C concepts, we have a fantastic course available. Here's the link to join:
See you in the next article!
Discussion