Sage-Code Laboratory
index<--

Basic Types

Rust is a strongly typed programming language. That means after a variable is declared its data type remain the same during program execution. All other types are derived from basic types.

Page Bookmarks



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.

Primitive Types

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.

Boolean types:

In Rust the Boolean values are true and false like in Python and Ruby. The operators for Boolean expressions are: {&&,||,!}.

Character types:

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

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.

Variables

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.

Example:

/* 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:

Expressions

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 { +, – }.

Example:

(x + y) / 2 < (x + y)

Notes:

Pointers in Rust

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:

Here'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.

Borrowing System

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

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.

Type Inference

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

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