DEV Community

Cover image for Rust number types explained
Augustine Madu
Augustine Madu

Posted on • Edited on • Originally published at cudi.dev

Rust number types explained

A number can either be an integer or a float. Numbers without fractional components are integers, while numbers with fractional components are called floats, which is short for a floating point number.

Integers in Rust may either be signed or unsigned. Signed integers as the name implies are integers that carry a sign. They can be negative, positive, or zero. Unsigned integers on the other hand are integers without a sign and are either positive or zero.

If you prefer a video explanation of this article, you can watch the embedded video below.

Signed and Unsigned Integers

Signed integers are labeled with an i followed by the size of the integer specified in bits. Unsigned integers are labeled with a u followed by the size of the integer.

//integers: signed / unsigned integers

//signed
let x: i8 = 2;

let y: i16 = -3;

let z: i32 = 0;

//unsigned
let a: u8 = 2;

let b: u16 = 3;

let c: u32 = 0;
Enter fullscreen mode Exit fullscreen mode

We can specify the size and type of the integer by adding the label at the end of the number literal.

//integers: signed / unsigned integers

//signed
let x = 2_i8; // => i8

let y = -3_i16; // => i16

let z = 0_i32; // => i32

//unsigned
let a = 2_u8; // => u8

let b = 3_u16; // => u16

let c = 0_u32; // => u32
Enter fullscreen mode Exit fullscreen mode

If we don’t specify the type of integer, the variable is given a signed 32-bit integer by default.

let x = 2; // => i32

let y = -3; // => i32

let z = 0; // => i32
Enter fullscreen mode Exit fullscreen mode

There are 12 variants of the integer types in Rust and they are classified by sign and size. We have 8, 16, 32, 64, and 128-bit integers, which can be either signed or unsigned.

Rust integer types

The isize and usize variants, their sizes depend on the architecture of the computer on which the program is running. For 32-bit systems, the isize becomes i32 and the usize becomes u32. Likewise, for 64-bit systems, the isize is i64 and u64 for usize. They are mostly used in indexing a collection, like arrays and vectors.

Each integer variant can only store integers of a certain range due to their size and sign. For signed integers i-n the range is between 2n1-2^{n-1} to 2n112^{n-1}-1 inclusive. For unsigned integers u-n, its range is between 00 to 2n12^n - 1 .

So i8 can store integers from 27-2^7 to 2712^7 - 1 which is 128-128 to 127127 . While u8 can store integers from 127127 to 2812^8 - 1 , which equals 255255 . You can do this for other integer variants to get their ranges.

8 bits equals 1 byte, so an i8 and u8 integer takes up 1 byte in the memory, 16-bit integers take up 2 bytes, 32 take up 4, 64 take up 8, and so on.

To get the size of a data type, you can use the size_of function obtained from the mem module of the standard library.

use std::mem::size_of;

fn main() {
  println!("size of i8   : {}", size_of::<i8>());

  println!("size of i16  : {}", size_of::<i16>());

  println!("size of i32  : {}", size_of::<i32>());

  println!("size of isize: {}", size_of::<isize>());
}
Enter fullscreen mode Exit fullscreen mode
size of i8   : 1
size of i16  : 2
size of i32  : 4
size of isize: 8
Enter fullscreen mode Exit fullscreen mode

This also works for compound types like structs and tuples, even pointers and references.

Representing integer literals in Rust

There are several ways of writing a number literal. For large numbers, we can separate the numerals with an underscore for better readability.

//without underscore
let large_num = 12345678;

//with underscore
let large_num = 12_345_678;
Enter fullscreen mode Exit fullscreen mode

Integers can be written in hexadecimal format, in octal, or in binary form. For hexadecimal format, we prefix the integer with 0x, 0o for octal, and 0b for binary.

//decimal
let x = 42;

//hex
let y = 0x2a;

//octal
let z = 0o52;

//binary
let w = 0b101010;
Enter fullscreen mode Exit fullscreen mode

When the values of the integers are printed they will be displayed in their decimal format.

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

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

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

println!("w: {}", w);
Enter fullscreen mode Exit fullscreen mode
x: 42
y: 42
z: 42
w: 42
Enter fullscreen mode Exit fullscreen mode

To display an integer in hexadecimal format, in the curly brackets, we specify we want the integer to be formatted to hexadecimal by including a colon : followed by an x. For octal, we use o instead of x, and for binary we use b.

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

println!("y: {:x}", y);

println!("z: {:o}", z);

println!("w: {:b}", w);
Enter fullscreen mode Exit fullscreen mode
x: 42
y: 2a
z: 52
w: 101010
Enter fullscreen mode Exit fullscreen mode

Integer Overflow in Rust

One more thing to talk about before moving to a floating point number is an integer overflow. Since each integer variant can only store integers of a specific range. What happens when it overflows?

Here, we have a u8 integer whose value is 255. Since 255 is the largest u8 integer, if we add 1, it overflows.

// 255 is the max u8 integer
let x: u8 = 255;

// this will cause an overflow
let x: u8 = x + 1;
Enter fullscreen mode Exit fullscreen mode

When we build this program, the program won’t compile since the compiler can detect the overflow.

