Skip to content

Data Types

Scalar Types

Represents a single value

Integer Types

  • signed range: \(-(2^{n-1}) \rightarrow 2^{n-1}-1\)
  • unsigned range: \(0 \rightarrow 2^n-1\)

By default i32 is used

Type Description Range
i8 Integer 8 bit signed value -128 to 127
i16 Integer 16 bit signed value -32'768 to 32'767
i32 Integer 32 bit signed value -2'147'483'648 to 2'147'483'647
i64 Integer 64 bit signed value -9'223'372'036'854'775'808 to 9'223'372'036'854'775'807
i128 Integer 128 bit signed value -170'141'183'460'469'231'731'687'303'715'884'105'728 to 170'141'183'460'469'231'731'687'303'715'884'105'727
isize Integer architecture dependent signed value depends on architecture
u8 Integer 8 bit unsigned value 0 to 255
u16 Integer 16 bit unsigned value 0 to 65'535
u32 Integer 32 bit unsigned value 0 to 4'294'967'295
u64 Integer 64 bit unsigned value 0 to 18'446'744'073'709'551'615
u128 Integer 128 bit unsigned value 0 to 340.282'366'920'938'463e36
usize Integer architecture dependent unsigned value depends on architecture

Literals

Can be separated by an _ for easier read.

Number literal Example
Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b1111_0000
Byte (u8 only) b'A'

Floating-Point Types

Type Description Range Approximation Precision
f32 32-bit IEEE 754 floating point \(\pm 1.4 × 10^{-45}\) to \(\pm3.4 × 10^{38}\) \(\approx 6-7\) decimal digits
f64 64-bit IEEE 754 floating point \(\pm5.0 × 10^{-324}\) to \(\pm1.7 × 10^{308}\) \(\approx 15-16\) decimal digits
let x = 2.0;      // f64 double precision
let y: f32 = 3.0; // f32 single precision

bool Boolean Type

Used in conditionals. One bytes sized.

let t = true;        // type inferred
let f: bool = false; // with explicit type annotation

char Character Type

let c = 'z';               // type inferred
let z: char = 'Z';         // with explicit type annotation
let heart_eyed_cat = '😻'; // utf-8 support

Compound Types

Type Tuple

Grouping together different values of different types.

// create a tuple
let tup: (i32, f64, u8) = (50, 6.4, 1);

// destructing a tuple
let (x,y,z) = tup;

// access a single element
let w = tup.0;            // 50

// tuple without any value is unit
// represent empty value, empty return type
()

Array Types

Arrays are fixed size and live in the stack

Declaring arrays

// declare an array
let arr = [1, 2, 3, 4, 5];
let arr = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"];

// declare an array with its type
let arr : [<type>;<nbrElements>] = [1, 2, 3, 4, 5];
let arr : [i32;5] = [1, 2, 3, 4, 5];

// initialise array with same values
let arr = [<initvalue>; <nbrElements>]
let arr = [3; 5];  // equal to let arr = [3, 3, 3, 3, 3];

Accessing arrays

// unmutable array
let arr = [1, 2, 3, 4, 5];
let first = arr[0];            // 1
let second = arr[1];           // 2
let last = arr[arr.len(-1)];   // 5

let slice = &arr[1..4]         // [2, 3, 4]
let slice = &arr[1..=4]        // [2, 3, 4, 5]

// mutable array
let mut arr = [1, 2, 3, 4, 5];
let slice = &mut arr[1..4]     // [2 ,3 ,4]
slice[0] = 6                   // [6, 3, 4]

Array functions

let size = arr.len()     // get length of an array
println!("{:?}", arr);   // print array with Debug trait

String Type

String is a dynamic datatype which can grow and shrink over time. It hold multiple char <char> The data itself is stored in the heap and the String representation ptr, lengthand capacityis stored in the stack memory.

String Literal

let s = String::from("hello");
s.push_str(", world!");

Slice Types

let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];
assert_eq!(slice, &[2, 3])

String slices

Reference to part of a string- It contains a pointer to that start of the data and a length.

let s = String::from("hello world");

let hello = &s[0..5];   // referencing word hello
let world = &s[6..11];  // referencing word world

