Note: Data types are important in Rust because they determine how data can be stored and manipulated. Using types reduce probability to have runtime errors and make more robust applications by using type checking.
In Rust we have 3 primitive types. Signed, unsigned and floating precision numbers. Each type specify the number of bits, so it is much more easy to remember the type notation than in C language.
Data Type | Bytes | Description |
---|---|---|
i8 |
1 | Signed 8-bit integer |
i16 |
2 | Signed 16-bit integer |
i32 |
4 | Signed 32-bit integer |
i64 |
8 | Signed 64-bit integer |
i128 |
16 | Signed 128-bit integer |
isize |
* | Signed pointer-sized integer (can be 32 or 64 bits) |
u8 |
1 | Unsigned 8-bit integer |
u16 |
2 | Unsigned 16-bit integer |
u32 |
4 | Unsigned 32-bit integer |
u64 |
8 | Unsigned 64-bit integer |
u128 |
16 | Unsigned 128-bit integer |
usize |
* | Unsigned pointer-sized integer (can be 32 or 64 bits) |
f32 |
4 | 32-bit floating point number |
f64 |
8 | 64-bit floating point number |
char |
4 | Unicode scalar value (4 bytes) |
bool |
1 | Boolean value (true or false ) |
() |
0 | The empty tuple type, also known as "unit" |
(*) = platform-dependent
In Rust "i" numbers are signed integers. "u" numbers are unsigned. "f" numbers are floating point numbers. Variable size data types: isize and usize depends on machine architecture 32 or 64 bits.
In Rust the Boolean values are true and false like in Python and Ruby. The operators for Boolean expressions are: {&&,||,!}.
In Rust characters are Unicode not ASCII and occupy 4 bytes not 1 byte. A character is enclosed in single quotes like: {'0','1'....'a','b',.... 'π','α','β',... 'ω'}
Constants are values that cannot be changed. They are declared using the const
keyword.
For example:
const pi: f64 = 3.14159265358979323846;
Constants can be initialized with any expression that evaluates to a constant value. For example:
const pi: f64 = 3.14 + 0.00159265358979323846;
Constants can be used in any expression. For example:
let r = pi * 2;
Constants can also be used in function definitions. For example:
fn circumference(r: f64) -> f64 {
2 * pi * r
}
Constants are immutable, which means that their value cannot be changed once they have been initialized. For example:
pi = 3.141593; // This will not compile
Constants are also thread-safe, which means that they can be accessed from multiple threads without any synchronization.
Constants are a useful way to store values that will not change throughout the lifetime of a program. They can also be used to create functions that are thread-safe and immutable.
In Rust you must declare data type to create a variable using symbol ":"> and keyword let Variables are immutable in Rust unless you specify otherwise. Global variables can be defined using keyword static or const.
/* demo to declare a static variable */
static TEST:
i32 = 10; // global variable
fn main() {
let x = TEST + 1; // use TEST in expression
println!("TEST = {}",x); // printing the result
}
Notes:
Rust is using infix notation to create expressions. An expression is created using variables, constants and literals separated by operators. In previous example we use expression: TEST+1 to calculate initial value for x. The result of expressions can be captured into a new variable using assign symbol "=".
You can create larger expressions from small expressions using parenthesis and operators. The order of operations is important. Operators { *, / } have higher precedence than { +, – }.
(x + y) / 2 < (x + y)
In Rust, a pointer is a variable that stores the address of another variable in memory. Pointers are useful for interacting with data and memory at a lower level.
Rust has two operators for pointers:
&
, a reference operator that creates a reference to a value*
, a dereference operator that accesses the value at the address stored in a pointerHere's an example:
fn main() {
let mut number = 5;
println!("The number is {}", number);
let pointer_to_number = &mut number;
*pointer_to_number += 5;
println!("The new number is {}", number);
}
In this example, we define a variable number
and set its value to 5
. We then create a mutable reference to number
using the &mut
operator and store the reference in pointer_to_number
.
We can access the value of number
using the *
operator and modify it by adding 5
through pointer_to_number
.
Finally, we print the modified value of number
.
Pointers in Rust help prevent common memory-related errors such as null references and memory leaks. Rust's borrow checker also ensures that pointers are used safely by ensuring that there is no data race or concurrent access to mutable data.
Rust's borrowing system is a set of rules and checks that ensure safe and efficient memory usage. The system is designed to address common memory-related errors such as null references, dangling pointers, data races, and memory leaks.
The core concept of Rust's borrowing system is ownership. In Rust, every value has an owner, which is responsible for allocating and deallocating its memory. The ownership of a value can be transferred to other variables using move semantics or shared through references.
Rust has two kinds of references: immutable references and mutable references. Immutable references, denoted by the &
operator, allow read-only access to the referenced value. Mutable references, denoted by the &mut
operator, allow both read and write access to the referenced value but with the constraint that there is only one mutable reference at a time.
The borrowing system enforces a set of rules to prevent memory-related errors and ensure safe concurrency:
Here's an example:
fn main() {
let mut number = 5;
let reference_to_number = &number;
let another_reference_to_number = &number;
println!("The number is {}", number);
let mutable_reference_to_number = &mut number;
*mutable_reference_to_number += 5;
println!("The new number is {}", number);
}
In this example, we create an immutable reference to number
using the &
operator and store the reference in reference_to_number
. We then create another immutable reference to number
called another_reference_to_number
.
Because there are no mutable references to number
, we can still modify its value through a mutable reference mutable_reference_to_number
.
Rust's borrowing system helps prevent common memory-related errors by ensuring that data is used in a safe and efficient manner. It requires that mutable access to data is limited to a single owner at a given time and that shared access is immutable.
Reference counting is a memory management technique that allows shared ownership of resources by keeping track of the number of references to that resource. In Rust, reference counting is implemented using the Rc
smart pointer.
The Rc
smart pointer allows multiple immutable references to the same value, and only when all references to a value have been dropped, the value is deallocated. This ensures that the value is available as long as any reference to it exists.
use std::rc::Rc;
struct Person {
name: String,
age: u8,
}
fn main() {
let person_rc1 = Rc::new(Person {
name: String::from("Alice"),
age: 25,
});
{
let person_rc2 = person_rc1.clone();
println!(
"Person 1: {:#?}, Person 2: {:#?}",
person_rc1, person_rc2
);
}
println!("Person: {:#?}", person_rc1);
}
In this example, we define a Person
struct that contains a name and an age field. We then create an Rc
instance of the Person
struct using the Rc::new()
method.
We then create a second Rc
instance of the Person
struct, person_rc2
, by calling the .clone()
method on the first instance, person_rc1
.
After printing both Rc
objects, we see that they are identical. This is because they refer to the same underlying Person
struct. We then drop the second Rc
instance, and we see that the first Rc
instance continues to exist, preserving the Person
instance's data.
The Rc
smart pointer is useful when working with graphs or other data structures that require shared ownership, and manual memory management is not feasible. However, the Rc
smart pointer introduces runtime overhead for reference counting, which can affect performance.
Rust is a statically-typed language, which means that variables are given a specific type at compile-time, and the type of the variable cannot change throughout the program's execution. However, Rust also provides type inference, which allows the compiler to automatically determine the types of variables based on their usage.
For example, when a variable is assigned a value, the Rust compiler analyzes the value's type and uses that information to determine the variable's type. Here's an example:
fn main() {
let x = 5; // x is inferred to be of type i32
let y = 3.14; // y is inferred to be of type f64
let sum = x + y; // compiler error: cannot add f64 and i32
}
In this example, the variable x
is assigned the value 5
, which the Rust compiler infers to be of type i32
. Similarly, the variable y
is assigned the value 3.14
, which the Rust compiler infers to be of type f64
.
However, the Rust compiler will not automatically convert types or allow for mismatched types. In the example above, the compiler will produce an error because x
and y
have different types, and cannot be added together.
While Rust's type inference is powerful, it's important to note that it can make code less readable if used improperly. Explicitly specifying types, even when the Rust compiler can infer them, can make code easier to understand and maintain.
fn main() {
let x: i32 = 5; // x is explicitly set to type i32
let y: f64 = 3.14; // y is explicitly set to type f64
// let's use type cast with operator "as"
let sum = x as f64 + y; // the addition works
}
In this modified example, the types of x
and y
are explicitly set, which makes it more apparent what the types are, and prevents errors from occurring due to type inference.
Type coercion in Rust is the automatic conversion of one type of data to another type of data. Rust allows for basic type coercion to occur in certain situations, such as when converting a signed integer to an unsigned integer of the same or larger size, or when converting a float to an integer.
Here's an example where coercion occurs:
fn main() {
let x: u32 = 42;
let y: i64 = x as i64; // x is coerced from u32 to i64
println!("x: {}, y: {}", x, y);
}
In this example, the variable x
is assigned the value 42
, which Rust infers to be of type u32
. The variable y
is assigned the value of x
after it is coerced to type i64
. Rust allows for this coercion because i64
can store any value that u32
can store, so there is no risk of data loss.
However, it's worth noting that coercion can also have side effects that may not be immediately apparent. For example, coercing a float to an integer will cause the float's value to be truncated, which can lead to data loss.
Here is a short example that demonstrates this:
fn main() {
let x: f32 = 3.14;
let y: i32 = x as i32; // x is coerced to i32
println!("x: {}, y: {}", x, y);
}
In this example, the variable x
is assigned the value 3.14
, which Rust infers to be of type f32
. The variable y
is assigned the value of x
after it is coerced to type i32
. Because i32
cannot store decimal values, the decimal portion of x
is truncated, resulting in a value of 3
for y
.
Overall, type coercion can be a useful feature in Rust, but should be used judiciously and with an awareness of the potential side effects.
Read next: Composite Types