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" can be interpreted in several different ways:
- "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'm pretending like there's something here, but there actually isn't. (“我假装这里有东西,但实际上没有。”)#
PhantomData#
The Rust standard library has many high-quality pieces of 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 is actually beneficial to the code.
In practice, I've seen it used primarily in two cases:
- Holding a lifetime specifier to restrict the lifetime of the containing struct. This can be useful for artificially attaching a lifetime 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))
}
}
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, rather than using a null pointer. 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 a null pointer and using None
. First, Option<T>
is an owned type, while a raw pointer is just a pointer 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
. Whereas *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. (“这里永远不会有东西。”)#
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 a size of zero), it is also a type. Therefore, sometimes it can be 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. (“我会让你一直等到时间的尽头,空手而归。”)#
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 after that point, so you need some finesse.
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.