String slice reference
Figure 1: String slice reference

let s = String::from("hello");
let len = s.len();

// trailing index can be dropped if it referes to the last element
let slice = &s[3..len];
let slice = &s[3..];

// preceding index can be dropped if it referes to the 0 element
let slice = &s[0..len];
let slice = &s[..];

enum

enum creates a type which can hold one of different variants. Enums are particularly powerful in Rust and are the idiomatic way to represent state and handle various data patterns.

Basic Enum Usage

enum SimpleEnum {
  FirstVariant,
  SecondVariant,
  ThirdVariant,
}

enum ComplexEnum {
  Nothing,
  Something(u32),
  LotsOfThings {
    usual_struct_stuff: bool,
    blah: String,
  }
}

enum variants can hold different types of data:

enum IpAddrKind {
  V4(u8, u8, u8, u8), // enum variants with list u8 data
  V6(String),         // enum variants with string data
}

enum Message {
  Quit,                       // variant with no data
  Move { x: i32, y: i32 },    // variant with anonymous struct
  Write(String),              // variant with string
  ChangeColor(i32, i32, i32), // variant with three integers
}
impl Message {
  fn some_function() {
    println!("some text")
  }
}

fn main() {
    let localhost = IpAddrKind::V4(127, 0, 1, 1);
}

match goes well with enums because all variants need to be matched:

enum Coin {
  Rappen,
  Franken,
  Zweifänkler,
  Fünfliber,
}

fn value_in_rappen(coin: Coin) -> u8 {
  match coin {
    Coin::Rappen => 5,
    Coin::Franken => 100,
    Coin::Zweifränkler => 200,
    Coin::Fünfliber => 500,
  }
}

Enums for State Management

Enums are the idiomatic way to represent state in Rust applications. Instead of using bool or integer constants, use enums to make your code more expressive and type-safe.

❌ Avoid using bool for state:

struct User {
    name: String,
    active: bool, // What does false mean? Inactive? Suspended? Deleted?
}

✅ Use enums instead:

use chrono::{DateTime, Utc};

#[derive(Debug, Clone)]
enum UserStatus {
    /// The user is active and has full access
    Active,
    /// The user's account is inactive but can be reactivated
    Inactive,
    /// The user's account has been temporarily suspended
    Suspended { until: DateTime<Utc> },
    /// The user's account has been permanently deleted
    Deleted { deleted_at: DateTime<Utc> },
}

struct User {
    name: String,
    status: UserStatus,
}

State Transitions with Methods

You can implement methods on enums to handle state transitions safely:

impl UserStatus {
    /// Suspend the user until the given date
    fn suspend(&mut self, until: DateTime<Utc>) {
        match self {
            UserStatus::Active => *self = UserStatus::Suspended { until },
            // For all non-active states, do nothing
            _ => {}
        }
    }

    /// Activate the user
    fn activate(&mut self) -> Result<(), &'static str> {
        match self {
            // A deleted user can't be activated!
            UserStatus::Deleted { .. } => Err("can't activate a deleted user"),
            _ => {
                *self = UserStatus::Active;
                Ok(())
            }
        }
    }

    /// Delete the user permanently
    fn delete(&mut self) {
        if let UserStatus::Deleted { .. } = self {
            // Already deleted, don't update timestamp
            return;
        }
        *self = UserStatus::Deleted {
            deleted_at: Utc::now(),
        }
    }

    // Helper methods for checking state
    fn is_active(&self) -> bool {
        matches!(self, UserStatus::Active)
    }

    fn is_suspended(&self) -> bool {
        matches!(self, UserStatus::Suspended { .. })
    }
}

Option

Option is also an enumof a generic types. It contains None or Some(). It is included by default in the scope.

enum Option<T> {
  None,            // no value
  Some(T),         // some value
}

Examples of Optional types.

let some_number = Some(5);             // type Option<i32> inferred
let some_string = Some("a string");    // type Option<&str> inferred
let absent_number: Option<i32> = None; // type can't be inferred

To extract a value from an Optional all possible variants needs to be covered .

let x: i8 = 5;
let y: Option<i8> = Some(5);

