Start learning free

LESSON 10 OF 414 · VARIABLES

Data Types in Rust: i32, f64, String, bool and char

Lesson Read the full explanation

TL ; DR:

In Rust, not all data is treated the same. There are different data types, such as:

  • i32 - represents integer numbers like 3 and -12

  • f64 - represents floating point numbers like 3.14

  • &str - represents a string (sequence of characters) like "John Doe"

  • bool - represents a boolean, data that can have one of two values: true or     false

  • char - represents a single character like 'a', 'Z' or '?'



Full lesson:

One instruction can have several distinct meanings depending on the data types it's working with.

For example, suppose you see this line of code somewhere in a program:

println!("{}", a + b);

Can you tell for sure what it does without knowing anything about the variables a and b?

It might add two numbers, if a and b are both numbers:

let a: i32 = 10;
let b: i32 = -5;

println!("{}", a + b); // Output: 5

But it also might concatenate two strings, if a and b are both strings:

let a = String::from("Hello ");
let b = "world!";

println!("{}", a + b); // Output: Hello world!

Of course, addition and string concatenation are two completely different operations and Rust needs to figure out which one to do.

And this is exactly why Rust assesses the data type of every value you use in your program. In order to be able to make these decisions when needed.

In our example, when it reads a + b, Rust checks the data type of a and b. If a is a String and b a &str, it performs string concatenation. If both are numbers, it performs addition.


Invalid operations:

Assessing data types is also helpful for Rust to make sure that what you do with the data actually makes sense.

For example, computers can perform mathematical operations on numbers, but not on strings. Of course, nobody can stop you from writing a program like this:

let a = "peanut";
let b = "butter";

println!("{}", a * b); // compile error

But when you try to compile this program, Rust will refuse with error[E0369]: cannot multiply `&str` by `&str`.

This is the way of Rust telling you: "Hey, I'm a little confused. How do you multiply two strings? I've never heard of that."


No null: Option, and String vs &str:

Rust has no null. A value that might be absent is an Option<T> — either Some(x) (a value) or None (nothing). Also note Rust's two string types: an owned, growable String versus a borrowed slice &str; and integers are sized, like i32.

let maybe_age: Option<i32> = Some(10);
let nothing: Option<i32> = None;

let owned: String = String::from("John");  // owned String
let borrowed: &str = "Doe";                // borrowed &str

Assignment
Follow the Coding Tutorial and let's practice with data types!


Hint
Look at the examples above if you get stuck.


STEP 1 OF 4
    Ctrl + Enter to check

    RUST · VARIABLES

    Data Types in Rust, explained

    What a data type is

    Every value in a Rust program has a data type that says what kind of data it is and what you may do with it. The compiler knows the type of every value before the program runs, and it uses that knowledge for two jobs: choosing what an operation means, and refusing operations that make no sense.

    These are the basic types you will use first:

    TypeHoldsExample values
    i32whole numbers3, -12
    f64numbers with a fractional part3.14, -0.5
    &strborrowed text"John Doe"
    Stringowned, growable textString::from("John")
    booltrue or falsetrue, false
    charone character'a', 'Z', '?'

    The same symbol, different meanings

    The expression a + b can mean different things depending on the types of a and b. With two numbers it adds them:

    let a: i32 = 10;
    let b: i32 = -5;
    
    println!("{}", a + b); // Output: 5
    

    With a String on the left and a &str on the right it joins text:

    let a = String::from("Hello ");
    let b = "world!";
    
    println!("{}", a + b); // Output: Hello world!
    

    Addition and concatenation are different operations, and the compiler picks the right one from the types. The choice is strict, though. Two &str values can't be added, and the pair of types has to be one the language defines.

    Invalid operations are compile errors

    Knowing the types also lets Rust catch nonsense before the program runs. Multiplying two strings is not defined:

    let a = "peanut";
    let b = "butter";
    
    println!("{}", a * b);
    

    The compiler rejects this with error[E0369]: cannot multiply `&str` by `&str`. Nothing is built, so there is no run-time failure to chase. The same happens when you mix number types. Adding an i32 to an f64 gives error[E0277]: cannot add `f64` to `i32`, because Rust never converts between number types silently. Convert one side yourself when you need to.

    Types the compiler checks in declarations

    A type annotation, written after the variable name, states the type you intend. If the value doesn't fit, you get mismatched types:

    • let b: bool = 1; fails with expected `bool`, found integer. Rust has no "truthy" numbers.
    • let c: char = "a"; fails with expected `char`, found `&str`. A char uses single quotes, text uses double quotes.
    • let s: String = "Hello"; fails as well: a literal is a &str, and String::from("Hello") is needed to make an owned String.

    Numbers: integer or float

    Integers and floats are separate types, so the same-looking operation behaves differently. Integer division discards the fractional part: 5 / 2 is 2. Float division keeps it: 5.0 / 2.0 is 2.5. A float with no fractional part, such as 7.0, prints as 7.

    The 32 in i32 is the number of bits it uses, which fixes its range from -2147483648 to 2147483647. Rust has other sizes too, such as i64, but i32 is the default for a plain integer literal and f64 for a plain decimal literal.

    Text: &str, String and char

    Rust has two main string types. A literal in double quotes is a &str: it borrows text that is fixed in the program and can't be changed or grown. A String owns its text, so it can be modified and passed around as its own value. You turn a literal into a String with String::from, or with .to_string().

    A char is a single character in single quotes. It isn't limited to ASCII, because it holds any Unicode character, and 'a' and "a" are different types: one is a char, the other a &str of length one.

    No null: Option

    Rust has no null. When a value might be missing, its type says so. Option<i32> is either Some(10), holding a value, or None:

    let maybe_age: Option<i32> = Some(10);
    let nothing: Option<i32> = None;
    println!("{:?} {:?}", maybe_age, nothing); // Output: Some(10) None
    

    An Option<i32> is not an i32, so maybe_age + 1 doesn't compile. You must handle the missing case before using the number. That is how the type system prevents a whole family of bugs.

    Solving the exercise

    You declare four variables, each with an explicit type and a matching value:

    1. my_int as i32, with a whole number such as 5.
    2. my_float as f64, with a decimal literal such as 3.14. A value like 3 would be an integer and be rejected.
    3. my_string as String, created with String::from("Hello") so the variable owns its text.
    4. my_bool as bool, with true or false.
    let my_int: i32 = 5;
    let my_float: f64 = 3.14;
    let my_string: String = String::from("Hello");
    let my_bool: bool = true;
    

    Printing them with println!("{} {} {} {}", my_int, my_float, my_string, my_bool) gives 5 3.14 Hello true.

    Key takeaways

    • Every value has a type, and Rust checks types when it compiles, before the program runs.
    • The core types are i32, f64, &str and String, bool and char.
    • Integers and floats are different types and don't mix without a conversion.
    • Text in double quotes is a &str, a single character in single quotes is a char, and String::from makes an owned String.
    • A value that may be absent is an Option, not null.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free