Skip to content

The Rust Engineering Handbook

Appendix E — Conversion and View Trait Matrix

Select Rust conversion, borrowing, dereference, ownership, and collection traits without hiding failure, allocation, lookup semantics, or mutation.

Suppose a library represents a validated network port as Port. This implementation is attractive because it makes .into() available:

struct Port(u16);

impl From<u32> for Port {
    fn from(value: u32) -> Self {
        assert!(value != 0 && value <= u16::MAX as u32);
        Self(value as u16)
    }
}

It is also the wrong contract. From presents the conversion as infallible, but the implementation can panic. Replacing the assertion with truncation would compile and would be worse: the API would silently change the value’s meaning. The honest implementation is TryFrom<u32> for Port, with zero and out-of-range represented as ordinary error cases.

Conversion traits are not interchangeable spelling conveniences. Each answers a different question: Does the source get consumed? Can the operation fail? Is the output owned or borrowed? Must lookup laws remain equivalent? Can mutation trigger allocation? Does the operation create a collection or add to one? Use this matrix by answering those questions before adding a bound.

The matrix

Trait or type Receiver and result Failure Allocation or work contract Select it when Do not use it to mean
From<T> for U consumes T, produces U no ordinary failure channel may do nontrivial work; document cost the conversion is total and meaning-preserving validation, lossy narrowing, or parsing that can fail
Into<U> for T consumes T, produces U no ordinary failure channel same conversion as the reciprocal From implementation a generic caller accepts anything convertible into U a trait library authors normally implement directly
TryFrom<T> for U consumes T, produces Result<U, E> explicit E may validate, allocate, or transform input may be rejected without panic a recoverable operation hidden behind From
AsRef<U> borrows self, produces &U must not fail cheap reference-to-reference view an API needs a view, possibly of one field owned conversion or hash-map lookup equivalence
Borrow<U> borrows self, produces &U must not fail cheap view plus semantic equivalence owned and borrowed key forms share Eq, Ord, and Hash behavior exposing an arbitrary component
Deref<Target = U> borrows pointer-like self, produces &U and participates in coercion/method lookup must not fail should be transparent, cheap access the type is genuinely pointer-like general conversion or inheritance
ToOwned borrows, produces associated owned form no failure channel cloning/allocation may occur a borrowed form such as str needs its owned counterpart a cheap view
Cow<'a, B> stores borrowed &B or <B as ToOwned>::Owned mutation has no error channel to_mut clones only when currently borrowed most inputs pass through, a minority require owned mutation a guarantee of zero allocation
FromIterator<A> consumes an iterator, creates Self trait has no error channel collection construction policy .collect() should build a fresh value updating an existing value
Extend<A> mutably borrows self, consumes an iterator trait has no error channel may reserve, reallocate, or replace duplicate keys add items to an existing value transactional all-or-nothing insertion

The matrix says nothing about whether a particular implementation is fast. From<&str> for String allocates and copies; it is still infallible. AsRef<Path> is normally cheap; the filesystem operation performed after obtaining the path may not be. Trait selection communicates semantic shape, not a universal performance grade.

From and Into: one conversion, two viewpoints

Implement From<T> for U when you own U or otherwise satisfy coherence. The standard library supplies the reciprocal Into<U> for T blanket implementation. At a call site, choose the spelling that makes the destination clear:

let text = String::from("ready");
let text: String = "ready".into();

For a generic input boundary, T: Into<U> is usually the more permissive caller-facing bound. It accepts types with an Into implementation even if they did not obtain it through From. Do not add Into<String> merely to make every API look flexible. Taking &str is often clearer for observation; accepting impl Into<String> is useful when the function needs to retain an owned string and callers may already have one to move.

The design test for From is stronger than “this cannot return Result.” Ask whether every valid source value has one unsurprising destination value and whether the conversion preserves relevant meaning. A numeric narrowing, text parse, validation step, environment lookup, or I/O action normally fails that test. A lossy operation deserves a named method such as saturating_to_u16, truncate, or to_string_lossy so the policy is visible.

