When is it okay to cast types with `as`?
6 min read July 11, 2024 updated: July 14, 2024 #rust #programmingUpdate: Added a section about using as
to cast to wider types.
swan
asked me this after reading an earlier version of
"Surprises with Rust's as
". I thought that post had already gotten long,
so I decided to break this part out. I recommend reading that post first if you want the full context.
This is a complicated question. I don't think there are absolute answers
in software engineering — and we should probably beware people who
try to give them.
Almost every engineering decision requires trade-offs, and this is 100% the case with
using TryFrom
instead of as
. TryFrom
gives us more certainty that this
particular part of our codebase won't cause us surprises, but we trade away simplicity and convenience.
For me, that trade-off is worth it. When my code — or any code I have to work with — encounters something
unexpected that it can't handle correctly, I prefer it to fail as fast as possible
and give me as much relevant context as possible,1 instead of chugging on and
doing strange things (and making me go on a wild chase to find the cause). So, I am clearly biased against as
.
With that said, casting types with as
is probably okay if:
- You are building a toy project or a prototype just to learn or figure something out
- And you just need to get it working and don't care about handling errors
- And you're 100% sure that this will always be a toy project or a prototype where you won't care about handling errors (I've made the wrong assumption with this before)
- And if you're not 100% sure, you leave notes/issues/tickets/
// TODO
comments to replaceas
withTryFrom
for when you start to care about handling errors, and pinky promise to actually do that - Or you're an experienced Rust programmer doing some low-level type juggling, and
you have considered all the possible ranges and types of values your code would ever deal with,
and determined that
as
is the best option, in which case you probably don't need my advice on this (but I would welcome your feedback!)
So, in short, something like this:
// This should be okay because we're only ever dealing with ASCII-inputs.
// TODO: Use `TryFrom`/add error handling if we ever have to accept a wider range of inputs!
let byte = some_char as u8;
Otherwise, if in doubt, I don't think it would be a bad idea to use TryFrom
instead.
I appreciate swan
's feedback that led to this addendum, and hope that it helps!
§Using as
to widen types
As Ryan Goldstein helpfully pointed out in response to an earlier version of this post,
casting from a smaller type to a larger one (for example, u8
to u16
, or f32
to f64
) will always produce
expected results:
[src/main.rs:13:5] result = 16
However, I would still avoid using as
here.
- If the type of
number
were to change to, for example,u32
, and its value was no longer guaranteed to fit intou16
(like, if we gotnumber
from some struct or function that someone updated without us realizing), we would once again risk getting results we might not expect - For type conversions that guarantee to be lossless and preserve the original value, we can use
From
/Into
Compared to the above example, the latter would both give us arguably better ergonomics (we can let Rust infer the type we want to convert to, instead of having to specify it manually):
// --snip--
and, importantly, if the type of number
were to change from under us, we would get a compiler error :
// --snip--
error[E0277]: the trait bound `u16: From<u32>` is not satisfied
--> src/main.rs:12:40
|
12 | let result = get_half_floor(number.into());
| ^^^^ the trait `From<u32>` is not implemented for `u16`, which is required by `u32: Into<_>`
|
= help: the following other types implement trait `From<T>`:
<u16 as From<Char>>
<u16 as From<bool>>
<u16 as From<u8>>
= note: required for `u32` to implement `Into<u16>`
For more information about this error, try `rustc --explain E0277`.
I don't think that error is very helpful in itself,2 but it would force us to investigate what is going on
and update our code, instead of running into potentially surprising and unwanted behavior at runtime,
which we might get if we stuck with as
.3
§Footnotes
What "fail" means will depend on the context. It could be just "crash with a clear error message", or, more likely in a complex system, log a clear error message and bail out of whatever procedure we attempted to do with that unexpected input.
When I get the the trait bound ... is not satisfied
errors, Rust telling me what other types implement
the trait usually doesn't help me much, because I probably can't just use one of those types instead.
In my experience, getting that error almost always means I made some mistake in the logic, and
used something in the wrong place or forgot a step. As a newcomer, I found the
help: the following other types implement trait
especially confusing (and not helpful), because not only
did I get a compiler error, that error also came with a lot of information that I had to mentally process,
and which rarely led me to the solution I needed. But there are probably situations I'm not thinking of
where knowing what other types implement a trait you're trying to use might get you closer to what you want.
If you are reading this post and have one in mind, please send me a note, so I can update this post with an example!
And, because lossless conversions for primitive types
internally use as
and are almost always inlined,
you most likely won't pay any performance penalty for using them —
and
if you know they are affecting your performance,
you probably already know why you need to use as
directly.