// use value of y if there is Some otherwise use default value
let sum = x + y.unwrap_or(0);
fn plus_one(x: Option<i32>) -> Option<i32> {
  match x {
    None => None,
    Some(i) => Some(i+1),
  }
}

Result

Result is also an enum of generic types. It contains a Ok value or an Err.

enum Result<T, E> {
  Ok(T),
  Err(E),
}

struct

Block of data grouped together. Can derive traits and implement methods or associated methods

#[derive(Debug)] // basic debug trait implementation
struct Rectangle {
  width: u32,
  height: u32
}

// multiple impl possible
impl Rectangle {
  // Method (function tied to an instance of a struct)
  fn area(&self) -> u32 {
    self.width * self.height
  }

  fn can_hold(&self, rectangle: &Rectangle) -> bool {
    let area1 = self.area();
    let area2 = rectangle.area();

    return area1 > area2;
  }
}
impl Rectangle {
  // associated method (missing the &self, not tied to an instance of a struct
  fn square(size: u32) -> Self {
    Rectangle {
      width: size,
      height: size
    }
  }
}

fn main() {
  let rect = Rectangle {
    width: 30,
    height: 50,
  };
  let rect1 = Rectangle {
    width: 20,
    height: 40,
  };
  let rect2 = Rectangle {
    width: 40,
    height: 50,
  };
  let _rect3 = Rectangle::square(30);

  println!("rect can hold rect1: {}", rect.can_hold(&rect1));
  println!("rect can hold rect2: {}", rect.can_hold(&rect2));
  println!("rect: {:?}", rect); // debug print
  println!("rect: {:#?}", rect); // debug print with line fix

  println!("The width of rect is {} pixels", rect.width);
  println!("The area of rect is {} pixels", rect.area());
}

Collections

Dynamic datatypes (partly shown above) stored in the heap and can shrink and grow over time.

vec Vector

vec can store only one type of elements

Creating vector

let v:Vec<i32> = Vec::new(); // type can't be inferred and needs to be explicitaly mentioned
v.push(1);
v.push(2);
v.push(3);

let v2 = vec![1,2,3];        // type can be inferred. the vec! macro allows initilising the vector from start

Accessing vector elements

let v = vec![1,2,3,4,5];

// accessing an existing element
let third: = &v[2];
println!("The third element is {}", third);

// accessing inexisting element
let twenty: = &v[20];  // possible but out of bound error during runtime

// using get method
match v.get(20) {
  Some(twenty) => println!("The twenty element is {}", twenty),
  None => println!(There is no twenty element)
}

// iterating over all elements read only
for i in &v {
  println!("{}", i)
}

// iterating over all elements
for i in &mut v {
  *i += 50;
}

For a vector to represent multiple types at the same time a enum can be used

enum SpreadsheetCell {
  Int(i32),
  Float(f64),
  Text(String),
}

let row = vec![
  SpreadsheetCell::Int(3),
  SpreadsheetCell::Text(String::from("blue")),
  SpreadsheetCell::Float(10.12),
];

match &row[1] {
  SpreadsheetCell::Int(i) => println!("{}", i),
  _ => println!("Not a integer!")
};

String

Are encoded at UTF-8 encoded bytes. Each character can have between 1-3 bytes.

// creating a string
let s1 = String::new();                    // String type
let s2 = "initial content";                // &str type
let s3 = s2.to_string();                   // String type
let s4 = String::from("initial contents"); // String type

// appending to a string
let mut s = String::from("foo");
s.push_str("bar");    // push string slice
s.push('!');          // push char

let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = format!("{}{}", s1, s2);

Indexing into a String

let namaste: String = String::from(“नमस्ते”);

// bytes representation
// [224, 164, 168, 224, 164, 174, 224, 164, 184, 224, 165, 141, 224, 164, 164, 224, 165, 135]
for b in “नमस्ते”.bytes() {
  println!("{}", b);
}

// char representation
// ['न', 'म', 'स', '्', 'त', 'े']
for c in “नमस्ते”.chars() {
  println!("{}", c);
}

// grapheme clusters
// ["न", "म", "स्", "ते"]
use unicode_segmentation::UnicodeSegmentation;