From also participates in error composition: the ? operator can convert an underlying error into a function’s declared error through From. That is useful only when the conversion preserves enough cause and context for the boundary. Turning every error into an opaque string is infallible mechanically but weakens diagnosis, matching, and source chains.

TryFrom: validation belongs in the type boundary

The companion fixture uses a nonzero u16 inside Port and distinguishes zero from an out-of-range u32:

impl TryFrom<u32> for Port {
    type Error = PortError;

    fn try_from(value: u32) -> Result<Self, Self::Error> {
        let narrowed = u16::try_from(value).map_err(|_| PortError::OutOfRange)?;
        NonZeroU16::new(narrowed).map(Self).ok_or(PortError::Zero)
    }
}

Implement TryFrom<T> for U; the reciprocal TryInto<U> for T follows. For generic input bounds, TryInto<U> is usually more inclusive. Preserve structured error information when callers need to distinguish repairable input from configuration or programming defects.

Do not make a failed conversion destructive unless consuming the source is itself the intended contract. Some standard conversions return an error that retains original input—for example, invalid UTF-8 conversion from owned bytes can preserve the bytes in its error. For a public boundary, review whether retry, logging, redaction, or recovery needs the original value.

AsRef and Borrow share a signature, not a promise

Both traits can return &T, but Borrow exists largely to connect owned and borrowed lookup forms. A HashMap<String, V> can be queried with &str because String: Borrow<str> and their equality and hashing agree. The same law matters for ordered collections: the owned and borrowed forms must compare consistently.

AsRef carries no such equivalence law. It is the right trait for exposing a cheap view of a field or accepting path-like inputs:

fn load(path: impl AsRef<std::path::Path>) -> std::io::Result<Vec<u8>> {
    std::fs::read(path.as_ref())
}

The fixture’s HeaderName exposes AsRef<str> because callers may need its normalized spelling. It deliberately does not promise Borrow<str> lookup semantics: if the owned type later defines case-insensitive equality while str remains case-sensitive, the laws would disagree. A CanonicalKey whose Eq, Ord, and Hash use its exact stored string can validly implement Borrow<str>.

AsRef must be cheap and infallible. It is not fully reflexive for every type, and it does not recursively dereference every smart pointer. Write bounds from actual supported inputs, then compile consumer-shaped tests. If conversion may fail, use a named method returning Option or Result.

Deref changes the language surface

Deref is more powerful than a view method. Deref coercion can turn &Wrapper into &Target, and method lookup can find methods on the target. That makes Deref appropriate for pointer-like abstractions such as Box<T>, Rc<T>, Arc<T>, String to str, and Vec<T> to [T].

Do not implement Deref merely to save .inner() or .as_ref(). A domain wrapper that dereferences to a representation can expose methods that bypass its vocabulary, create name collisions as dependencies evolve, and make invariants harder to review. DerefMut is an even stronger promise: arbitrary mutable access to the target must not allow callers to violate the wrapper’s invariant. Prefer explicit methods when access is partial, validated, costly, fallible, or semantically surprising.

ToOwned and Cow: ownership on demand

ToOwned generalizes Clone from “&T to T” to a borrowed type and its associated owned form. For str, the owned form is String; for [T] with cloneable elements, it is Vec<T>. Calling to_owned() may allocate and copy.

Cow<'a, B> stores either borrowed data or that owned form. It helps when an operation frequently returns its input unchanged but sometimes must transform it:

fn normalize_ascii(input: &str) -> Cow<'_, str> {
    if input.bytes().any(|byte| byte.is_ascii_uppercase()) {
        Cow::Owned(input.to_ascii_lowercase())
    } else {
        Cow::Borrowed(input)
    }
}

This is not automatically an optimization. Cow adds a branch and API complexity; the changed path still allocates, and repeated mutation after converting to owned has different cost from repeated reconstruction. Use it when profiles or boundary economics support the borrowed fast path, and expose plain owned output when predictable ownership is more valuable.

