How it works...

The most important trait for conversion is From. Implementing it means defining how to obtain a type from another.

We present this on the example of DoubleVec[6]. Its concept is simple, when you construct it out of a Vec, it doubles all its elements. For this purpose, we implement From<Vec<T>> [11] with a where clause specifying T: MulAssign<i32>[13], which means that the trait will be implemented for all types that can be assigned to the result of a multiplication with an integer. Or, in terms of code, all types that allow the following, assuming the t variable is of the T type:

t *= 2;

The actual implementation should be self-explanatory, we simply multiply every element in the vector by two and wrap it in our DoubleVec[19]. Afterwards, we implement From as well for slices of the same type [25].

It is considered good practice to extend all of your trait definitions that work with vectors ( Vec<T> ) to also work with slices (&[T]). This way, you gain generality and performance, as you can operate on direct references to arrays (such as &[1, 2, 3]) and ranges of other vectors (vec[1..3]) without converting them first. This best practice carries over to functions as well, where you should always accept a slice of your type (as shown with print_elements()), if possible, for the same reasons.

In this implementation, however, we see something interesting:

fn from(slice: &[T]) -> Self {
slice.to_vec().into()
}

slice.to_vec() simply converts the slice into a vector. But what's up with .into()? Well, it comes from the Into trait, which is the opposite of the From trait, it converts a type into another. But how does a Vec know how to turn into a DoubleVec?

Let's take a look at the standard library's implementation of Into at https://github.com/rust-lang/rust/blob/master/src/libcore/convert.rs, where we find the following lines:

// From implies Into
impl
<T, U> Into<U> for T where U: From<T> { fn into(self) -> U { U::from(self) } }

Aha! According to this, every T type that implements From for U automatically lets U implement Into for T. And sure enough because we implemented From<Vec<T> for DoubleVec<T>, we automatically also implemented Into<DoubleVec<T>> for Vec<T>. Let's look at our code from before again:

fn from(slice: &[T]) -> Self {
slice.to_vec().into()
}

The slice gets turned into a Vec, which implements Into for DoubleVec, among many others. Because our function signature says that we return Self, Rust knows which Into implementation to use, as only one of them returns DoubleVec as well.

Another useful trait for type conversion is AsRef. Its only function, as_ref, is nearly identical to into, but instead of moving itself into another type, it takes a reference to itself and returns a reference to another type. In a way, it translates references. You can expect this operation to be cheap in most cases, as it typically just returns a reference to an internal object. In fact, you have already used this method in the last recipe:

let hello_world_name = get_name_attribute(ast).unwrap_or_else(|| identifier.as_ref());

identifier internally holds a String of its name. The compiler knows that hello_world_name has to be a &str, as the return type of get_name_attribute(ast) is Option<&str> and we are trying to unwrap it with a default value. Based on this information, as_ref() tries to return a &str, which it can, as the only implementation of AsRef for identifier that returns a &str is the one that returns a reference to the aforementioned String that holds its name.

We are only implementing AsRef for Vec, and not for a slice, because of a reference to a vector (&Vec<T>) with automatically deref-coerce into a slice  (&[T]), which means we automatically implement it. You can read more about the concept of deref coercion at https://doc.rust-lang.org/book/second-edition/ch15-02-deref.html#implicit-deref-coercions-with-functions-and-methods.

AsRef also has a brother called AsMut, which is identical but operates on mutable references. We intentionally didn't implement it in this example, as we don't want users messing with the internal state of DoubleVec. In general, you should be very conservative with this trait as well, as excessive access to the internals of anything can quickly become very chaotic.

The main function contains some examples of converting types. A popular example is the conversion from &str to String in lines [46 to 48]. Interestingly, &str can also be converted into a vector of its underlying bytes [52 and 53]. Let's look at how our DoubleVec can be converted in the same way.

The next line showcases how the &Vec<i32> returned by double_vec.as_ref() seamlessly behaves like an &[i32], as print_elements() only accepts slices [67]:

print_elements(double_vec.as_ref());

The last part of the recipe is about API design. There is a little implementation of From in the standard library that reads:

impl<T> From<T> for Option<T> {
fn from(val: T) -> Option<T> {
Some(val)
}
}

This means that every type can be converted into an Option. You can use this trick, as showcased in the implementation of ergonomic_public_func [100], to make functions with multiple parameters of the Option type easier to use and look at, as you can see by comparing the following two function calls [71 and 72]:

easy_public_func(Some(1337), Some(123), None);
ergonomic_public_func(1337, 123, None);

However, because some extra typing is required to achieve this, it's okay if you only do this on functions that are part of your API, that is, available to users of your crate. If you want to read some more tips about clean API design in Rust, check out Rust core developer Pascal Hertleif's excellent blog entry:  https://deterministic.space/elegant-apis-in-rust.html.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
13.58.51.36