Outline#
- I pretend there is something here, but there isn't.
- PhantomData
- This is empty now.
- Null
- Option::None
- There will never be anything here.
- The empty tuple
- I will leave you waiting here, empty-handed, until the end of time.
- The never type
This is a short collection of ways to express "nothing" in the Rust language.
In programming, the concept of "nothing" has several different interpretations:
- "I pretend there is something here, but there isn't."
- "This is empty now."
- "There will never be anything here."
- "I will leave you waiting here, empty-handed, until the end of time."
Although this sounds like the last thing my ex said, I'm fine.
I pretend there is something here, but there isn't. ("I'm pretending like there's something here, but there actually isn't.")#
PhantomData#
The Rust standard library has many examples of high-quality code, but few are as pure and elegant as std::marker::PhantomData
. (Its implementation is as elegant and pure as std::mem::drop
.)
PhantomData<T>
is a zero-sized type, regardless of what T is. It's like telling the compiler a little lie: you claim to hold a T
, but you actually don't. Unlike some lies, this one actually benefits the code.
In practice, I've seen it used primarily in two cases:
- Holding a lifetime specifier to restrict the lifetimes of the structs it's contained in. This can be useful for artificially attaching lifetimes to raw pointers.
struct PointerWithLifetime<'a, T> {
pointer: *const T,
_marker: std::marker::PhantomData<&'a ()>,
}
- Simulating ownership of a value of type T when the actual value is held (or managed) by another system. You might encounter this when working with non-traditional storage models or interacting with FFI.
mod external {
pub fn get(location: u64) -> Vec<u8> { /* ... */ }
}
struct Slot<T> {
location: u64,
_marker: std::marker::PhantomData<T>,
}
impl<T: From<Vec<u8>>> Slot<T> {
fn get(&self) -> T {
T::from(external::get(self.location))
}
}
This is empty now. ("There is nothing here now.")#
Null#
There's no null in Rust.
You've been deceived, maybe even controlled. I get it. "Oh, Null has no issues."
In safe Rust, that's correct.
However, sometimes you need to peel off the band-aid and see what's happening beneath the surface.
let n: *const i32 = std::ptr::null();
unsafe {
println!("{}", *n); // Segmentation fault
}
(Reminder: Dereferencing raw pointers can only be done in unsafe blocks.)
Rust's design keeps you from needing to delve into pointer operations very often. You might encounter raw pointers (*const and *mut types) when interacting with C code or when rewriting Quake III in Rust.
Option::None#
The standard library provides the Option
enum type, which has two variants: Some
and None
. This is the recommended way to represent a value that may or may not exist, instead of using null pointers. It's like a small, safe wrapper, and you should use it unless you know what you're doing and are prepared for the consequences, or you're working in isolation.
However, there are significant differences between using null pointers and using None
. First, Option<T>
is an owned type, while raw pointers are just pointers to some space in memory. This means that, in addition to the unsafe operations and all the other considerations you must be careful with when using raw pointers, the size of None
can vary to accommodate whatever it surrounds. It's just a variant of the Option<T>
enum type, and if T
is Sized
, any Option<T>
value is at least as large as T
, including None
. On the other hand, *const T
(when T: Sized
) is always the same size as usize
.
Type | Size
*const T
| 8 (platform-dependent)
Option<&T>
|8 (platform-dependent)
Option<std::num::NonZeroU8>
|1
Option<u8>
| 2
Option<std::num::NonZeroU32>
| 4
Option<u32>
| 8
Option<std::num::NonZeroU128>
| 16
Option<u128>
| 24
There will never be anything here. ("There will never be anything here.")#
The empty tuple#
The empty tuple is written as empty parentheses ()
.
I used to write Java code. It wasn't perfect, but at least it had style. In Java, a method with a void return type doesn't return anything, no matter what or how much you give it.
The empty tuple serves a similar purpose in Rust: functions that don't return an actual value implicitly return the empty tuple. But its uses go beyond that.
Since the empty tuple is a value (albeit a value with no content and zero size), it's also a type. So sometimes it's useful to use it to parameterize a Result type to indicate a function that can fail without providing meaningful feedback.
impl Partner {
fn process_request(&mut self, proposition: Proposition) -> Result<(), (u32, RejectionReason)> {
use std::time::{SystemTime, Duration};
use chrono::prelude::*;
self.last_request = SystemTime::now();
if SystemTime::now().duration_since(self.last_request).unwrap() < Duration::from_secs(60 * 60 * 24 * 7) {
Err((429, RejectionReason::TooManyRequests))
} else if proposition.deposit < self.minimum_required_deposit {
Err((402, RejectionReason::PaymentRequired))
} else if SystemTime::now().duration_since(self.created_at).unwrap() < Duration::from_secs(60 * 60 * 24 * 366 * 18) {
Err((451, RejectionReason::UnavailableForLegalReasons))
} else if Local::now().hours() < 19 {
Err((425, RejectionReason::TooEarly))
} else if Local::now().hours() > 20 {
Err((503, RejectionReason::ServiceUnavailable))
} else if proposition.len() >= 6 {
Err((413, RejectionReason::ContentTooLarge))
} else if !proposition.flushed() {
Err((409, RejectionReason::Conflict))
} else if !matches!(proposition.origin_address, Location::Permanent(..)) {
Err((417, RejectionReason::ExpectationFailed))
} else {
Ok(())
}
}
}
I'm going to leave you, waiting here, empty-handed, until the end of time. ("I'm going to leave you, waiting here, empty-handed, until the end of time.")#
The never type#
How do you call a function whose return type not only doesn't return a value but will never return at all? Well, you can try all the traditional methods, but you'll never be able to continue beyond that point, so you need some fine handling.
That's where the so-called "never" type comes in. Here are a few ways you might encounter it:
let never_loop = loop {}; // loop never exits
let never_panic = panic!(); // panic terminates execution
let value: u32 = match Some(1) {
Some(x) => x,
None => return, // `return` is of type never
};
While the syntax is still experimental, the "never" type is represented by an exclamation mark !. In the meantime, you can use Infallible as an alternative.
The "never" type can be useful when implementing a trait with associated types that you never need. Again, using Result as an example:
trait FailureLogProvider {
type Error;
fn get_failure_logs(&self) -> Result<Vec<FailureLog>, Self::Error>;
}
impl FailureLogProvider for Partner {
type Error = !;
fn get_failure_logs(&self) -> Result<Vec<FailureLog>, Self::Error> {
Ok(self.failure_log)
}
}
In the example, the function implementation always succeeds, but the trait allows for failure. To represent this, the associated Error type is the "never" type.