Open In App

Rust Types and Inference

Improve
Improve
Like Article
Like
Save
Share
Report

Pre-requisites: Rust, Scalar Datatypes in Rust

Rust is a multi-paradigm programming language like C++ syntax that was designed for performance and safety, especially safe concurrency by using a borrow checker and ownership to validate references. 

In this article, we will focus on Rust’s primitive types. By understanding the way to declare these types we will have a solid foundation to start with in Rust and later on, it will help with more advanced concepts.

  • Integers (signed and unsigned).
  • Floating-point numbers (f32 and f64).
  • Booleans (bool).
  • Characters (char).
  • Strings (String and &str).
  • Tuples.
  • Arrays

Primitive Variables in Rust 

Primitive types are types that are already defined in the language most authors refer to as data types which is the most common reference. They serve as the building blocks for more complex data structures and types.

In Rust, the following are the primitive types:

Primitive Data Types Description
Integer types  Signed integers: i8, i16, i32, i64, i128, and isize
 Unsigned integers: u8, u16, u32, u64, u128, and usize
Floating-point types
 

floating-point: f32
floating-point: f64

Boolean type

bool (which values true and false)

Character type

char (represents a Unicode scalar value)
Tuple types
      
Tuples are fixed-size collections of heterogeneous elements, such as (i32, f64, and bool).

Array types
       

 

