Data Types

In this Rust tutorial lesson we learn more about the primitive data types in Rust. We cover bool, char, the various signed and unsigned ints, float and double. We also introduce and briefly cover the problem of integer overflow.

What are data types

In a previous tutorial about variables and constants, we learned that data containers have a data type that represents the type of value we are able to store inside them.

Data types are simply the different types of values that are supported by the Rust language. These data types are controlled by The Rust Type System and specify which types can be stored or manipulated by a program.

The type system ensures that sufficient space is reserved in memory for a specific type, and that code behaves as we expect. It also allows for code hinting and automated documentation.

A variable initialization in Rust can automatically infer the data type based on the value stored inside it. However, we can explicitly declare a data type for the variable.

Example:
fn main() {

    let pi:f64 = 3.14;

    println!("The value of PI is: {}\n", pi);

}

In the example above, the f64 part of the variable initialization is the data type.

In this lesson we’ll take a look at the following primitive data types in Rust.

  • bool
  • char
  • i8, i16, i32, i64, i128
  • u8, u16, u32, u64, u128
  • usize
  • f32, f63

Rust also supports the following data types, which we cover in their own separate tutorial sections.

  • Array
  • Tuple
  • Slice
  • String

The bool (boolean) type

The bool (boolean) data type represents two truth values of logic, true and false.

The values of true and false are keywords in Rust that represent 1 and 0 respectively, and they help us to write code with logical conditions.

Example:
fn main() {

    let is_learning = true;
    let explcit_type:bool = true;

    println!("is_learning: {}", is_learning);

}

As mentioned, booleans are often used in logical conditional statements such as if.

Example:
fn main() {

    let is_birthday = true;

    // conditional
    if is_birthday {

        println!("Happy Birthday!");
    } else {

        println!("No cake for you");
    }

}

We cover if statements in the tutorial lesson on conditional control flow.

The char (unicode character) type

The char type is a single unicode character such as A or a. Because of unicode support, a char in Rust uses 4 bytes of memory.

Example:
fn main() {

    let letter = 'a';
    let sigma = 'Σ';
    let emoji = '❤';
    let coffee = '☕';

    println!("{}", letter);
    println!("{}", sigma);
    println!("{}", emoji);
    println!("{}", coffee);

}

A char type must be enclosed within single quotes, unless we’re specifying the unicode escape sequence.

A unicode escape sequence is a sequence of numbers that Rust automatically converts into a unicode character.

Example:
fn main() {

    let character = '❤';
    let escape_sequence = "\u{2764}";

    println!("Character: {}", character);
    println!("Escape Sequence: {}", escape_sequence);

}

The unicode escape sequence for the heart symbol is \u2764. When we assign it to a variable we need to format it differently than we would the symbol itself.

  1. The escape sequence must be enclosed within double quotes.
  2. The numbered portion of the escape sequence must be enclosed within open and close curly braces { }.

RapidTables has a full list of Unicode characters with their escape sequences and html numeric code.

Signed int - i8, i16, i32, i64, i128

An integer is a whole number without any decimal points.

The i represents the word integer, specifically signed integer. The number represents the size of the integer value. A signed numerical value can be both negative and positive.

The following table lists each signed integer with their minimum and maximum values.

Int TypeMinMax
i8-128127
i16-3276832767
i32-21474836482147483647
i64-92233720368547758089223372036854775807
i128-170141183460469231731687303715884105728170141183460469231731687303715884105727

If we explicitly declare the type of a variable, we need to use one of the keywords above.

Example:
fn main() {

    let number_small:i8 = -128;

    println!("i8: {}", number_small);
    println!("i8 Min: {}", i8::min_value());
    println!("i8 Max: {}", i8::max_value());

}

If at any time we need to see the minimum and maximum values of a type, we can use the min_value() and max_value() functions.

If you are familiar with other languages, like the C family, we could compare the Rust types to the following integer types:

Rust typeSame as
i8byte
i16short
i32int
i64long
i128long long

Unsigned int - u8, u16, u32, u64, u128

An unsigned integer is a whole number, similar to a signed integer. However, an unsigned integer doesn’t support negative values.

