y and z here have the types *const i32 and *mut i32, both kinds of raw pointers. In order to print y and z, we must de-reference them, which means that we need an unsafe block.

Incidentally, this code is safe. Raw pointers are allowed to alias. And we have no &T or &mut Ts here. So we’re good.

Why does this matter? Well, a lot of people think that as soon as you drop into unsafe, you’re missing the point of Rust, and that you lose all of its guarantees. It’s true that you have to do a lot more work when writing unsafe, since you don’t have the compiler helping you in certain situations, but that’s only for the unsafe constructs.

For example, let’s look at Rust’s standard library, and the LinkedList<T> that it contains. It looks like this:

I’m not gonna go into too much detail here, but it has a head pointer, a tail pointer, a length. NonNull<T> is like *mut T, but asserts that it will never be null. This means that we can combine it with Option<T>, and the option will use the null case for None:

This optimization is guaranteed due to guarantees on both Option<T> and NonNull<T>.

So now, we have a sort of hybrid construct: Option<T> is safe, and so we can do some operations entirely in safe code. Rust is now forcing us to handle the null checks, even though we have zero runtime overhead in representation.

I think a good example of how this plays out in practice is in the implementation of append, which takes two LinkedList<T>s and appends the contents of one to the end of the other:

pub fn append(&mut self, other: &mut Self) {
// do we have a tail?
match self.tail {
// If we don't, then we have no elements, and so appending a
// list to this one means that that list is now this list.
None => mem::swap(self, other),
// If we do have a tail...
Some(mut tail) => {
// the first thing we do is, check if the other list has
// a head. if it does...
if let Some(mut other_head) = other.head.take() {
// we set our tail to point at their head, and we
// set their head to point at our tail.
unsafe {
tail.as_mut().next = Some(other_head);
other_head.as_mut().prev = Some(tail);
}
// We set our tail to their tail
self.tail = other.tail.take();
// finally, we add their length to our length, and
// replace theirs with zero, because we took all
// their nodes!
self.len += mem::replace(&mut other.len, 0);
} // there's no else case for that if let; if they don't
// have a head, that list is empty and so there's
// nothing to do.
}
}
}

We only need unsafe when mutating the next of the tail of the first list, and the prev of the head of the second list. And that’s even only in the case where the tail exists in the first place. We can do things like “set our tail to their tail” in safe code, because we’re never de-referencing pointers. Safe Rust is helping us manage many, but not all, of our guarantees. We could move the unsafe block to encompass the entire body of this function, and Rust would still make us check those Option<T>s. It would still check that self and other are not aliased by &T or &mut Ts in the body of the function.

unsafe is a powerful tool, and should be used with care. But that doesn’t mean that you throw out many of the things that make Rust what it is.