Arrays are fixed-size collections of homogeneous elements, such as [i32; 5

 

Example 1:

Rust




fn main() {
    // we use let to declare a variable
    let variable = 2;
    println!("{}", variable);
    
    // we use let with mut to make the variable mutable
    let mut variable = variable;
    
    println!("{}", variable);
    variable = 5 * 2;
    println!("{}", variable);
}


Output: 

2
2
10

Constants

While variables declared with the let keyword make them immutable, there is another separate const keyword for declaring constants. Constants are similar to immutable variables when we compare immutability, but they have some additional features assigned and restrictions, so here are the rules:

  1. Constants must have an explicit type annotation
  2. Constants can only be initialized with constant expressions, meaning they must have a value that can be determined at compile time. Runtime calculations and function calls are not allowed for initializing constants.
  3. Constants have a global scope, which means you can use them throughout your entire program, not just within the scope where they are declared.
  4. Constants cannot be bound to mut like let allows.

Example 2:

Rust




fn main() {
    // this is a constant in rust, const cannot be bound to a mut state
    const PI: f32  = 3.14;
    println!("{}", PI);
}


Output:

3.14

Integers

Integer types in Rust and their maximum values. Let’s take a look at the types of integers that Rusts offers.

Integer Type Description
Signed integer types
  1. i8, i16, i32, i64, and i128 are signed integer types in Rust, where the first number indicates the number of bits used to represent the integer. For example, i8 uses 8 bits and can represent integers from -128 to 127.
  2. isize is a signed integer type with a size that depends on the architecture of the system it’s running on (32 bits or 64 bits).

Unsigned integer types

  1. u8, u16, u32, u64, and u128, u stands for Unsigned making the integers only represent non-negative numbers. For example, u8 uses 8 bits and can represent integers from 0 to 255, if we try to give the Unsigned type a negative Rust will generate an error.
  2. usize like isize but Unsigned (32 bits or 64 bits).

Signed integers are in most programming languages, and signed integer types are more common than unsigned integer types. The default integer type in many programming languages, such as Java, C#, Python, and Kotlin, is a 32-bit signed integer.

Using signed integers allows developers to work with a wider range of values ​​without having to worry about the limitations of unsigned integers, which can only represent NON-negative values. However, unsigned integer types have their use cases, like interacting with array indexes, we can only represent NON-negative numbers.

Example 3:

Rust




fn main() {
    // types of int with their max values
  
    // i8 is same as Byte type in java, kotlin, C# and others
    let int: i8 = i8::MAX;
    println!("i8 : {}", int);
  
    let int: i16 = i16::MAX;
    println!("i16 : {}", int);
  
    // i32 is the default Int in most programing languages
    let int: i32 = i32::MAX;
    println!("i32: {}", int);
  
    // i64 is same size as Long in other programing languages
    let int: i64 = i64::MAX;
    println!("i64 : {}", int);
  
    let int: i128 = i128::MAX;
    println!("i128 : {}", int);
  
    // isize is bound to the system. Eg :: (32 bits or 64 bits)
    let int: isize = isize::MAX;
    println!("isize : {}", int);
  
    // the unsigned ints are integers which variable can only receive NO-negatives
    let uint: u8 = u8::MAX;
    println!("u8 : {}", uint);
  
    let uint: u16 = u16::MAX;
    println!("u16 : {}", uint);
  
    let uint: u32 = u32::MAX;
    println!("u32 : {}", uint);
  
    let uint: u64 = u64::MAX;
    println!("u64 : {}", uint);
  
    let uint: u128 = u128::MAX;
    println!("u128 : {}", uint);
  
    let uint: usize = usize::MAX;
    println!("usize : {}", uint);
}


 

In the code above, we can see all the integer types in Rust with their:: Max values ​​assigned, and in the output, we can see that the value of unsigned is doubled because it doesn’t save bits to represent negatives and this allows these types to represent a large variety of positive aspects.

Output: 

i8 : 127
i16 : 32767
i32: 2147483647
i64 : 9223372036854775807
i128 : 170141183460469231731687303715884105727
isize : 9223372036854775807
u8 : 255
u16 : 65535
u32 : 4294967295
u64 : 18446744073709551615
u128 : 340282366920938463463374607431768211455
usize : 18446744073709551615

Float

In Rust, there are two floating-point types: f32 and f64. These represent 32-bit single-precision and 64-bit double-precision floating-point numbers, respectively. The f64 type is more precise and has a larger range, making it the default floating-point type in Rust.

Example 4:

Rust




fn main() {
    // types of floats with their max values
  
    // f32 occupies 32 bits of memory and less precise than f64
    let mut float: f32 = 1.00000762;
    float += 1.00000762;
    println!("{}", float);
  
    // f64 occupies 64 bits of memory and is more precise
    let mut float: f64 = 1.00000762;
    float += 1.00000762;
    println!("{}", float);
}


 

We can see from the different output when loading the variable, while f64 allows us to have more precision in the decimals, f32 rounds the value to 7 decimals, so if the program only needs 0.2 to 0.5 decimals we can use f32, if it needs 0.5 to 0.15 decimal, an f64 type handles this better.

Output:

2.0000153
2.00001524

Boolean

Boolean in Rust is like in most other programming languages, it has only two possible values, true or false which are reserved keywords for Boolean types. You can use Boolean to check conditions, it stores comparisons between variables, by default when checking conditions in languages ​​they always check if it has a true value if you want to check false just use ! in front of the variable.

Example 5:

Rust




fn main() {
    // declare a variable as a boolean
    let is_true: bool = true;
    println!("I'm reading a GFG article : {}", is_true);
  
    // declare another boolean variable
    let is_false: bool = false;
    println!("I go outside : {}", is_false);
  
    // using booleans in if condition
      
    // is_true it check if the boolean has a true value
    if is_true {
        println!("The condition is true!");
    } else {
        println!("The condition is false!");
    }
      
    // !is_true will check if the boolean has  a false value
    if !is_true {
        println!("The condition is false!");
    } else {
        println!("The condition is true!");
    }
  
    // we can do any type of comparation and store the result in a boolean
    let a = 5;
    let b = 10;
    let is_greater: bool = a > b;
    println!("Is {} greater than {}? {}", a, b, is_greater);
      
    let c = 10.2;
    let d = 10.9;
    println!("Is {} greater than {}? {}", d, c, d > c);
      
}


Output:

I'm reading a GFG article : true
The economy looks great : false
The condition is true!
The condition is true!
Is 5 greater than 10? false

Char

Now let’s see the code demonstrates the use of the char type in Rust and how to store individual characters, including Unicode characters.

  • let c: char = ‘A’; declares a variable named c of type char and assigns the character between two to it.
  • let unicode_char: char = ‘\u{42}’; we can assign any Unicode character using \u{code}.

Example 6:

Rust




fn main() {
    // type a variable as a char
    let c: char = 'C';
    println!("the character is: {}", c);
  
    // unicode value for the character @
    let unicode_char: char = '\u{40}';
    println!("Unicoded: {}", unicode_char);
}


Output:

the character is: C
Unicoded: @

String

Before we take a look at Tuples and Arrays, we should take a quick look at a String, and since this type is built on top of a more complex data structure than the basic type, they are not considered primitive types.

In Rust, we have two types of String:

  • The string is the type that is similar to other implementations used in another programing language. The String is the type that is mutable and allows manipulation of itself, we can increase the string remove elements, etc.
  • &str is a reference to the s type of String, it references the above String type, when we use this type we are only allowed to read from the variable, we cannot modifier it, as it’s immutable.

The below code demonstrates the basic initialization of String and &str types in Rust.

Example 7:

Rust




fn main() {
    // String::from() create a new String like most other languages
    let text = String::from("A random text");
    
    // &str is a string type immutable, a reference to the String type 
    let name: &str = "Any";
    
    println!("{name}");
    println!("{text}");
}


Output:

Any
A random text

Tuples

Now tuple is a general way of grouping together a number of values with a variety of types into one compound type. Tuples have a fixed length: once declared, they cannot grow or shrink in size. 

The signature starts let or const  keywords, next step is to name and assign the elements in parentheses, so we have this signature: let <name> : (<define type before assign>) = (<assign elements separated by a comma, assign in order of types if you define type previous>)

Tuples we can read elements and edit them if declared with let mut only and with let if assign it to a new variable.
 

Here’s an example demonstrating how to initialize tuples in Rust, here we also used the print Debug formatter which is a println!(“{:?}”, tup), it’s shown on the console the elements inside a struct.

Infer type Explicit type
let tup = (500, “A random text”, 0.32) let tup: (i32, &str, f64) = (500, “A random text”, 0.32)

Example 8:

Rust




fn main() {
    // tuples are way to store different types together
    let tup: (i32,  &str, f32) = (500, "This a immutable string", 0.32);
    println!("{:?}", tup);
    
    // you can replace &str for String to make the str mutable
    let tup: (i32,  String, f32) = (500, String::from("This a mutable string"), 0.32);
    println!("{:?}", tup);
  
    // with a mut tuple we can edit like this
    let mut tup = tup;
    tup.0 = 10;
    tup.1 = String::from("editing");
    tup.2 = 0.64;
    println!("{:?}", tup);
}


Output:

(500, "This a immutable string", 0.32)
(500, "This a mutable string", 0.32)
(10, "editing", 0.64)

Array

Another way to have a collection of multiple values is with an array. Unlike a tuple, every element of an array must have the same type.

The below code demonstrates different ways to create and initialize arrays in Rust.

  • Initializing an array with elements.
  • Initializing an array with explicit type, size, and elements.
  • Initializing an array with a default value and size.
  • Initializing an array with a default value, size, and explicit type.
Infer type array Explicit type array Infer type by default value Explicit type and set the default value
let array = [1, 2, 3, 4, 5]; let array: [i32; 5] = [1, 2, 3, 4, 5]; let array = [1; 5]; let array: [$str; 5] = [“default”; 5];

Example 9:

Rust




fn main() {
    // this is how you init a array = [elements]
    let array = [1, 2, 3, 4, 5];
    println!("{:?}", array);
    
    // this is how you define the type of the 
  // array [<type>; <size>] = [elements]
    let array: [i32; 5] = [1, 2, 3, 4, 5];
    println!("{:?}", array);
    
    // this is how you init a array with a default :: 
  //= [<type and default value>, <size>]
    let array = ["this will be my default value"; 5];
    println!("{:?}", array);
    
    // this is same above array, but hand typed. [<type>; 
  // <size>] = [<type and default value>, <size>]
    let array: [&str; 5] = ["default"; 5];
    println!("{:?}", array);
    
    // with a mut array now
    let mut array = array;
    array[0] = "can";
    array[1] = "edit";
    array[2] = "elements";
    array[3] = "this";
    array[4] = "way";
    println!("{:?}", array);
    
    // remember when hand type, assign has to match the type
}


Output: 

[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
["this will be my default value", "this will 
be my default value", "this will be my default value",
"this will be my default value", 
"this will be my default value"]
["default", "default", "default", "default", "default"]
["can", "edit", "elements", "this", "way"]

Hand-Typing

Even though all the example code given here has an explicit type when creating the variable, Rust is also able to infer the type at compile time, which means we don’t have to give it an explicit type. But explicit typing provides clarity making the code easier to understand for other developers or even for yourself when you revisit the code later. It helps to understand the purpose and constraints of the variable, specifying the type also helps the compiler to ensure that the value assigned to the variable meets the constraints of the type specified in advance. That way, the compiler can catch potential type-related errors early in the development process.



Last Updated : 01 May, 2023
Like Article
Save Article
Previous
Next
Share your thoughts in the comments
Similar Reads