FromIterator creates; Extend modifies

FromIterator defines construction from a sequence and powers .collect(). Extend adds a sequence to an existing value. A type often implements both, but their jobs differ:

let mut names: MetricNames = ["latency", "errors"]
    .map(str::to_owned)
    .into_iter()
    .collect();
names.extend(["requests".to_owned()]);

Specify duplicate and ordering behavior. Extending a map with an existing key updates its value; extending a set ignores an equivalent duplicate; extending a sequence retains another element. Neither trait provides a general rollback contract. For validation that can fail midway, validate into a temporary value or provide a named transactional method rather than leaving partial mutation ambiguous.

Fallibility may still be encoded in the item and destination type. For example, collecting an iterator of Result<T, E> into Result<Vec<T>, E> stops at the first error through the destination’s FromIterator implementation. That does not turn an arbitrary collection’s own from_iter method into a fallible operation, and it does not give Extend transactional rollback. State where the error channel and partial-progress policy actually live.

Avoid depending on unstable convenience methods in stable baseline code. The core required method of Extend is stable; some provided single-item or reservation hooks can remain unstable in a given toolchain and must not silently enter an MSRV promise.

API-selection drill

For each boundary, write the trait only after stating ownership, failure, equivalence, and cost:

  1. A configuration type accepts a path but does not retain it. Compare &Path with impl AsRef<Path>; list the caller types that justify genericity.
  2. A normalized identifier is used as a HashMap key. Decide whether Borrow<str> laws hold under its equality and hashing policy. If not, design a lookup method that performs explicit normalization.
  3. A parser validates an owned byte buffer. Compare TryFrom<Vec<u8>>, TryFrom<&[u8]>, and a named parser; record whether failure must return the buffer.
  4. A redaction pass changes fewer than one percent of log labels. Measure Cow<str> against always-owned output, including allocation count and call-site complexity.
  5. A collection ingests a batch with duplicate keys and one invalid record. Define whether FromIterator, Extend, or a fallible transactional method can state the required behavior honestly.

The verified crate at examples/rust-engineering-handbook/appendices/conversion-type-selection-lab/ contains accepted tests for all five trait families plus rejected doctests for fallible .into() and treating a path as Unicode text.

Review card

  • Use From only for total, unsurprising value conversion; use a named method for lossy policy.
  • Implement From or TryFrom; use Into or TryInto where a generic caller benefits from broader acceptance.
  • Make validation and narrowing failures explicit through TryFrom or a named fallible operation.
  • Use AsRef for a cheap view; use Borrow only when owned and borrowed comparison and hashing laws agree.
  • Reserve Deref and especially DerefMut for transparent pointer-like access.
  • Treat ToOwned as potentially allocating and Cow as a measured ownership trade, not a zero-copy badge.
  • Use FromIterator to build a fresh value and Extend to mutate an existing one; document duplicates, order, capacity, and partial progress.
  • Keep parsing, I/O, environment access, and lossy conversion out of infallible trait contracts.
  • Preserve consumer-shaped compile tests for public bounds and rejected evidence for failure paths.

Appendix F uses the same contract-first method to choose concrete standard-library types. A correct trait boundary can still hide the wrong collection, pointer, clock, lock, channel, path, or initialization policy.

Sources and version notes

  • The standard library’s std::convert module and the From, TryFrom, and AsRef pages define conversion direction, blanket implementations, failure, and view guidance.
  • Borrow, ToOwned, and Cow document lookup equivalence, owned counterparts, and clone-on-write behavior.
  • Deref documents deref coercion and its effect on method resolution.
  • FromIterator and Extend define fresh construction and in-place extension. Provided unstable methods are not part of this appendix’s stable examples.
  • Examples target Rust 2024. The fixture declares Rust 1.85 as its MSRV; blanket implementations, stabilization annotations, diagnostics, and public bounds require independent revalidation with the publication toolchain and MSRV before acceptance.