Implicit type conversions in Rust
Translator: iamazy
Original: www.possiblerust.com/guide/what-…
Rust supports multiple type conversions, implicitly converting one type to another. Like other languages that support type conversion, Rust makes a trade-off between legibility and writability. While there is disagreement as to whether Rust’s list of supported type conversions is the best, it makes sense to learn about type conversions because some are at the heart of idiomatic Rust code. In this article, I describe what type conversions Rust supports and where they can be applied.
Note: All type conversions described in this article are implicit casts and will be referred to simply as strong casts.
What is strong
Before discussing type coercion, it’s a good idea to understand what it means. Rust supports multiple types of conversion. The From and Into features are used for infallible transformations at the library level. TryFrom and TryInto are used to handle error-prone type conversions. AsRef, AsMut, Borrow, and ToOwned provide more library-level conversions between different types. However, these are all explicit. To perform the transformation, the user must call the relevant function. By contrast, strong conversions are implicit, and the implicit nature of these conversions means that they are only available if their benefits depend on ease of use, and the potential harm from implicit type changes is minimal. Conversions done using the AS keyword are explicit and allow more types of explicit strong (cast) than implicit coercion.
INFO 1, Transmute – Unsafe Conversion The standard library has a function, STD ::mem:: Transmute, that can convert any type to another. The function is unsafe because it does not guarantee that a significant bit of an input type can be represented as a significant bit of an output type. It is up to the user to ensure that the two types are compatible.
There is an effort to develop a “Safe Transmute” option in Rust, which can be called “Project Safe Transmute.” Their work is ongoing to eliminate the need to use the unsafe version of Transmute when the transformation in question is legal (meaning that the significant bit of the source type is always the significant bit of the target type).
What types of coercion are there?
Rust supports a variety of implicit type strongholds that, although they are unofficially defined, still require some degree of standardization. In fact, the long-term specification of these transformations is expected to be part of the eventual standardization process, as they are critical to understanding Rust’s type system.
INFO 2, The criticism that Rust is less trustworthy than C/C++ due to the lack of a specification regularly comes up, and I’ll explain here: First, it’s true that Rust has no specification like C/C++ (published and managed by the International Standards Organization), but that doesn’t mean Rust has no standards at all. Rust has a reference that codifies most of the expected semantics of the language. It also has RFC processes to manage language change and teams to oversee the evolution of languages. These groups include the Unsafe Code Guidelines Working Group, which seeks to better specify the semantics, requirements, and guarantees that affect Unsafe Rust Code. The team developed mirI, an interpreter for the MIR (Mid-Level Internal Representation) language in Rust, It automatically verifies that the MIR code is consistent with the “STACKED Borrows” model in Rust semantics (proposed by UCG WG). The main Rust compiler is also thoroughly tested, including experimental feature changes and automated regression testing of new compiler versions. There is an alternative implementation of RUSTC available – MRUSTC, although it is not typically used by end users. There is also an updated effort to implement a collection of RUst-enabled GNU compilers called rust-GCC. Ferrocene has been working to obtain Rust certification for use in key safety areas, including avionic and automation industries. It is maintained by Ferrous Systems, a Rust consulting firm, whose team includes major language and community contributors. Finally, the challenge of formally specifying and the assurance of proving Rust has been addressed in academia, with multiple projects building models, including Patina, Oxide, RustBelt, KRust, and K-rust. These work are studied and extended by Alexa White in her master’s thesis Towards a Complete Formal Semantics of Rust, which is a good entry point to understand these different scientific work. All of this, while not a standard, raises the bar for Rust to achieve the capabilities it guarantees. There are reliability vulnerabilities in the master Rust compiler that are tracked and resolved over time. As described in RFC 1122, the Rust stability policy makes an exception for disruptive changes to fix reliability vulnerabilities. It is also worth noting that C was introduced in 1972, while the first official non-draft version of the C language standard appeared in 1989 (ANSI X3.159-1989 “Programming Langua-C,” has since been withdrawn). C++ was introduced in 1985, and the first non-draft version of its standard was published in 1998 (ISO/IEC 14882:1998 “programming languages — C++”). The first public version of Rust was released in 2010, with a 1.0 release on May 15, 2015 following major changes to the earlier version of the language. Six years have passed since 1.0 was released. Standardization takes time, and patience is a virtue.
Reference downgrade strong turn
Citation Degraded strong is a very common strong operation that converts &mut T strong to &t. Obviously, such strong conversions are always safe because immutable references are more restricted. It also allows the borrowing inspector to accept code that you might not think will compile or work properly.
An example of a reference to degraded strong turns looks like this:
struct RefHolder<'a> {
x: &'a i64,}impl<'a> RefHolder<'a> {
fn new(x: &'a i64) -> RefHolder<'a> {
RefHolder { x }
}
}
fn print_num(y: &i64) {
println!("y: {}", y);
}
fn main() {
// Create `x`
let mut x = 10;
// Make sure `y` is `&mut i64`.
let y = &mut x;
// Package the downgraded reference into a struct.
let z = RefHolder::new(y);
// Print `y` downgrading it to an `&i64`.
print_num(y);
// Use the `z` reference again.
println!("z.x: {}", z.x);
}
Copy the code
In this example, we can see that the print_num function only needs &i64, but it passes in &mut i64. It works because the reference degradation is strongly converted to an immutable reference. This also solves the problem of aliasing mutable borrowings. The same happens with the RefHolder type constructor.
Note the number of times this strong turn occurs. Here is a similar example of a failure to compile.
struct RefHolder<'a> {
x: &'a i64,}impl<'a> RefHolder<'a> {
fn new(x: &'a i64) -> RefHolder<'a> {
RefHolder { x }
}
}
fn print_num(y: &i64) {
println!("y: {}", y);
}
fn main() {
// Create `x`
let mut x = 10;
// Make sure `y` is `&mut i64`.
let y = &mut x;
// Package the downgraded reference into a struct.
//
//---------------------------------------------------
// NOTE: this is a _fresh_ reference now, instead of
// being `y`.
//---------------------------------------------------
let z = RefHolder::new(&mut x);
// Print `y` and update it, downgrading it
// to `&i64`.
print_num(y);
// Use the `z` reference again.
println!("z.x: {}", z.x);
}
Copy the code
In this case, even though the reference is degraded in the function signature, the borrowing inspector still observes that two mutable references are created in the same scope (for the same memory), which is not allowed.
Dereference strong transfer
The next strong shift is the cornerstone of Rust ergonomics. A “dereference strong” is a strong turn produced by the implementation of two characteristics: Deref and DerefMut. The explicit purpose of these is to opt-in such strongholds so that containers can transparently use the types they contain (these containers are often referred to as “smart Pointers”).
Such features are defined as follows:
pub trait Deref {
type Target:?Sized;
pub fn deref(&self) -> &Self::Target;
}
pub trait DerefMut: Deref {
pub fn deref_mut(&mut self) - > &mut Self::Target;
}
Copy the code
The first feature, Deref, defines a type that provides references to other “target” types. The target is an association type, not a type parameter, because each “smart pointer” should be dereferenced to only one type. If it is defined as Deref
, then any type can provide as many implementations as possible because they can provide internal types, and then the compiler selects the correct internal type according to some mechanism. The key to dereference strongholds is that they are implicit, so unambiguous type annotations often conflict with the benefits of dereference strongholds.
The DerefMut feature requires a Deref as a supertype, both to give it access to the Target association type and to ensure that the Target types of DerefMut and DerefMut are always the same. Otherwise, you might enable strong casts for one type in a mutable context and another type in an immutable context. This level of flexibility adds more complexity to dereference strongholds, but there is no obvious benefit, so it is not available.
The methods deref and deref_mut required by these characteristics are implicitly called when a method is called on a type that implements these characteristics. For example, Deref
is implemented on Box
so that methods of its containing type can be called transparently, making Box
more ergonomic than having the user explicitly access its content for each operation.
The raw pointer is strong
A bare pointer to Rust may be strong from *mut T to *const T. Although using these Pointers by dereference is unsafe and subject to Rust’s safety requirements for Pointers (that is, access never hangs or is unaligned), these conversions are part of Safe Rust (that is, not reserved for use in an unsafe context).
The following is an example of the strong conversion of a bare pointer:
#[derive(Debug)]
struct PtrHandle {
ptr: *const i32,}fn main() {
let mut x = 5;
let ptr = &mut x as *mut i32;
// The coercion happens on this line, where
// a `*mut i32` is set as the value for a field
// with type `*const i32`, coercing to that type.
let handle = PtrHandle { ptr };
println!("{:? }", handle);
}
Copy the code
INFO 3 Pointer conversion security Rust also allows *const T to be converted to *mut T via AS. While it may seem surprising that *const T is allowed to be converted to *mut T, sometimes this conversion is necessary. For example, FFI code might create a *mut T from Box::into_raw, but only provide a *const T for C users of the API. So the equivalent delete function provided by the FFI interface needs to take *const T as an argument, convert it back to *mut T to pass it to Box:: from_RAW, so that Rust releases Box at the end of the function. Although the details of provenance mean that this conversion is not always an undefined behavior, it may be an undefined behavior if the original provenance of a pointer is not mutable. In other words, if a value is originally of type *mut T, it can be used as *mut T in the future, even if the type is converted to *const T in the meantime (interim).
References and naked pointer strong
You can convert &t to *const T and &mut to *mut T. Although the resulting bare Pointers can only be dereference in an unsafe code block, these strong conversions are safe.
Similar to the previous example, but this time the reference is converted to a pointer rather than changing the variability of the pointer type.
// Notice that these coercions work when
// generic types are present too.
#[derive(Debug)]
struct ConstHandle<T> {
ptr: *const T,
}
#[derive(Debug)]
struct MutHandle<T> {
ptr: *mut T,
}
fn main() {
let mut x = 5;
let c_handle = ConstHandle {
// Coercing `&i32` into `*const i32`
ptr: &x,
};
let m_handle = MutHandle {
// Coercing `&mut x` into `*mut i32`
ptr: &mut x,
};
println!("{:? }", c_handle);
println!("{:? }", m_handle);
}
Copy the code
Strong rotation of function pointer
A closure is a function plus its execution context. This makes them very useful in many situations, but sometimes the superfluous state they carry impeded their use, especially when there is no actual state capture. In Rust, in addition to the nameless closure types generated at compile time, there are function pointer types that represent functions without context. To make closures as flexible as possible, closures force Pointers if and only if they do not capture any variables from the context.
An example of a function pointer is as follows:
// This function takes in a function pointer, _not_ a generic type
// which implements one of the function traits (`Fn`, `FnMut`, or
// `FnOnce`).
fn takes_func_ptr(f: fn(i32) - >i32) - >i32 {
f(5)}fn main() {
let my_func = |n| n + 2;
// The coercion happens here, and is possible because `my_func`
// doesn't capture any variables from its environment.
println!("{}", takes_func_ptr(my_func));
}
Copy the code
Note that the use of generics to implement Fn, FnMut, and FnOnce features in Rust is much more common than the use of function Pointers. If you want to pass or store closures captured from context, you need to use one of these three characteristics.
Strong conversion of subtype
To the surprise of some, Rust supports strong subtypes. While Rust’s type system is often thought of as supporting only parametric polymorphism, it actually supports subtype polymorphism as well, applicable to survival. When one duration is longer than another, the durations in Rust form subtype relationships with each other. In this case, long durations are subtypes, and short durations are supertypes. Because any subtype can replace the supertype in subtype polymorphism, this for survival means that longer lifespans can be safely used when the expected lifespans are shorter.
This strong turn means that the lifetime is allowed to “shorten” at the strong turn point, so longer lifetimes can be used instead of the shorter bounds required by the function. The net result for Rustacean is that the compiler can accept more programs.
A common problem that arises in languages like Rust that support parameter and subtype polymorphism is how the subtype relationships of a generic type relate to the subtype relationships of its generic parameters. This property is called variance.
Generic types have three useful variations. Each of them is associated with a specific generic type; If a type has more than one generic parameter, it does a separate type determination for each parameter.
-
Covariance: For type A
, A
is A subtype of A
if T is A subtype of U. The subtypes of the container match the subtypes of its generic parameters.
-
Contravariance: For type A
, A
is A subtype of A
if T is A subtype of U. The subtype of a container inverts the subtype of its generic parameter.
-
Invariance: With respect to type A
, there is no subtype relation between A
and A
. Containers have no subtypes.
In Rust, since subtypes exist only in the lifetime, and the lifetime represents how long the data is alive, this means:
-
Covariant types are allowed to have longer lifetimes than expected (these lifetimes are allowed to be “shortened”, which is not a problem because references are always used for less time than they are valid).
-
The lifetime of contravariant types is allowed to be prolonged (as in avoiding function Pointers carrying reference types by requiring the lifetime of ‘static instead of ‘a).
-
Immutable types have no subtype relationships and require a lifetime that neither shortens nor lengthens.
Perhaps an example of an inverse lifetime with strong subtype inversion can help to understand:
struct FnHolder {
f: fn(&'static str) - >i32,}fn number_for_name<'a>(name: &'a str) - >i32 {
match name {
"Jim"= >32, _ = >5,}}fn main() {
// Voila! A subtype coercion! In this case coercing a
// lifetime in a contravariant context (the lifetime in
// the function pointer type parameter) from `'a` to `'static`.
//
// `'static` is longer than `'a`, which in this case is safe
// because it's always fine to make the function _less_ accepting.
//
// Once it's been assigned into the `FnHolder` type, it'll only
// accept string literals (which have a `'static` lifetime).
let holder = FnHolder { f: number_for_name };
// The extra parentheses are part of the syntax for calling
// functions as fields, to disambiguate between this and
// calling a method on the `FnHolder` type.
println!("{}", (holder.f)("Jim"));
}
Copy the code
Never turn strong
There is a special type in the Rust type system – the never type (writing!). . This type can be strongly cast to all other types, usually indicating non-termination. For example, unimplemented! , unreachable! And todo!!! Macros are back! Type. ! Type strongholds can take advantage of these macro type checks, if they are performed at runtime. Panic is implemented as a guaranteed panic for the current thread. Exit the STD ::process::exit function of the current process returns! For the same reason.
Never strong-transfer programs can use Panic or exit to pass type checking.
// Turn off some warnings about unreachable code.
#! [allow(unreachable_code)]
#! [allow(unused_variables)]
#! [allow(dead_code)]
struct Value {
x: bool,
y: String,}fn never() -> ! {
// `loop`s without some way to exit
// like this have the `! ` type, because
// the expression (and, in this case,
// the containing function) will never
// terminate / return.
loop{}}fn main() {
let x = never();
letv = Value { x: todo! ("uhhh I haven't gotten to this"),
y: unimplemented!("oh, not this either"),};// This program compiles because `never`,
// `todo! `, and `unimplemented! ` all return
// the `! ` type, which coerces into any type.
}
Copy the code
Strong slice to turn
Slice strength refers to the conversion from array to slice. They are part of the “Unknown Size strong” collection (along with feature object strong and trailing unsized strong). They are so called because they range from types that have size (types whose size is known at compile time and whose Sized characteristics are implemented) to types of unknown size (types whose size is not known at compile time and whose Sized characteristics are not implemented). In the process of slice strong rotation, the type of known size is [T; n](T array with fixed size N), and the type of unknown size is [T](slice of T array).
Slice strength turns happen more often than you might think:
#[derive(Debug)]
struct SliceHolder<'a> {
slice: &'a [i32],}fn main() {
let nums = [1.2.3.4.5];
// It may not look like, but there's a coercion here!
//
// The type of `&nums` is `&[i32; 5]`, which is coerced
// into `&[i32]` to match the `slice` field on `SliceHolder`.
let holder = SliceHolder { slice: &nums };
println!("{: #? }", holder);
}
Copy the code
Note that while it is also possible to turn Vec
strong into &[T], it is not slice strong but dereference strong. Because of the history of the language, arrays cannot be used with const generics (because const generics do not implement Deref), so a special strong transform is required to silently convert them to slices.
Feature object strong rotation
Feature objects are the dynamic scheduling mechanism of Rust, and feature object strong rotation exists to facilitate the construction of feature objects. This strong transformation converts from some type T to dyn U, where U is a feature implemented by T and U satisfies Rust’s object security rules. We’ve discussed object safety rules before, but the point is that the object characteristic type must be constructible (meaning that it does not depend anywhere on generic types that are indeterminate at compile time (generics do not include associated functions, there is no way to refer to Self – indeterminate at compile time, and include no Self: Sized bounds, excluding functions that get Self by value).
An example of a function being called via a feature object coercion:
trait HasInt {
fn get(&self) - >i32;
}
struct IntHolder {
x: i32,}impl HasInt for IntHolder {
fn get(&self) - >i32 {
self.x
}
}
fn print_int(x: &dyn HasInt) {
println!("{}", x.get());
}
fn main() {
let holder = IntHolder { x: 5 };
// The coercion happens here, from `&IntHolder`
// into `&dyn HasInt`.
print_int(&holder);
}
Copy the code
The trailing unsized turn better
Trailing unsized strong means that T can be strong to U if the last field of type T is of a known size and can be cast to an unknown size type, and there is a U type that is of type T but performs strong on the last field. Because this definition is very special, we can specify:
T
It has to be a structureT
The field ofA
Must be strong to unknown sizeB
typeT
The last field of theA
T
Cannot contain other fieldsA
- if
T
The last field is itself containedA
, the structure must be strongly converted to another type that contains an unknown size for substitutionA
theB
Type.
This is more accurate than the original explanation. In essence, limited unsized strong transformations are allowed in a structure body when the related field is the last field.
Minimum upper bound strong turn
Sometimes Rust needs to be strong at multiple strong points at the same time so that they can all become the same type. For example, this could happen in an if/else expression, where each branch of the condition returns a type that needs to be strong. In this case, Rust tries to find the most common type, which is known as “minimum upper bound strong turns.”
The override can be triggered by:
- A series of
if/else
branch - A series of
match
branch - Array elements
- A sequence in a closure
The return value
- A series of functions
The return value
This strong twist is performed by iterating over each type in each series to see if they can be converted to the same type as previously identified. If so, continue. If not, try to find A type C that can be strongly converted from both the previously seen type A and the most recent type B. The final type C is determined to be the type of all expressions in the series.
Strong transfer
Rust supports strong transitivity. If type A can be strongly converted to type B, and type B can be strongly converted to type C, type A can be strongly converted to type C. This feature is currently under development and may not always work.
Where does the strong rotation occur
The location in your code where type coercion occurs is called a strong coercion site. There are many types of coercion in Rust.
Strong point transfer
The first is the declaration of variables, whether by let, const, or static. In these cases, if the variable’s type is explicitly declared on the left, the right side will be strongly cast to that type. If this strong twist is not possible, a compiler error is issued.
Next comes the function argument, where the argument is strongly converted to the type of the type parameter. In method calls, the recipient type (the type of Self) can only use unsized strongholds.
Then you can have literal instances of any structure or enumeration. The point at which fields in these data types are instantiated is the strong conversion point, where the actual type is forced to the official type defined in the overall data type declaration.
Expression for strong propagation
Some expressions are considered strong-propagating, which means they will pass strong-checking to their subexpressions.
Array literals are strongly propagated and are propagated into each element definition of the array literal declaration. If used with a repetition syntax, it repeats the initial definition of the element a given number of times.
Tuples are similarly strongly propagated on each individual expression within them.
If the expression has parentheses, strong conversions propagate to the expression in parentheses. If it is enclosed in parentheses, making it a block, strong turns propagate to the last line of the block.
Unsized Strong turn and strong turn point
Compared to other strong conversions, unsized strong conversions (the above strong conversions to slices, feature objects, or trailing unsized types) can occur in an additional context. Specifically, if there is a reference to type T, a bare pointer or a (owned) pointer, where T has a usized strong cast of type U, then strong casts can be made by reference or pointer type.
This means that the following strong conversions are valid only for unsized strong conversions:
&T
to&U
&mut T
to&mut U
*const T
to*const U
*mut T
to*mut U
Box<T>
toBox<U>
This is why the above example of strong slice rotation works! The strong conversion in this case occurs after the reference, turning [i32; 5] strong to [i32].
conclusion
Strong turns are very powerful because they are implicit and sometimes controversial.
No matter how you feel about the proper use of strong turns, it’s important to understand what strong turns are possible and where they might occur. In this article, we named and described all possible strong spins in Rust and described which types of expressions might contain strong spins and which expressions can propagate strong spins. Hopefully, this helps shed some light on this often hidden part of Rust.