If the variable comes from an external source, like the command line, the compiler can’t detect an overflow if we perform arithmetic operations on it.

Here, we have a program that reads two inputs from the command line and parses them to a u8 integer, then prints the sum of those 2 integers.

fn main() {
  println!("Enter first number: ");

  let mut input = String::new();

  std::io::stdin().read_line(&mut input);

  let first_num: u8 = input.trim().parse().expect("Enter a valid u8 integer");

  println!("Enter second number: ");

  let mut input = String::new();

  std::io::stdin().read_line(&mut input);

  let second_num: u8 = input.trim().parse().expect("Enter a valid u8 integer");

  let sum = first_num + second_num;

  println!("sum: {sum}");
}
Enter fullscreen mode Exit fullscreen mode

If we run this program in debug mode and enter two integers whose sum is greater than 255, the program panics.

But if we build and run this program in release mode, for the first integer enter 255, and for the second enter 1.

Enter first number: 
255
Enter second number: 
1
sum: 0
Enter fullscreen mode Exit fullscreen mode

The output is 0, this is because in release mode, instead of panicking, our program wraps it from the minimum. 256 becomes 0, 257 becomes 1, and so on.

256   257   258   259   260
 :     :     :     :     : 
 0     1     2     3     4 
Enter fullscreen mode Exit fullscreen mode

To handle the cases of overflow explicitly, you can use the wrapping add method, which wraps it from the minimum, 255 plus 2 equals 257, and since it’s greater than 255 the maximum unsigned 8-bit integer, it is then wrapped, and x becomes 1.

let x = 255_u8.wrapping_add(2);

println!("wrapping_add : {:}", x);
Enter fullscreen mode Exit fullscreen mode
wrapping_add : 1
Enter fullscreen mode Exit fullscreen mode

The overflowing add method wraps the sum but returns a tuple where the first element is the wrapped sum and the second element is a boolean indicating whether the sum was wrapped. In this case, it is true since 257 was wrapped to become 1.

let y = 255_u8.overflowing_add(2);

println!("overflowing_add : {:?}", y);
Enter fullscreen mode Exit fullscreen mode
overflowing_add : (1, true)
Enter fullscreen mode Exit fullscreen mode

The checked add method returns the None value if it overflows, and the saturating method saturates the value at the maximum or minimum depending on where it overflowed.

let z = 255_u8.checked_add(2);

let w = 255_u8.saturating_add(2);

println!("checked_add    : {:?}", z);

println!("saturating_add : {:?}", w);
Enter fullscreen mode Exit fullscreen mode
checked_add    : None
saturating_add : 255
Enter fullscreen mode Exit fullscreen mode

There are similar methods for other arithmetic operations like subtraction, multiplication, division, exponential and so on.

Floats

Floating point numbers are numbers with fractional components or decimal points. In Rust, a float can be either 32 bits or 64 bits in size.

64-bit floats have higher range and precision than 32-bit floats.

Floating point types are specified with an f followed by the number of bits.

// floats (32-bit or 64-bit)

// 32-bit
let x: f32 = 3.14;

let y: f32 = 2.00;

// 64-bit
let a: f64 = 3.14;

let b: f64 = 2.00;
Enter fullscreen mode Exit fullscreen mode

We can specify the type of float after the variable name and a colon or at the end of the number literal.

// floats (32-bit or 64-bit)

// 32-bit
let x = 3.14_f32;

let y = 2.00_f32;

// 64-bit
let a = 3.14_f64;

let b = 2.00_f64;
Enter fullscreen mode Exit fullscreen mode

If we do not specify the type of float, it is given an f64 by default.

let a = 3.14 // => f64
Enter fullscreen mode Exit fullscreen mode

A 32-bit float is a bit faster and uses less memory than a 64-bit float but it comes with less range and precision.

Below are the minimum and maximum values a 32-bit and a 64-bit float can have.

f32::MIN ->          -3.40282347E+38  
f32::MAX ->           3.40282347E+38  

f64::MIN -> -1.7976931348623157E+308  
f64::MAX ->  1.7976931348623157E+308
Enter fullscreen mode Exit fullscreen mode

32-bit floats are best used in graphics & video processing where memory usage is a primary concern and their range and precision are significant for the task.

64-bit floats on the other hand are used in scientific calculations and statistical analysis where precision is very necessary.

Byte Number Literal

There is also a kind of number literal called byte, which stores ASCII character literal as an 8-bit unsigned integer.

let a = b'A'; // => u8
Enter fullscreen mode Exit fullscreen mode

If we print out the variable a, we get the numerical value of the ASCII character ‘A’.

println!("b'A = {a}");
Enter fullscreen mode Exit fullscreen mode
b'A' = 65
Enter fullscreen mode Exit fullscreen mode

Replacing the character literal with a string literal will result in an array of bytes, which is an array of u8 integers.

let a = b"Apple";
Enter fullscreen mode Exit fullscreen mode

Printing out this variable we can see it’s an array containing the numerical value of each ASCII character.

println!("{a:?}");
Enter fullscreen mode Exit fullscreen mode
[65, 112, 112, 108, 101]
Enter fullscreen mode Exit fullscreen mode

Thanks for reading.

References:

https://cudi.dev/articles/rust_number_types_explained

Top comments (0)