for g in “नमस्ते”.graphemes(true) {
  println!("{}", g);
}

HashMap's

Allows to store key-value pairs

use std::collections::HashMap

let blue = String::from("Blue");
let yellow = String::from("Yellow");

let mut scores = HashMap::new();

scores.insert(blue, 10); // ownership of Strings are moved into the HashMap
scores.insert(yellow, 50);

// accessing and element
let team_name = String::from("Blue");
let score = scores.get(&team_name);

// iterating over HashMap
for (key, value) in &scores {
  println!("{}: {}", key, value);
}

Updating the HashMap

let mut scores = HashMap::new();

scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 20);

// write an entry if None exist
scores.entry(String::from("Yellow")).or_insert(30);
scores.entry(String::from("Yellow")).or_insert(40);

Example word count

let text = "Hello world wonderful world";

let mut map = HashMap::new();

for word in text.split_whitespace() {
  let count = map.entry(word).or_insert(0);
  *count += 1;
}

println!("{:?}", map);

Operations

fn main() {
  // addition
  let sum = 5 + 10;

  // subtraction
  let difference = 95.5 - 4.3;

  // multiplication
  let product = 4 * 30;

  // division
  let quotient = 56.7 / 32.2;
  let truncated = -5 / 3;     // Results in -1

  // remainder
  let remainder = 43 % 5;
}

Type Convertions

Using as for type casting

The as keyword is used for basic type casting between primitive types. It's a simple way to convert, but it can be unsafe because it might truncate values if the destination type is smaller than the source type. For example, converting a u16 to a u8 will silently drop the upper 8 bits.

let a: u16 = 511;
let b: u8 = a as u8; // b is now 255, the lower 8 bits of 511 (0b11111111)

let f: f32 = 3.14;
let i: i32 = f as i32; // i is now 3, the fractional part is truncated

Traits From and Into

The From and Into traits provide a more robust and idiomatic way to handle type conversions. If you implement the From trait for your type, the Into trait is automatically implemented for the other type. This is used for infallible conversions.

From is used to define how to create your type from another type. Into is used to convert your type into another type.

// String implements From<&str>
let my_str = "hello";
let my_string = String::from(my_str);

// Thanks to Into, we can also do this:
let other_string: String = my_str.into();

// Example of implementing From
#[derive(Debug)]
struct MyNumber(i32);

impl From<i32> for MyNumber {
    fn from(item: i32) -> Self {
        MyNumber(item)
    }
}

let num = MyNumber::from(30);
let int_num: MyNumber = 30.into();
println!("MyNumber: {:?}", num); // MyNumber(30)
println!("MyNumber from into: {:?}", int_num); // MyNumber(30)

Traits TryFrom and TryInto

These traits are used for fallible conversions, where a conversion might fail. They return a Result type, which is Ok(value) on success and Err(error) on failure. This is safer than as because it forces you to handle the possibility of a conversion error.

use std::convert::TryFrom;

#[derive(Debug, PartialEq)]
struct EvenNumber(i32);

impl TryFrom<i32> for EvenNumber {
    type Error = ();

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        if value % 2 == 0 {
            Ok(EvenNumber(value))
        } else {
            Err(())
        }
    }
}

// Successful conversion
assert_eq!(EvenNumber::try_from(8), Ok(EvenNumber(8)));

// Failed conversion
assert_eq!(EvenNumber::try_from(5), Err(()));

Traits AsRef and AsMut

The AsRef and AsMut traits are used for cheap, reference-to-reference conversions. They are not about converting one type to another, but rather about getting a reference to a type from another type. This is often used in functions to accept a wider range of types (e.g., both String and &str) without taking ownership.

AsRef<T> provides a method .as_ref() to get a &T. AsMut<T> provides a method .as_mut() to get a &mut T.

fn print_str<S: AsRef<str>>(s: S) {
    println!("{}", s.as_ref());
}

let my_string = String::from("hello");
let my_str = "world";

print_str(&my_string); // prints "hello"
print_str(my_str);     // prints "world"

let mut s = String::from("foo");
let mut_ref: &mut str = s.as_mut();
mut_ref.make_ascii_uppercase();
println!("{}", s); // prints "FOO"