fn test(x: f64) {
println!("{}", x);
}
fn main() {
let i = 1;
test(i as f64);
test(i.into());
}
I think in the official Rust book the From and Into traits are not even mentioned, at least I can not remember having read about it there, and the mdbook search function gives no results.
So I used always "as" for type conversion of primitive types. But in other people's source code examples I have seen a lot of into() usage. And as we see in the example above, into() seems to be more convenient, as we do not have to care about the actual destination type, just hoping that the compiler will somehow make the data type fit. So the actual question is:
Is the use of into() for primitive types instead of conversion with "as" now the recommended way? And is into() guaranteed to be always as efficient as use of "as"?
into has an advantage that you don't need to know the final type before hand. For example, if you changed type of x to u64, with into you only need to change it in one place, with as you need to update it at every function call as well.
I think you can also use test(i as _) which will convert to correct type, but only works for primitives.
That was exactly what I tried to express with "more convenient, as we do not have to care about the actual destination type, just hoping that the compiler will somehow make the data type fit."
Still conversion with "as" is more "straight forward" and transparent, which might be more the philosophy of Rust. And I still wonder if it is guaranteed that each into() call is always inlined and fully optimized.
EDIT
use test(i as _)
That is interesting, I never saw that syntax before. And it works in Playground!
.into() only works if the conversion is infallible and preserves the value of the number (i.e. for converting to a bigger number type or converting an integer to a float). It has a dual, .try_into(), which goes the other way and returns a Result, indicating that the conversion may fail.
On the other hand, as will silently convert the number even if it has to truncate the number to fit the new type, and as such it may cause surprising behavior.
For this reason, .into() is often preferred (or even a f64::from(i) for explicitness) over as, unless truncating is intentional. In more and more cases, explicit methods are added to replace as casts with the intended behavior.
Yes. into() has much more restricted semantics, hence it is better. Use that when it is sufficient.
Hopefully we will eventually have the other kinds of casts performed by as written as standard explicit methods as well (such as .wrapping_into()) and as can be deprecated.
Nothing about performance is guaranteed, but generally yes, it is equally efficient.
Thinking about it, the use of into() seems to be against the philosophy of Rust.
Rust is generally explicit: Mixed data types are generally not allowed in arithmetic expressions, "1.0 + 2" does not compile. "x += 1" does not compile when x is a float. A function call like my_func(1) does not compile when my_func() expects a float argument. But then my_func(1.into()) compiles. So, when we now have a less strict and less explicit language, why can the compiler not always add an into() call when ever an argument does not match, so that we can just write my_func(1)?
The conversion is obvious: it’s the only reasonable conversion between the two types. Otherwise it’s better to have it be a named method or constructor, like how str::as_bytes is a method and how integers have methods like u32::from_ne_bytes, u32::from_le_bytes, and u32::from_be_bytes, none of which are From implementations. Whereas there’s only one reasonable way to wrap an Ipv6Addr into an IpAddr, thus IpAddr: From<Ipv6Addr> exists.
OK. But that explains not why we have to type .into() at all. For my_func(x.into()) we see not from the code which conversion is actually performed, or if no conversion is performed. So why can the compiler not just do these obvious conversions on its own then, as in many other languages. When I started with Rust, I thought: Well, the compiler makes no type conversions on its own. So we have to do explicit conversions with "as". That is more work for us programmers to type that, but the advantage is that we see what is going on, and we even see when there is a minimal performance loss e.g. due to a int to float conversion. With into(), that advantage is gone, So why then the compiler can not add that into() internally, so that we can save us the typing, and get cleaner source code?
Because of the goal to be explicit, as you said. The xxx.into() at least tells the reader that a conversion is made, and to look for the From trait impl to find out more. It is (sort of) similar to requiring as for integer widening.
It is always a trade-off. Rust does do some "conversions" automatically because it would be so awkward without them. I'm thinking of adding a & to an owned var when calling a method that takes &self. There are probably a couple more I'm not thinking of. But very few.
You can (usually) write a variable type or use from() instead of into().
let y: f64 = x.into();
let y = f64::from(x);
Note also that none of these, nor even as, specify the input type. The problem with as is not the explicitness; it’s that as does many kinds of conversions, whereas From/Into is expected to provide non-lossy conversions only.
If the compiler inserted a possible into at every location, type inference would be completely impossible. (Kind of like how if you write .into().into() you’ll always get a type inference failure since the type in the middle is unknown.) Rust’s principles aren’t about “everything explicit all the time”; Rust generally lets you choose how explicit your code is, but with a minimum level of “something is happening here”, so you can’t have a totally implicit conversion.
The type inference isn't specific to into(), types are often inferred automatically in Rust.
The into() call also indicates that there is some conversion happening, so the advantage is not gone, the conversion call is still there in the code indicating a possible minimal performance loss.
as is basically a 1:1 equivalent to C casts, so they're mainly useful for translating C code or idioms into Rust (for example, you might see as *mut () scattered around an ffi boundary that heavily uses void pointers)
I know it's a pain in the butt to specify these conversions all the time. But you know what's way more painfull? Debugging code that has implicit conversions like C or C++. The ability to always see where there is a conversion is worth a lot. And then you can start to make the .into() calls more specific by annotating types or changing to from().
In C++ you have to start to make evey possible implicit conversion explicit if you want to find conversion based errors.
I tried to summarize the discussion in Type conversions - Rust for C-Programmers for now. I think my understanding should be mostly correct now -- if you think I am totally wrong, you can create an issue at GitHub. I will then fix it when possible. Of course I will continue verifying that text myself, and fix it if I should find contradicting information.
But there is a good reason for this. The size of usize depends on the system. There are systems that have a usize of size 16 bit. Because into() needs to be infallible and lossless there cannot be an Into<usize> for u32 implementation. So it makes sense to choose between 0_u32 as usize which is potentially lossy and 0_u32.try_into() where you have to think about what you want to do in the error case.