The following table lists each unsinged integer with their minimum and maximum values.

Int TypeMinMax
u80255
u16065535
u3204294967295
u64018446744073709551615
u1280340282366920938463463374607431768211455

We use an unsigned type when we know the value shouldn’t go below 0. An example of this would be the hitpoints in a game.

Example:
fn main() {

    let number_small:u8 = 255;

    println!("u8: {}", number_small);
    println!("u8 Min: {}", u8::min_value());
    println!("u8 Max: {}", u8::max_value());

}

Similar to signed integers, we can use the min_value() and max_value() functions to see the minimum and maximum value an unsigned integer may contain.

Integer Overflow

When a number inside a data container is larger than the type allows, it will produce what is known as an int overflow.

Example:
fn main() {

    let number_small:u8 = 256;

    println!("u8: {}", number_small);

}

The maximum value for an unsigned 8-bit integer is 255. In the example above, we specify the number as 256 so the compiler raises an error when we try to compile the program.

Output:
error: literal out of range for `u8`
 --> src\main.rs:3:24
  |
3 |     let number_small:u8 = 256;
  |                           ^^^
  |
  = note: #[deny(overflowing_literals)] on by default

error: aborting due to previous error

The error: literal out of range for ‘u8’ means that we are not in the valid range for a u8 number.

The compiler will detect int overflow at compile time, but if we change the variable at runtime, it will “panic”.

Example:
fn main() {

    let number_small:u8 = 255;
    println!("u8: {}", number_small);

    // cause int overflow
    let number_small = number_small + 1;
    println!("u8: {}", number_small);
}

In the example above, we add 1 to the variable at runtime and try to print it again.

Output:
  thread 'main' panicked at 'attempt to add with overflow', src\main.rs:7:21

The error above states that we tried to overflow the number at runtime, which caused a mess. Subsequently, the program couldn’t exit successfully.

The solution would be to use specialized overflowing_* functions to check of the value can be added to without causing overflow.

Example:
fn main() {

    let number_small:u8 = 255;
    println!("u8: {}", number_small);

    // cause int overflow
    let result = number_small.overflowing_add(1);
    println!("{:?}", result);

}

In the example above, we use the overflowing_add() function to check if we can add 1 to the variable number_small.

The result is a tuple that shows the addition as well as a boolean true or false if the overflow would occur.

Output:
  (0, true)

In this case an overflow would occur, as stated by the true value. The 0 is what the result of the operation would be if we did add 1 to the number_small variable.

The number will revert to 0 in an overflow situation for protection.

The concept and its solution is too advanced for us at this stage in the course, so we will cover it again in more depth in a later tutorial.

However, you can read more on the overflowing functions in the Rust documentation.

Float & Double - f32, f64

Rust supports floating-point numbers. That’s to say, numbers with a decimal point.

The f32 type is a single precision floating point number, and f64 is a double precision floating point number.

If you’re familiar with other programming languages such as the C family, f32 is like a float and f64 is like a double.

Example:
fn main() {

    let float:f32 = 1.123456;
    let double:f64 = 1.123456789012345;

    println!("Float: (6 digit precision) {}", float);
    println!("Double: (15 digit precision) {}", double);

}

The f64 (or double) is more commonly used due to the wider range it has over a float, in spite of slower performance and bandwidth costs.

Further reading:

Summary: Points to remember

  • Data types indicate to Rust the type of data that can be temporarily stored or manipulated by the program.
  • The bool type only has the values true and false, representing 1 and 0 respectively.
  • The char type is a single Unicode character, wrapped inside single quotes.
  • An integer is a whole number and supports various sizes from i8 and u8 to i128 and u128. They could be compared to types in other languages, such as C:
    • i8 is a byte
    • i16 is a short
    • i32 is an int
    • i64 is a long
    • i128 is a long long
  • Signed integers support negative values, unsigned integers only support positive values starting at 0.
  • Int overflow occurs when we try to store a value greater than what the type can hold.
  • f32 (float) is a floating point number with single precision. It takes up 32 bytes in memory and has a precision of 6 digits.
  • f64 (double) is a floating point number with double precision. It takes up 64 bytes in memory and has a precision of 15 digits.