Why you should prefer T::from(U) over U.into()
Disclaimer: This is my first article on Rust, and I’m still a beginner learning the nuances of the language. If you have any feedback, suggestions, or differing opinions, I would love to hear them. Your insights will help me learn and grow as a Rustacean!
When writing Rust code, choosing between using T::from(U)
and U.into()
for type conversions significantly impacts readability and code navigation. I want to propose here to prefer T::from(U)
over U.into()
as it can lead to better code readability, discoverability, and reduces the cognitive load required when reading code.
Example: T::from(U)
vs. U.into()
Consider the following example:
fn main() {
let user = User {
name: "Alice".to_owned(),
age: 30,
};
let s = String::from(user);
println!("{}", s);
}
struct User {
name: String,
age: u32,
}
impl From<User> for String {
fn from(user: User) -> String {
format!("{} ({})", user.name, user.age)
}
}
In this snippet, let s = String::from(user);
explicitly converts a User
into a String
. This makes the conversion type clear, but more importantly, it makes it easier for developers to navigate to the implementation of the From
function, which is crucial for understanding how the conversion is performed.
If we instead use user.into()
, the code would look like this:
fn main() {
let user = User {
name: "Alice".to_owned(),
age: 30,
};
let s: String = user.into();
println!("{}", s);
}
With user.into()
, navigating to the actual implementation of the From
function can be very difficult, as an IDE will often direct you to the generic Rust implementation of the into
method rather than the specific From
implementation.
Advantages of Using T::from(U)
To recap, the main benefits of using the from
function are:
- Code Navigation: IDEs can easily locate the
From
implementation, which helps in understanding how the conversion is performed. This is especially important when working with custom types. - Readability: Explicitly specifying the target type (
T::from(U)
) makes the code easier to read, improving comprehension in more complex or unfamiliar codebases. - Explicit Implementation:
String::from(user)
makes it clear which type the conversion is targeting, enhancing readability and reducing ambiguity.
Considering Alternatives: Explicit Into
Implementation and Custom Conversion Functions
Someone might ask: what about implementing the Into
trait explicitly when you need to use into()
, so that navigating from usage to definition is easier with any good IDE?
Let’s see:
fn main() {
let user = User {
name: "Alice".to_owned(),
age: 30,
};
let s: String = user.into();
println!("{}", s);
}
// Let's replace the From implementation with an Into implementation:
impl Into<String> for User {
fn into(self) -> String {
format!("{} ({})", self.name, self.age)
}
}
It works! You might think this is a clever workaround.
Wait, there’s a problem, and its name is Clippy:
warning: an implementation of `From` is preferred since it gives you `Into<_>` for free where the reverse isn't true
--> src/main.rs:15:1
|
15 | impl Into<String> for User {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
...
help: replace the `Into` implementation with `From<User>`
So, yes, you could do that, but you should be ready to ignore this Clippy rule.
Another approach — which is still reasonable — is to not rely on the From
/ Into
traits to implement your type conversion logic, but instead implement a custom function. Pros: easier navigation from usage to implementation and vice versa. Cons: it’s less idiomatic for Rust and may reduce consistency with community practices.
Conclusion
In conclusion, while both T::from(U)
and U.into()
are valid ways to handle type conversions in Rust, the choice between them can significantly impact the readability, maintainability, and discoverability of your code. Preferring T::from(U)
makes your code more explicit, enhances navigation, and aligns well with idiomatic Rust practices.
While alternatives like explicit Into
implementations or custom conversion functions can work, they come with trade-offs that can complicate code maintenance or diverge from community conventions. By adopting T::from(U)
where possible, you make your code easier to understand for others and yourself, ultimately leading to more robust and maintainable software.