The Rust language doesn’t have values like NULL, None, null, nil, values that represent “nothing”, so how does Rust represent and handle “nothing”?

When comparing objects, you need to check whether the two exist. Take this following Python code as an example

def inorder(p, q):
    if p is None and q is None: return true
    if p is None or q is None: return False
  
  	if not inorder(p.left, q.left): return False
    if p.val != q.val: return False
    if not inorder(p.right, q.right): return False

This is an algorithm for checking whether two binary trees are the same. In order to execute the last three lines of code, it is necessary to check whetherp and q are “nothing”. Rust gives a slightly different point of view on this.

Rust’s Answer Sheet

In Rust, there is an enum class called Option<T>. It is defined as follows

enum Option<T> {
  Some(T),
  None,
}

Developers who are familiar with generics also familiar with the token T here. For those who don’t know generics yet, T is the abbreviation of Template, which can be replaced by any type. Option<T> has two members, Some(T) and None. Some(T) means “something” exists, None means “nothing” exists. Through the following explanation, you will be more clear about the role of T.

Some means “something” is there.

let some_number = Some(5);
let some_char = Some('e');

None means “nothing” is there. In addition, “Nothing” is relative to “something”, for example, when we say

No one is immortal in the world.

It is relative to the world where people live a immortal life. So when defining variables, we need to specify the type of that to indicate what “nothing” is relative to.

let some_number: Option<i23> = None;
let some_char: Option<char> = None;

When we want to compare two objects, we can use Some<T> for comparison, for example

let x = Some(4);
let y = Some(6);
let z: Option<i32> = None;

println!("{:?}", x == y); // false
println!("{:?}", x < z);  // false

In the above code T is i32. We mentioned earlier that T can refer to any type in general, The type of T in Some(4) can be inferred at compile time . This code shows that not only can Some<T> be compared to others, but also can be compared with None.

However, if we want to compare Some<T> with T, a compilation error will occur.

let x = Some(4);
let y = 4;
println!("{:?}", x == y); // error[E0277]: `i32` cannot be compared to `Option<i32>`

This is because Some<T> and T are two different types and cannot be compared. If you want to compare them, you need to convert T to Some<T>.

let x = Some(4);
let y = 4;
println!("{:?}", x == Some(y)); // true

You may ask, why can’t we compare None with T directly just like we can in Python. This is because Rust focuses on safety and does not allow such implicit conversions.

Let’s imagine a world that allows so.

let x = 4;
// ...some other code
if x == None {
  // do something
}

In the above example, x is an integer, but it can be compared with None. This is unsafe because None is a special value that means “nothing”. What is this “nothing” relative to? If x is an integer, then None is integer-relative, but what if x is a string?We can’t ensure the type of x, it’d be terrible to allow None compare with so many different types. With this in mind, Rust doesn’t allow such implicit conversions.

Let’s look at an example to help you understand.

Now we want to make a game that allows us to control the forward, backward, left and right movement of the character in the game through the w, s, a, d keys. we don’t want to accept any input other then that.

Some part of the code might look like this:

//...
enum Movement {
    Up,
    Down,
    Left,
    Right,
}

fn get_movement() -> Option<Movement> {
    let mut input = String::new();
    io::stdin().read_line(&mut input).unwrap();
    match input.trim() {
        "w" => Some(Movement::Up),
        "s" => Some(Movement::Down),
        "a" => Some(Movement::Left),
        "d" => Some(Movement::Right),
        _ => None,
    }
}
// ...

We have Movement an enum with elements corresponding to four keys and a get_movementfunction.

When we call get_movement, we return what was read from the keyboard. Content is always uncertain. In addition to the four keys, other inputs are also possible, such as j, k etc., so our return type is Option<Movement>. This means that what is returned is either a value of Some(Movement) or None.

This gives us the advantage that the place where this function is called always gets “something” or “nothing”, and in the case of “something”, it always gets Some(Movement) instead of Some(char) or Some(i32). Now, if somewhere in the code, I want to record the wrong operation of the user, I can write like this

let mv = get_movement();
if let None = mv {
    // log invalid movement
}

Which is considered safe.

Summary

A direct effect to Rust by doing this is that operations are divided into two kinds, one is state-determined, in which there is only “something”.

In that scenario, “nothing” does not exists, there are always a certain type of object.

The other is where “something” or “nothing” exists. The objects we compared is not certain, we don’t know if we are comparing “something” of the same type, or comparing “something” with “nothing”, or “nothing” of different type, etc,.

Such isolation is the key to Rust’s safety.